Когда цикл разработки от компиляции до развертывания до тестирования занимает слишком много времени, желательно иметь возможность своевременно заменить работающий код без необходимости перезапуска сервера приложений и ожидания завершения развертывания. Коммерческие решения, такие как JRebel, или платформы с открытым исходным кодом, такие как Grails, помогают в таких ситуациях.
Замена кода во время выполнения не поддерживается «из коробки» JVM в таком виде, как вы можете динамически загружать классы, например, с помощью Class.forName() . В основном у вас есть следующие варианты:
- HotSwap: Технология, представленная в Java 1.4, которая позволяет переопределять классы в сеансе отладчика. Этот подход очень ограничен, так как он позволяет вам только изменять тело метода, но не добавлять новые методы или классы.
- OSGi: эта технология позволяет вам определять пакеты. Во время выполнения пакет может быть заменен более новой версией этого пакета.
- Разгрузка загрузчиков классов: оборачивая отдельный загрузчик классов во все классы вашего модуля, вы можете выбросить загрузчик классов и заменить его, как только появится новая версия вашего модуля.
- Инструментирование классов с помощью агента Java: Агент Java может инструктировать классы до того, как они будут определены. Таким образом, он может внедрить код в загруженные классы, который связывает этот класс с одной версией файла класса. Как только новая версия доступна, новый код выполняется.
Технология Beils Grails называется подпружиненной и использует подход «Java Agent» для классов инструментов, которые загружаются из файловой системы, а не из файла JAR. Но как это работает под капотом?
Чтобы понять подпружиненный, мы настроили небольшой пример проекта, который позволяет нам изучить технологию более подробно. Этот проект состоит только из двух классов: класс Main вызывает метод print() класса ToBeChanged и некоторое время спит:
|
1
2
3
4
5
6
7
|
public static void main(String[] args) throws InterruptedException { while (true) { ToBeChanged toBeChanged = new ToBeChanged(); toBeChanged.print(); Thread.sleep(500); }} |
Метод print() просто распечатывает версию, так что мы видим, что она изменилась. Кроме того, мы также распечатываем трассировку стека, чтобы увидеть, как она меняется со временем:
|
1
2
3
4
5
6
7
8
|
public void print() { System.out.println("V1"); StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (StackTraceElement element : stackTrace) { System.out.println("\t" + element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber()); }} |
При запуске приложения мы должны предоставить файл jar, содержащий агент Java, используя опцию javaagent . Поскольку подпружиненный загружает байт-код способом, который не нравится верификатору, мы должны отключить проверку байт-кода, передав опцию noverify в JVM. Наконец, мы передаем папку, содержащую наши файлы классов, с помощью cp и сообщаем JVM класс, который содержит метод main() :
|
1
2
3
4
|
java -javaagent:springloaded-1.2.4.BUILD-SNAPSHOT.jar -noverify -cp target/classes com.martinsdeveloperworld.springloaded.Main |
После обновления версии в классе ToBeChanged с V1 на V2 и перестройки проекта с использованием mvn package мы видим следующий вывод:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
...V1 java.lang.Thread.getStackTrace:-1 com.martinsdeveloperworld.springloaded.ToBeChanged.print:7 com.martinsdeveloperworld.springloaded.Main.main:8V2 java.lang.Thread.getStackTrace:-1 com.martinsdeveloperworld.springloaded.ToBeChanged$$EPBF0gVl.print:7 com.martinsdeveloperworld.springloaded.ToBeChanged$$DPBF0gVl.print:-1 com.martinsdeveloperworld.springloaded.ToBeChanged.print:-1 com.martinsdeveloperworld.springloaded.Main.main:8... |
Трассировка стека версии V1 выглядит так, как мы и ожидали. Из Main.main() ToBeChanged.print() метод ToBeChanged.print() . Это отличается для версии V2 . Здесь метод ToBeChanged.print теперь вызывает метод ToBeChanged$$DPBF0gVl.print() . Также обратите внимание, что номер строки для вызова ToBeChanged.print() изменился с 8 на -1, что указывает на то, что строка неизвестна.
Новая строка -1 указывает на то, что Java-агент инструментировал метод ToBeChanged.print() таким образом, чтобы он мог вызывать новый метод вместо выполнения старого кода. Чтобы подтвердить это предположение, я добавил несколько операторов регистрации в код подпружиненного кода и функцию, которая выгружает каждый файл instrumtend на локальный жесткий диск. Таким образом, мы можем проверить, как метод ToBeChanged.print() выглядит после инструментария:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
0 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type> 3 ldc #72 <0> 5 invokevirtual #85 <org/springsource/loaded/ReloadableType.changed> 8 dup 9 ifeq 42 (+33) 12 iconst_1 13 if_icmpeq 26 (+13) 16 new #87 <java/lang/NoSuchMethodError> 19 dup 20 ldc #89 <com.martinsdeveloperworld.springloaded.ToBeChanged.print()V> 22 invokespecial #92 <java/lang/NoSuchMethodError.<init>> 25 athrow 26 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type> 29 invokevirtual #56 <org/springsource/loaded/ReloadableType.fetchLatest> 32 checkcast #58 <com/martinsdeveloperworld/springloaded/ToBeChanged__I> 35 aload_0 36 invokeinterface #94 <com/martinsdeveloperworld/springloaded/ToBeChanged__I.print> count 2 41 return 42 pop 43 getstatic #100 <java/lang/System.out> 46 ldc #102 <V1> 48 invokevirtual #107 <java/io/PrintStream.println> 51 invokestatic #113 <java/lang/Thread.currentThread> 54 invokevirtual #117 <java/lang/Thread.getStackTrace> 57 astore_1...152 return |
Код операции getstatic извлекает значение для нового поля r$type и помещает его в стек (код операции ldc ). Затем ReloadableType.changed() метод ReloadableType.changed() для ссылки на объект, который был ReloadableType.changed() в стек ранее. Как видно из названия, метод ReloadableType.changed() проверяет, существует ли новая версия этого типа. Возвращает 0, если метод не изменился, и 1, если он изменился. Следующий код операции ifeq переходит к строке 42, если возвращаемое значение было нулевым, т.е. метод не изменился. Начиная со строки 42 мы видим оригинальную реализацию, которую я здесь немного сократил.
Если значение равно 1, инструкция if_icmpeq переходит к строке 26, где статическое поле r$type читается еще раз. Эта ссылка используется для вызова метода ReloadableType.fetchLatest() для него. Следующая инструкция checkcast проверяет, что возвращаемая ссылка имеет тип ToBeChanged__I . Здесь мы впервые натыкаемся на этот искусственный интерфейс, который подпружиненный генерирует для каждого типа. Он отражает методы, которые были у исходного класса, когда он был инструментирован. Через две строки этот интерфейс используется для вызова метода print() для ссылки, которая была возвращена ReloadableType.fetchLatest() .
Эта ссылка — не ссылка на новую версию класса, а на так называемый диспетчер. Диспетчер реализует интерфейс ToBeChanged__I и реализует метод print() со следующими инструкциями:
|
1
2
3
|
0 aload_11 invokestatic #21 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBF0gVl.print>4 return |
Динамически генерируемый класс ToBeChanged$$EPBF0gVl является так называемым исполнителем и воплощает новую версию типа. Для каждой новой версии создается новый диспетчер и исполнитель, только интерфейс остается прежним. Как только новая версия становится доступной, интерфейсный метод вызывается в новом диспетчере, и этот метод в простейшем случае перенаправляет на новую версию кода, воплощенную в исполнителе. Причиной, по которой интерфейсный метод не вызывается непосредственно в exeuctor, является тот факт, что подпружиненные могут также обрабатывать случаи, когда методы добавляются в новую версию класса. Так как эти методы не существуют в старой версии, к интерфейсу и диспетчеру добавляется универсальный метод __execute() . Этот динамический метод может затем отправлять вызовы новым методам, как показано в следующем наборе команд, взятых из сгенерированного диспетчера:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
0 aload_3 1 ldc #25 <newMethod()V> 3 invokevirtual #31 <java/lang/String.equals> 6 ifeq 18 (+12) 9 aload_210 checkcast #33 <com/martinsdeveloperworld/springloaded/ToBeChanged>13 invokestatic #36 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBFaboY.newMethod>16 aconst_null17 areturn18 aload_3...68 areturn |
В этом случае я добавил новый метод newMethod() в класс ToBeChanged . Начало метода __execute() сравнивает, соответствует ли вызванный дескриптор новому методу. Если это так, он перенаправляет вызов новому исполнителю. Чтобы позволить этой работе, все вызовы нового метода должны быть переписаны в метод __execute() . Это также делается с помощью инструментов оригинальных классов и также работает для отражения.
Вывод
подпружиненный демонстрирует, что можно «заменить» класс более новой версией во время выполнения. Для достижения этого используется ряд технологий Java, таких как Java-агент и инструментарий байт-кода. При более внимательном рассмотрении реализации можно многое узнать о JVM и Java в целом.
| Ссылка: | Обновление кода во время выполнения (подпружиненная демистификация) от нашего партнера по JCG Мартина Моиса в блоге Martin’s Developer World . |