Когда цикл разработки от компиляции до развертывания до тестирования занимает слишком много времени, желательно иметь возможность своевременно заменить работающий код без необходимости перезапуска сервера приложений и ожидания завершения развертывания. Коммерческие решения, такие как 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: 8 V2 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_1 1 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_2 10 checkcast # 33 <com/martinsdeveloperworld/springloaded/ToBeChanged> 13 invokestatic # 36 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBFaboY.newMethod> 16 aconst_null 17 areturn 18 aload_3 ... 68 areturn |
В этом случае я добавил новый метод newMethod()
в класс ToBeChanged
. Начало метода __execute()
сравнивает, соответствует ли вызванный дескриптор новому методу. Если это так, он перенаправляет вызов новому исполнителю. Чтобы позволить этой работе, все вызовы нового метода должны быть переписаны в метод __execute()
. Это также делается с помощью инструментов оригинальных классов и также работает для отражения.
Вывод
подпружиненный демонстрирует, что можно «заменить» класс более новой версией во время выполнения. Для достижения этого используется ряд технологий Java, таких как Java-агент и инструментарий байт-кода. При более внимательном рассмотрении реализации можно многое узнать о JVM и Java в целом.
Ссылка: | Обновление кода во время выполнения (подпружиненная демистификация) от нашего партнера по JCG Мартина Моиса в блоге Martin’s Developer World . |