Статьи

JRebel Unloaded

Добро пожаловать во второй выпуск серии  Discotek.ca  по разработке байт-кода. Первая статья, обзор разработки байт-кода, может быть найдена  здесь .

JRebel бесспорно , является в отрасли  перегрузочного класса  программного обеспечения. Это полезный продукт заработал свою репутацию, помогая ускорить разработку Java для многих организаций. Как работает этот продукт — загадка для большинства. Я хотел бы объяснить, как я думаю, что это работает и предоставить  базовый прототип  (с  исходным кодом ).

Со времени принятия серверов приложений для изоляции бизнес-логики от универсальной логики подключения разработчики страдали от трудоемкого процесса создания и повторного развертывания перед тестированием изменений кода на стороне сервера. Чем больше приложение, тем длиннее цикл сборки / повторного развертывания. Для разработчика, который часто тестирует, время, потраченное на сборку и перенос, может занять значительную часть рабочего дня. Фактическая стоимость проекта может быть приравнена к числу разработчиков * зарплата / в час * количество часов, потраченных на строительство и повторное развертывание. Эта цифра не должна быть просто стоимость ведения бизнеса.

Некоторое время назад, когда я изучал инструментарий, я написал продукт под названием  Feenix , который, как я думал, поможет людям преодолеть ту же перезагрузку классов, что и JRebel, но этого не произошло. Продукт все еще существует на моем веб-сайте, но я сомневаюсь, что кто-нибудь на самом деле его использует. Пока я держу это там как болезненное напоминание о моей неудаче, которая должна вдохновить меня на создание лучшей. Я не понимал, почему мой продукт потерпел неудачу, пока  Антон Архипов , автор JRebel, не  дал несколько проницательных критических замечаний :

Feenix может сделать столько, сколько позволяет API Java Instrumentation API. Что в основном означает, что это не добавляет ценности по сравнению со стандартным HotSwap JVM. 

Существует несколько продуктов, которые предоставляют механизм изменения функциональности классов в работающей JVM, но не все они созданы равными. Вероятно, наиболее известным является встроенная горячая замена Java, которой IDE, как Eclipse, пользуются в режиме отладки. Другие, такие как Feenix, используют встроенный инструментарий Java API. Из-за ограничений JVM большинство из этих попыток терпят неудачу. В частности, JVM ограничивает типы изменений, разрешенных для загруженного класса. Например, JVM не позволит вам изменить схему класса. Это означает, что вы не можете изменить количество полей или методов или их подписей. Вы также не можете изменить иерархию наследования. Они также не могут изменить поведение существующих объектов. К сожалению, это резко снижает полезность этих продуктов.

Введите JRebel. JRebel, по-видимому, является самым функциональным и высоко оцениваемым продуктом класса перезарядки на рынке. У него очень мало недостатков, и, похоже, он очень хорошо поддерживается. JRebel является коммерческим продуктом и, вероятно, будет чрезмерно дорогим для большинства разработчиков, которые платят за инструменты из своего кармана. Сторонники JRebel опубликовали несколько статей, в которых обсуждается, как они решили различные проблемы перезагрузки классов, но, поскольку они являются коммерческим продуктом, они, естественно, не обсуждают реализацию в деталях. Знание деталей может привести к появлению альтернативного продукта с открытым исходным кодом. Если будет достаточно интереса, я интегрирую перезагрузку класса стиля JRebel в Feenix и открою его.

Создание механизма перезагрузки классов (CRM) должно решить несколько проблем:

  1. CRM должен знать, где находятся новые версии классов. Эти классы могут находиться на локальном диске или в удаленном месте. Они могут быть связаны в банке, войне или ухе.
  2. Хотя технически это не классовая загрузка, CRM также должен поддерживать перезагрузку неклассовых ресурсов, таких как изображения или HTML-файлы.
  3. CRM должен гарантировать, что когда загрузчик классов загружает класс в первый раз, он загружает последнюю версию. Несмотря на то, что класс уже загружен загрузчиком классов, CRM должен гарантировать, что новые экземпляры класса будут использовать функциональные возможности последней версии класса.
  4. CRM должен гарантировать, что функциональность существующих объектов должна использовать функциональность последней версии своего класса.
  5. Хотя перезагрузка классов, несомненно, является основной функциональностью, требуемой для любого CRM, во многих приложениях используются общие платформы, для изменения конфигурации которых потребуется цикл сборки / повторного развертывания. Эти изменения должны быть менее частыми, чем изменения кода, но все же имеет смысл обеспечить функциональность такой перезагрузки.

Четвертая проблема выше затмевает другие с точки зрения сложности, но и полезности. Для серверов приложений дешевле использовать повторно объединенные объекты, чем всегда создавать новые экземпляры. Если CRM не может сообщать объединенным экземплярам об изменениях класса, это будет служить очень малой цели. Разработчики JRebel заявляют, что для решения этих проблем они используют «управление версиями классов», но оставляют много места для интерпретации реализации. Мы знаем, что загрузчики классов могут загружать класс только один раз. Исключением из этого правила является инструментарий, но мы знаем, что это не то, как JRebel решил эту проблему (в основном потому, что они открыто об этом, но также), потому что инструментарий не позволит изменить схему класса. Другой подход к проектированию CRM обычно известен как «одноразовые загрузчики классов»,который использует новый загрузчик классов для загрузки каждой новой версии класса. Эта конструкция имеет много недостатков, но, прежде всего, не может решить проблему введения новых функциональных возможностей в существующие объекты.

Чтобы представить новые функциональные возможности существующим объектам, их выполнение должно быть передано методу, который содержит новые функциональные возможности. Поскольку загрузчик классов может загрузить данный класс только один раз, новая функциональность должна быть размещена в классе с новым уникальным именем. Однако класс не может знать имя своего преемника во время компиляции или выполнения. Мы можем использовать инструментарий для изменения класса по мере его загрузки, но мы не будем знать имена его преемников, пока CRM не обнаружит новые скомпилированные классы и не сделает их доступными для JVM. Для пересылки выполнения его преемнику можно использовать два механизма: отражение или интерфейс. Reflection может проверять методы класса и вызывать метод с соответствующим именем и сигнатурой. Известно, что отражение медленное и не подходит для каждого вызова метода. С другой стороны,может быть создан интерфейс, который определяет метод, позволяющий в общем случае вызывать любой метод в классе наследника. Такой метод может иметь следующие имя и подпись:

public Object invoke(int methodId, Object invoker, Object args[]);

Если более новая версия данного класса реализует этот интерфейс, выполнение может быть передано соответствующему методу. Параметр  methodId  используется для определения метода. Параметр  invoker  обеспечивает доступ к состоянию (полям) исходного объекта, а   параметр args предоставляет новому методу доступ к аргументам исходного метода.

Рабочее решение имеет гораздо больше движущихся частей, чем приведенный выше контур. Это также вводит две дополнительные проблемы для решения. Каждый вызов метода перезагруженного объекта создает дополнительный неожиданный кадр в стеке, что может сбивать с толку разработчиков. Любое использование отражения в перезагруженных классах может вести себя некорректно (учитывая, что имя класса изменилось и   был добавлен метод invoke , иерархия наследования не существует и т. Д.). Выявление таких проблем важно, а также предоставление рабочих решений. Решение всех вышеперечисленных проблем в одной статье, вероятно, приведет к тяжелым векам. Вместо этого давайте сосредоточимся на элементарной реализации функциональности пересылки классов. Мы всегда можем вернуться к другим вопросам в другой статье, если есть интерес.

В этой статье будут рассмотрены следующие функциональные части механизма перезагрузки классов:

  1. Центральный компонент для обнаружения и управления версиями классов
  2. Создайте класс-преемник и интерфейс для ссылки на него
  3. Изменить класс приложения для пересылки вызовов методов его преемникам
  4. Измените java.lang.ClassLoader для установки вышеуказанных функций.

Прежде чем углубляться в детали, я хотел бы предупредить вас, что я переписал эту статью дважды. Несмотря на мой живой интерес к проектированию байт-кода, даже мне было до слез скучно писать объяснения кода ASM. Следовательно, этот третий и, надеюсь, окончательный проект будет содержать гораздо меньше кода ASM, чем другие. В нем будет больше внимания тому, как работает перезагрузка классов, но вы всегда можете обратиться к исходному коду в разделе Ресурсы,  чтобы увидеть подробности реализации.

Класс перегрузочного механизма Design

Менеджер версий классов (AKA ClassManager) будет иметь несколько заданий:

  • Загрузите конфигурацию, которая указывает пространство имен для перезагрузки и где их найти
  • Определите, устарела ли версия класса
  • Укажите байт-код для:

    • новые версии данного класса
    • универсальный вызываемый интерфейсный класс
    • класс реализации интерфейса (который содержит новую функциональность)

Если я подробно остановлюсь на всем вышеперечисленном, эта статья будет длиннее, чем «  Война и мир» . Вместо этого я замаскирую детали, которые не имеют прямого отношения к разработке байт-кода. Для получения подробной информации
о конфигурации вы можете обратиться к  ca.discotek.feenix.Configuraton  и статическому инициализатору  ca.discotek.feenix.ClassManager . Вот пример файла конфигурации:

<feenix-configuration project-name="example">
    <classpath>
        <entry>C:/eclipse/workspace/my-project/bin</entry>

        <!-- alternatively, you can use jar, war, and ear files -->
        <entry>C:/eclipse/workspace/my-project/dist/example.jar</entry>
        <entry>C:/eclipse/workspace/my-project/dist/example.war</entry>
        <entry>C:/eclipse/workspace/my-project/dist/example.ear</entry>

        <!--  Use the exclude tag to exclude namespaces. It uses a Java regular expression. -->
        <exclude>ca\.discotek\.feenix2\.example\.Example</exclude>
    </classpath>
</feenix-configuration>

Чтобы указать расположение файла конфигурации, используйте   системное свойство feenix-config, чтобы указать полный путь.

Чтобы определить, является ли класс устаревшим, мы будем использовать следующий код, найденный в  ca.discotek.feenix.ClassManager :

static Map<String, Long> classTimestampMap = new HashMap<String, Long>();

static boolean isOutDated(String className, long timestamp) {
    Long l = classTimestampMap.get(className);
    if (l == null) {
        classTimestampMap.put(className, timestamp);
        return false;
    }
    else {
        classTimestampMap.put(className, timestamp);
        return timestamp > l;
    }
}

Вызывающая сторона передает имя класса и отметку времени класса, который они хотят проверить.

Последняя задача диспетчера классов — предоставить байт-код класса, но давайте сначала вернемся к тому, как классы будут перезагружаться. Одним из важных шагов является переопределение  класса java.lang.ClassLoader JVM,  чтобы он мог обрабатывать классы приложений по мере их загрузки. Каждый класс приложения будет иметь следующую функциональность, вставленную в начало каждого метода:  если существует новая версия класса, перенаправьте выполнение соответствующему методу в экземпляре этого нового класса . Давайте посмотрим ближе на простой пример класса приложения:

class Printer {
    public void printMessage(String message) {
        System.out.println(message);
    }
}

Вышеуказанный класс будет оснащаться нашим специальным java.lang.ClassLoader, чтобы он выглядел примерно так: 

class Printer {

    Printer_interface printerInterface = null;

    static void check_update() {
        Printer_interface localPrinterInterface = ClassManager.getUpdate(ca.discotek.feenix.example.Printer.class);
        if (localPrinterInterface != null)
            printerInterface = localPrinterInterface;
    }

    public void printMessage(String message) {
        check_update();
        if (printerInterface != null) {
            printerInterface.invoke(0, this, new Object[]{message});
            return;
        }
        else {
            System.out.println(message);
        }
    }
}

Модифицированная версия класса Print имеет следующие изменения:

  • Добавлено  поле Printer_interface printerInterface .
  • Метод check_update  был добавлен.
  • Метод printMessage теперь имеет логику:

    1. Проверьте наличие обновлений класса
    2. Если обновление существует, вызовите соответствующий метод в новом классе.
    3. В противном случае выполните исходный код

Метод  check_update  вызывает  ClassManager.getUpdate (…) . Этот метод определит, доступно ли обновление, и если да, сгенерирует новый класс реализации:

public static Object getUpdate(Class type) {
    String dotClassName = type.getName();
    String slashClassName = dotClassName.replace('.', '/');

    File file = db.getFile(slashClassName + ".class");
    if (file != null && file.isFile()) {
        long lastModified = file.lastModified();
        if (isOutDated(dotClassName, lastModified)) {
            String newName = slashClassName + IMPLEMENTATION_SUFFIX + getNextVersion(slashClassName);
            byte bytes[] = getClassBytes(newName);
            try {
                Method method = ClassLoader.class.getDeclaredMethod("defineMyClass", new Class[]{String.class, byte[].class});
                Class newType = (Class) method.invoke(type.getClassLoader(), new Object[]{newName.replace('/', '.'), bytes});
                return newType.newInstance();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    return null;
}

Как только  getUpdate (…)  вызвал  ClassManager.getClassBytes (…)  для получения необработанных байтов, представляющих класс, он будет использовать отражение для вызова   метода defineMyClass в  java.lang.ClassLoaderdefineMyClass  — это метод, который мы добавим позже, когда создадим пользовательский   класс java.lang.ClassLoader . Чтобы преобразовать необработанные байты в   объект java.lang.Class , вам необходимо иметь доступ к   методам defineClass в  java.lang.ClassLoader , но все они ограничены  защищенным  доступом. Следовательно, мы добавляем наш собственный  открытый  метод, который будет перенаправлять вызов  defineClass метод. Нам нужно получить доступ к методу, используя отражение, поскольку оно существует во время компиляции.

Модифицированный   класс Printer представляет класс  Printer_interface,  а метод  ClassManager.getUpdate (…)  представляет новую версию   класса PrinterPrinter_impl_0 , которая реализует   интерфейсный класс Printer_interface . Эти классы не будут существовать в пути к классам приложения, поскольку они генерируются во время выполнения. Мы будем переопределять  java.lang.Classloader «s  loadClass  методы для вызова  getUpdate (…)  назвал ClassManager.getClassBytes (…) ,  чтобы открыть новые версии наших классов приложений и генерации интерфейса и реализации классов по мере необходимости. Здесь getUpdate (…)  вызвал  метод getClassBytes (…) :

public static byte[] getClassBytes(String slashClassName) {
    if (isInterface(slashClassName))
        return InterfaceGenerator.generate(slashClassName, trimInterfaceSuffix(slashClassName));
    else if (isImplementation(slashClassName)) {
        String rootClassName = trimImplementationSuffix(slashClassName);
        File file = db.getFile(rootClassName.replace('.', '/') + ".class");
        if (file != null)
            return ImplementationGenerator.generate(slashClassName, file);
    }
    else {
        File file = db.getFile(slashClassName + ".class");
        if (file != null)
            return ModifyClassVisitor.generate(slashClassName, file);
    }

    return null;
}

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

Если запрошенный класс предназначен для класса интерфейса, который реализует класс реализации,  InterfaceGenerator.generate (…)  вызывается для генерации класса интерфейса. Вот метод вызова сгенерированного интерфейса для   примера Printer :

public java.lang.Object __invoke__(int index, ca.discotek.feenix.example.gui.Printer__interface__, java.lang.Object[]) 

Класс  реализацииGenerator  используется для генерации класса, который реализует интерфейс, созданный InterfaceGenerator. Этот класс больше и сложнее, чем InterfaceGenerator. Он выполняет следующие работы:

  1. Создает необработанный байтовый код для класса с новым пространством имен. Имя будет таким же, как и оригинал, но с добавленным уникальным суффиксом.
  2. Он копирует все методы из исходного класса, но преобразует методы инициализатора в обычные методы с именем метода  __init__  и статическими именами инициализатора в  __clinit__ .
  3. Для нестатических методов добавляется параметр типа < interface, созданный InterfaceGenerator >.
  4. Изменяет нестатические методы, которые работают с  этим,  чтобы работать с параметром, добавленным в предыдущем пункте.
  5. Для конструкторов он удаляет вызовы  super. <Init> . Обычные методы не могут вызывать инициализаторы экземпляров.

В  InterfaceGenerator  и  ImplementationGenerator  классы бесполезны без возможности изменять классы приложения , чтобы воспользоваться ими. ModifyClassVisitor  делает эту работу. Он добавляет   метод check_update и модифицирует каждый метод так, что он будет проверять наличие обновленных версий классов и переадресовывать выполнение тем, если они существуют. Это также меняет все поля на  общедоступные  и  не финальные, Это необходимо, чтобы к ним могли обращаться классы реализации. Эти атрибуты наиболее функциональны во время компиляции, но, конечно, эти изменения могут повлиять на приложения, использующие отражение. Решение этой проблемы нужно будет сейчас включить в список дел, но я подозреваю, что это не так уж сложно. Решение, вероятно, включает в себя правильное переопределение классов отражения классов JRE (кстати, оно также может решить проблемы, возникающие в результате использования отражения в отношении методов и полей, которые мы добавили в классы приложений).

Давайте теперь обсудим, как изменить  java.lang.ClassLoader . JRebel генерирует загрузочную флягу, которая содержит новый   класс java.lang.ClassLoader (среди прочих) и заменяет java.lang.ClassLoader JRE с   помощью параметра -Xbootclasspath / p: JVM   . Мы также воспользуемся этим подходом, но вы должны заметить, что вам, вероятно, придется выполнять эту задачу для каждой версии целевой JVM, которую вы хотите запустить. Между версиями могут быть внутренние изменения API, которые нарушили бы совместимость, если бы вы использовали сгенерированный   класс ClassLoader из JRE X с JRE Y.

Чтобы сгенерировать новый  java.lang.ClassLoader , я создал три класса:

ClassLoaderGenerator  выполняет несколько основных задач. Это точка входа в программу. Его основной метод требует путь к файлу rt.jar целевой JRE и выходной каталог. Он тянет необработанные байты из rt.jar в  java.lang.ClassLoader , он вызывает  ClassLoaderClassVisitor  производить необработанные байты нашего модифицированного  java.lang.ClassLoader , а затем свяжите эти байты в виде  Java / языках / ClassLoader.class  записи из Финикса-classloader.jar  файла, который является осажденным на указанный выходной каталог.

ClassLoaderClassVisitor  использует ASM для непосредственного внесения изменений в байт-код, но также извлекает необработанный байт-код из  ClassLoaderTargeted . В частности, я написал методы в  ClassLoaderTargeted,  которые я хотел бы появиться в сгенерированной версии  java.lang.ClassLoader, Хотя мне и нравится писать инструкции для байт-кода напрямую с ASM, это может быть очень утомительно, особенно если вы постоянно вносите изменения по мере своего развития. При написании кода на Java этот процесс становится более похожим на обычную разработку Java (в отличие от разработки на уровне байтового кода). Этот подход может заставить некоторых людей сказать «Но почему бы не использовать Asmifier» для генерации кода ASM для вас? Этот подход, вероятно, находится на полпути между моим подходом и написанием кода ASM с нуля, но запуск ASM и копирование сгенерированного кода в ClassLoaderClassVisitor тоже  довольно утомительная работа.

Давайте посмотрим под капот  ClassLoaderClassVisitor . Первую работу он будет делать , будет переименовать  defineClass  и  loadClass  методы (мы добавим наши собственные  defineClass  и  loadClass  методы позже):

public MethodVisitor visitMethod(int access,
        String name,
        String desc,
        String signature,
        String[] exceptions) {

    MethodVisitor mv = super.visitMethod(access, METHOD_NAME_UTIL.processName(name), desc, signature, exceptions);
    if (name.equals(LOAD_CLASS_METHOD_NAME) && desc.equals("(Ljava/lang/String;)Ljava/lang/Class;"))
        return new InvokeMethodNameReplacerMethodVisitor(mv, methodNameUtil);
    else if (name.equals(DEFINE_CLASS_METHOD_NAME))
        return new InvokeMethodNameReplacerMethodVisitor(mv, methodNameUtil);
    else
        return mv;
}

Метод  visitMethod  строки 7 вызывается для каждого метода, определенного в  java.lang.ClassLoader . METHOD_NAME_UTIL — это объект, который инициализируется для замены строк, соответствующих «defineClass» или «loadClass» с тем же именем, но с префиксом «_feenix_».  ClassLoader в  loadClass (имя String)  метод вызывает  loadClass (имя String, логическое разрешаемыми)  Строки 8-9 используется для обновления каких — либо инструкций методы в новой _feenix_loadClass (имени String)  метод таким образом, что  _feenix_loadClass (имя String, логическое решительность)  вызываются вместо. Точно так же строки 10-11 гарантируют, что новые   методы _feenix_defineClass всегда будут вызывать другие  _feenix_defineClass методы, а не   методы defineClass .

Другая интересная часть  ClassLoaderClassVisitor  — это   метод visitEnd :

public void visitEnd() {
    try {
        InputStream is =
            Thread.currentThread().getContextClassLoader().getResourceAsStream(ClassLoaderTargeted.class.getName().replace('.', '/') + ".class");
        ClassReader cr = new ClassReader(is);
        ClassNode node = new UpdateMethodInvocationsClassNode();
        cr.accept(node, ClassReader.SKIP_FRAMES);

        Iterator<MethodNode> it = node.methods.listIterator();
        MethodNode method;
        String exceptions[];
        while (it.hasNext()) {
            method = it.next();
            if (method.name.equals(DEFINE_CLASS_METHOD_NAME) ||
                method.name.equals(LOAD_CLASS_METHOD_NAME) ||
                method.name.equals(DEFINE_MY_CLASS_METHOD_NAME)) {

                exceptions = method.exceptions == null ? null : method.exceptions.toArray(new String[method.exceptions.size()]);
                MethodVisitor mv = super.visitMethod(method.access, method.name, method.desc, method.signature, exceptions);
                method.accept(mv);
            }
        }
    }
    catch (Exception e) {
        throw new Error("Unable to create classloader.", e);
    }

    super.visitEnd();
}

Этот метод читает все методы, определенные в  ClassLoaderTargeted,  и добавляет методы, которые мы хотим (некоторые только там, чтобы скомпилировать) в наш  java.lang.ClassLoader . Все нужные нам методы — это  методы defineClassloadClass и  defineMyClass  . С ними есть только одна проблема: некоторые инструкции методов в этих классах будут работать с  ClassLoaderTargeted , а не с  java.lang.ClassLoader , поэтому нам нужно просмотреть каждую инструкцию метода и настроить ее соответствующим образом. Вы заметите, что в строке 6 мы используем   объект UpdateMethodInvocationsClassNode для чтения  ClassLoaderTargeted байт код Этот класс будет обновлять инструкции метода по мере необходимости.

Класс перезарядки в действии

Чтобы попробовать Feenix 2.0 (кстати, я называю его 2.0, чтобы отличать его от оригинальной версии 1.0, но ни в коем случае не следует считать это полностью функционирующим финальным выпуском), сделайте следующее:

  1. Загрузите  дистрибутив Feenix 2.0  и распакуйте архив . Допустим, вы поместили его в  /projects/feenix-2.0 .
  2. Предположим, что ваша целевая JVM находится в  /java/jdk1.7.0 . Выполните следующую команду , чтобы создать  Финикс-classloader.jar  файл в  /projects/feenix-2.0  каталоге:
/java/jdk1.7.0/bin/java -jar /projects/feenix-2.0/discotek.feenix-2.0.jar /java/jdk1.7.0/jre/lib/rt.jar /projects/feenix-2.0 
  1. Загрузите  пример проекта  в каталог / projects / feenix-example и распакуйте в этот каталог.
  2. Создайте проект в вашей любимой среде IDE, который вы будете использовать для редактирования примера кода проекта.
  3. Настройте файл /projects/feenix-example/feenix.xml так, чтобы он указывал на каталог, в котором содержатся скомпилированные классы проекта. Если вы Eclipse, вы, вероятно, можете пропустить этот шаг, так как он уже указывает на  каталог bin проекта  .
  4. Используя вашу IDE, запустите  ca.discotek.feenix.example.Example  со следующими параметрами JVM:
 -Xbootclasspath/p:C:\projects\feenix-2.0\feenix-classloader.jar;C:\projects\feenix-2.0\discotek.feenix-2.0.jar -noverify -Dfeenix-config=C:\projects\feenix-example\cfg\feenix.xml
  1. Появится окно с тремя кнопками. Нажмите каждую кнопку, чтобы создать базовый текст.

    1. Печать с существующего принтера . Демонстрирует, как вы можете изменить функциональность для существующего объекта.
    2. Печать с нового принтера . Демонстрирует, как вы можете изменить функциональность для новых объектов.
    3. Печать статическая . Демонстрирует, как вы можете изменить функциональность статического метода.
  2. Перейдите к   классу ca.discotek.feenix.example.gui.Printer и измените текст для   поля сообщения . Перейдите к  ca.discotek.feenix.example.gui.ExampleGui  и измените параметр String в  Printer.printStatic . Сохраните изменения, чтобы среда IDE компилировала новые классы.
  3. Снова нажмите каждую кнопку в окне и наблюдайте за изменениями.

Это завершает наше исследование по перезагрузке класса. Следует помнить, что эта демонстрация является проверкой концепции и может не работать должным образом с вашим собственным кодом проекта (он не был тщательно протестирован). Также следует учитывать следующие моменты:

  • Следует отметить, что параметр JVM -noverify необходим для перезагрузки конструкторов.
  • Код для переопределения  java.lang.ClassLoader  не переопределяет  defineTransformedClass .
  • Есть еще некоторые нерешенные вопросы (в основном связанные с рефлексией).
  • Существует все еще большая проблема с доступом к полям или методам, которые существуют только в новых версиях класса.
  • Следует рассмотреть возможность использования  синтетического  модификатора для любых сгенерированных полей или методов.
  • Feenix использует восстановленную копию  ASM . Он перераспределяется с  префиксом  пакета ca.discotek.rebundled , чтобы избежать столкновений классов, когда приложение требует ASM на пути к классам для своих собственных целей.
  • Некоторые из целей Механизма перезагрузки классов, перечисленных во введении, не были решены (не перезагружать ресурсы, не относящиеся к классам, или файлы конфигурации инфраструктуры).

Ресурсы

Следующий блог в сериале Тизер

Я был бы удивлен, если бы кто-нибудь, кто следит за последними новостями Java, еще не слышал о  Plumbr . Plumbr использует Java-агент для выявления утечек памяти в вашем приложении. На момент написания статьи Plumbr составлял «139 долларов США за JVM в месяц». ОЙ! В моем следующем блоге по разработке байт-кода я покажу вам, как вы можете бесплатно идентифицировать утечки памяти в вашем коде, используя инструментарий и  фантомные ссылки .

Если вам понравилась эта статья, вы можете  подписаться на discotek в твиттере .

— Подробнее см .:  https://discotek.ca/blog/?p=230.