Загрузка классов в Java. Практика

    Данная статья является продолжением статьи Загрузка классов в Java. Теория.

    В статье рассмотрена реализация каркаса приложения с плагино-модульной архитектурой. В качестве движка приложения будет использоваться пользовательский загрузчик классов, которым будут загружаться дополнительные плагины приложения.

    Код приложения не претендует на оригинальность, а лишь объясняет подходы и принципы написания пользовательских загрузчиков классов и методы инвокации динамического кода.

    Мотивация



    Зачастую, архитектура сложных систем подразумевает использование механизма динамической загрузки кода. Это бывает необходимо, когда заранее не известно какой именно код будет исполняться в рантайме. Например, всем известная игра для программистов Robocode использует собственный загрузчик классов, для загрузки пользовательских танков в игру. Можно рассматривать отдельный танк как модуль, разрабатываемый изолированно от приложения по заданному интерфейсу. Похожая ситуация рассматривается в статье, только на максимально упрощенном уровне.

    Кроме того, можно привести еще несколько очевидных примеров использования механизма динамической загрузки кода. Допустим байт-код классов хранится в БД. Очевидно, что для загрузки таких классов нужен специальный загрузчик, в обязанности которого будет входить еще и выборка кода классов из БД.

    Возможно, классы требуется загружать по сети/через интернет. Для таких целей нужен загрузчик, способный получать байт-код по одному из сетевых протоколов. Можно также выделить, существующий в Java Class Library URLClassLoader, который способен загружать классы по указанному пути в URL.

    Подготовка



    Реализуемое в рамках статьи приложение будет представлять собой каркас движка для динамической загрузки кода в JRE и его исполнения. Каждый модуль будет представлять собой один Java класс, реализующий интерфейс Module. Общий для всех модулей интерфейс необходим для их инвокации. Здесь, важно понимать, что существует еще один способ исполнения динамического кода — Java Reflection API. Однако, для большей наглядности и простоты будет использоваться модель с общим интерфейсом.

    При реализации пользовательских загрузчиков важно помнить следующее:
    1) любой загрузчик должен явно или неявно расширять класс java.lang.ClassLoader;
    2) любой загрузчик должен поддерживать модель делегирования загрузки, образуя иерархию;
    3) в классе java.lang.ClassLoader уже реализован метод непосредственной загрузки — defineClass(...), который байт-код преобразует в java.lang.Class, осуществляя его валидацию;
    4) механизм рекурентного поиска также реализован в классе java.lang.ClassLoader и заботиться об это не нужно;
    5) для корректной реализации загрузчика достаточно лишь переопределить метод findClass() класса java.lang.ClassLoader.

    Рассмотрим детально поведение загрузчика классов при вызове метода loadClass() для объяснения последнего пункта вышеуказанного списка.

    Реализация по-умолчанию подразумевает следующую последовательность действий:
    1) вызов findLoadedClass() для поиска загружаемого класса в кеше;
    2) если класса в кеше не оказалось, происходит вызов getParent().loadClass() для делегирования права загрузки родительскому загрузчику;
    3) если иерархия родительских загрузчиков не смогла загрузить класс, происходит вызов findClass() для непосредственной загрузки класса.

    Поэтому для правильной реализации загрузчиков рекомендуется придерживаться указанного сценария — переопределения метода findClass().

    Реализация



    Определим интерфейс модулей. Пусть модуль сначала загружается (load), потом исполняется (run), возвращая результат и затем уже выгружается (unload). Данный код представляет собой API для разработки модулей. Его можно скомпилировать отдельно и упаковать в *.jar для поставки отдельно от основного приложения.

    public interface Module {
      
      public static final int EXIT_SUCCESS = 0;
      public static final int EXIT_FAILURE = 1;
      
      public void load();
      public int run();
      public void unload();

    }

    * This source code was highlighted with Source Code Highlighter.


    Рассмотрим реализацию загрузчика модулей. Данный загрузчик загружает код классов из определенной директории, путь к которой указан в переменной pathtobin.
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.InputStream;

    public class ModuleLoader extends ClassLoader {
      
      /**
       * Путь до директории с модулями.
       */
      private String pathtobin;
      
      public ModuleLoader(String pathtobin, ClassLoader parent) {
        super(parent);    
        this.pathtobin = pathtobin;    
      }

      @Override
      public Class<?> findClass(String className) throws ClassNotFoundException {
        try {
          /**
           * Получем байт-код из файла и загружаем класс в рантайм
           */
          byte b[] = fetchClassFromFS(pathtobin + className + ".class");
          return defineClass(className, b, 0, b.length);
        } catch (FileNotFoundException ex) {
          return super.findClass(className);
        } catch (IOException ex) {
          return super.findClass(className);
        }
        
      }
      
      /**
       * Взято из www.java-tips.org/java-se-tips/java.io/reading-a-file-into-a-byte-array.html
       */
      private byte[] fetchClassFromFS(String path) throws FileNotFoundException, IOException {
        InputStream is = new FileInputStream(new File(path));
        
        // Get the size of the file
        long length = new File(path).length();
      
        if (length > Integer.MAX_VALUE) {
          // File is too large
        }
      
        // Create the byte array to hold the data
        byte[] bytes = new byte[(int)length];
      
        // Read in the bytes
        int offset = 0;
        int numRead = 0;
        while (offset < bytes.length
            && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
          offset += numRead;
        }
      
        // Ensure all the bytes have been read in
        if (offset < bytes.length) {
          throw new IOException("Could not completely read file "+path);
        }
      
        // Close the input stream and return bytes
        is.close();
        return bytes;

      }
    }

    * This source code was highlighted with Source Code Highlighter.


    Теперь рассмотрим реализацию движка загрузки модулей. Директория с модулями (файлами .class) указывается в качестве параметра приложению.

    import java.io.File;

    public class ModuleEngine {
      
      public static void main(String args[]) {
        String modulePath = args[0];
        /**
         * Создаем загрузчик модулей.
         */
        ModuleLoader loader = new ModuleLoader(modulePath, ClassLoader.getSystemClassLoader());
        
        /**
         * Получаем список доступных модулей.
         */
        File dir = new File(modulePath);
        String[] modules = dir.list();
        
        /**
         * Загружаем и исполняем каждый модуль.
         */
        for (String module: modules) {
          try {
            String moduleName = module.split(".class")[0];
            Class clazz = loader.loadClass(moduleName);
            Module execute = (Module) clazz.newInstance();
            
            execute.load();
            execute.run();
            execute.unload();
            
          } catch (ClassNotFoundException e) {
            e.printStackTrace();
          } catch (InstantiationException e) {
            e.printStackTrace();
          } catch (IllegalAccessException e) {
            e.printStackTrace();
          }
        }
        
        
      }

    }

    * This source code was highlighted with Source Code Highlighter.


    Реализуем простейший модуль, который просто печатает на стандартный вывод информацию о стадиях своего исполнения. Это можно сделать в отдельном приложении добавив к CLASSPATH путь до скомпилированного .jar файла c классом Module (API).

    public class ModulePrinter implements Module {

      @Override
      public void load() {
        System.out.println("Module " + this.getClass() + " loading ...");
      }

      @Override
      public int run() {
        System.out.println("Module " + this.getClass() + " running ...");
        return Module.EXIT_SUCCESS;
      }

      @Override
      public void unload() {
        System.out.println("Module " + this.getClass() + " inloading ...");    
      }
    }

    * This source code was highlighted with Source Code Highlighter.


    Скомпилировав данный код, результат в виде одного class файла можно скопировать в отдельную директорию, путь к которой необходимо указать в качестве параметра основного приложения.

    Немного иронии


    Динамическая загрузка кода, отличный и легальный способ предать обязанности по расширению системы пользователю ;)
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 19
    • +1
      продолжение будет? к примеру выгрузка старого модуля и загрузка нового? описание каких-то других нетривиальных особенностей?
      • +2
        на практике, к сожалению, часто бывает OutOfMemoryError: PermGen space
        • 0
          причина явно не в принципе динамической реализации загрузки классов, а в частной реализации
          • +1
            При любой реализации после загрузки/выгрузки определенного количества более-менее сложного кода случится Perm.
            • 0
              у меня в одном из проектов есть реализация сложного механизма класс-лоадеров, все работает замечательно. Главное правильно сделать все. Т.е. проблема решается. К примеру, в Rhino (имплементация javascript на java) была указанная вами проблема, однако, залез внутрь переписал одно из мест, работает отлично.
              • 0
                А что именно в Rhino приводило к проблеме? Сам именно в Rhino не столкнулся, хотя и предполагал, что может начаться.
                • 0
                  при каждом вызове скрипта Rhino генерит один из классов который отвечает за работу оператора print, причем этот класс постоянно имеет новое имя. При периодическом вызове скрипта каждую секунду, сервак падал с ошибкой OutOfMemoryError, после рефактиринга работает многие сутки напролет.
                  • –1
                    Шикарный баг! С Rhino должен спасать еще и небольшой размер сгенерированных классов.
                    • 0
                      я думаю, размер особого значения не имеет, при длительной работе при наличии утечки памяти даже самый маленький класс приведет к OOME, а при правильной реализации и большие классы отрабатывают отлично.
                      Интересно было бы почтить как автор решает проблему полной перегрузки ранее загруженных классов…
            • 0
              Или вы имеете в виду частную реализацию JVM? Тогда да, но ни одна из распространенных этой проблемы не избежала.
              • 0
                Говорят JRockIt умеет. Думаете маркетинг?
                • 0
                  Сам не сталкивался, однако знаю от коллег, что многократный ридиплой в аппсервер на ней так же приводит к ошибке. Даже если там нет прямого эквивалента сановского Perm, что-то приводит к аналогичному сбою управления кучей (или где там у них лежат классы).

                  Считаю нужным пояснить, что в первом комментарии я скорее имел в виду, что JVM склонны давать такую ошибку не смотря на принимаемые меры в виде аккуратного кодирования, правильной работы с загрузчиками и настройки машины свичами и т.п. Как показывает практика, эти меры имеют разный эффект для разных машин, и проблема скорее лежит в том, что поведение загрузчиков не специфицировано должным образом. Поэтому динамическая подгрузка классов может быть компромиссом между надежностью системы и сложностью развертывания, и в некоторых случаях неизбежна.
                  • 0
                    Нет. В Hotspot объем class metadata ограничен размером Permanent Generation (по умолчанию 64Mb). В JRockit объем class metadata не ограничен, т.е. классы будут загружаться, пока есть свободная физическая и/или виртуальная память. Таким образом, JRockit лишь откладывает проявление утечек памяти, но не избавляет от них насовсем.
              • +1
                на практике, к сожалению, часто бывает OutOfMemoryError: PermGen space

                Только в случае утечки памяти. Но на практике, тут Вы правы, утечки памяти бывают часто, даже у опытных Java программистов. Тем не менее, при выгрузке классов Hotspot вычищает из PermGen ВСЕ, что к ним относится, т.е. приложение с правильной организацией динамической загрузки может работать сколь угодно долго.

                P.S. Кстати, хорошая новость: скоро PermGen исчезнет из Hotspot'а насовсем.
                • +1
                  Стоит отметить, что выгрузка классов производится только после того, как соответствующий загрузчик стал недостижим.

                  Поскольку в данном примере загрузчик живет все время работы приложения, то вероятность такой ошибки при большом числе модулей достаточно велика.

                  Чтобы ее не происходил можно, например, менять загрузчик после загрузки каждых N модулей.
              • +4
                Не очень понятна семантика Module.unload(), а вообще да, пишите продолжение. Не забудьте про зависимости между модулями и грабли в виде ClassCastException. И ссылку на OSGi или что-нибудь подобное.
                • 0
                  Будет вам еще одна статья :)
                • 0
                  Эх… помню, как в зеленой юности писал свой загрузчик с собственным кэшем загруженных классов.
                  Потом почитал спецификацию java машины, и все переделал в несколько строк, примерно как в этой статье =)
                  • 0
                    Если кому интересно, то я когда-то по теме заметку опубликовал — «Java :: classpath менеджмент во время выполнения» — itfreak.ru/21

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.