Знакомимся с JRebel
Осенью прошлого года на конференции JavaOne в Сан-Франциско, традиционно были присуждены награды лучшим проектам года “2011 Duke’s Choice Awards”. И хотелось бы подготовить цикл небольших статей, коротко описывающих каждый из проектов-победителей.
Начнем данный цикл с номинации “Innovative Compiler for Java Code”, награду в которой получил проект JRebel, эстонской компании ZeroTurnaround, созданный Евгением Кабановым (Jevgeni Kabanov) и Томасом Румером (Toomas Römer).
Стоит отметить, что это не единственная их награда за прошедший год, этот проект был признан “Самой инновационной технологией Java” на JAX Innovation Awards. А в числе пользователей JRebel значатся IBM, HP, AT&T, Bank of America, Disney, LinkedIn, …
Для того, чтобы понять предназначение JRebel начнем чуть-чуть издалека
Рассмотрим два простых класса. Первый класс имеет лишь переопределенный метод toString():
public class TestModule {
@Override
public String toString() {
return "TestModule, version 1!";
}
}
Второй класс в бесконечном цикле создает экземпляр объекта первого класса и выводит его на экран:
public class Test {
public static void main(String[] argv) {
for (;;) {
TestModule t = new TestModule();
System.out.println(t);
System.console().readLine();
}
}
}
Если же во время выполнения программы мы изменим строку “TestModule, version 1!” на “TestModule, version 2!”, и применяя раздельную компиляцию заново скомпилируем класс TestModule, то от этого значение выводимой строки не измениться.
Это происходит потому, что стандартный (системный) ClassLoader при первом же обращении к классу, а точнее при вызове его конструктора (либо при обращении к статическому методу или полю), динамически загружает и кеширует байт-код класса в недра JVM, и в дальнейшем уже не обращается к файловой системе. Можете даже удалить файл TestModule.class, это никак не отразиться на работе программы.
Перечень загружаемых во время работы программы классов, и директории откуда они загружаются, можно узнать подставив при запуске программы ключик java -verbose:class …. Узнаете много чего интересного ;)
Из приведенного выше примера следует, то, что после изменения исходного кода (и, соответственно, байт-кода) необходимо перезапускать все приложение. А в случае промышленного (Java Web, Java EE) приложения заново разворачивать его на сервере. Вообщем-то логично.
Все бы ничего, но проведенные исследования показали, что разработчики тратят в среднем от 10 до 13 минут на час программирования, на периодическое развертывания приложений.
Чтобы сократить потери времени, необходимо “научить” JVM обновлять байт-код классов в случаи их изменений, без необходимости перезапуска приложения.
В простом случае для этого достаточно написать свой загрузчик классов, который наследуется от абстрактного класса ClassLoader, и переопределить в нем ряд методов таким образом, чтобы при каждом создании экземпляра класса его байт-код считывался с файловой системы. Это на самом деле делается не так страшно, как может показаться и подробно, вместе с примерами, описывается в статье “ClassLoader – скрытые возможности”.
Экземпляр собственного ClassLoader-а предается в качестве параметра статического метода Class.forName(String name, boolean initialize, ClassLoader loader), после чего с помощью рефлексии создается экземпляр требуемого класса:
public class Test {
public static void main(String[] argv) throws Exception {
for (;;) {
ClassLoader loader = new DynamicClassOverloader(new String[] {"."});
// текущий каталог "." будет единственным каталогом поиска для
// класса "TestModule"
Class clazz = Class.forName("TestModule",true,loader);
Object object = clazz.newInstance();
System.out.println(object);
//TestModule test = (TestModule) object;
System.console().readLine();
}
}
}
В данном примере DynamicClassOverloader (код по ссылке) как раз и является нашим загрузчиком классов (подробности в указанной статье “ClassLoader – скрытые возможности”).
Можно проверить, что теперь во время работы программы при изменении строки “TestModule, version 1!” на “TestModule, version 2!” будет меняться и выводимое в консоль сообщение.
Все было бы замечательно, если бы не ряд особенностей. Первая из которых начнется если откомментировать 10 строчку. В результате получим замечательное RuntimeException:
Exception in thread "main" java.lang.ClassCastException: TestModule cannot be cast to TestModule
Почему оно происходит? Запустим нашу программу с ключиком java -verbose:class Test, и обратим внимание на следующий фрагмент:
...
[Loaded TestModule from __JVM_DefineClass__]
TestModule, version 1!
[Loaded TestModule from file:/C:/Projects/DynamicClassLoader/build/classes/]
...
В первой строке происходит загрузка класса TestModule с помощью нашего собственного загрузчика классов, во второй – вывод сообщения, в третей – стандартный (системный) загрузчик классов снова загружает класс TestModule, именно в тот момент когда пытаемся осуществить приведение типов:
TestModule test = (TestModule) object;
Получается, что класс к которому мы хотим привести (преобразовать) объект загружен с помощью системного загрузчика, а класс на основании которого этот объект object был создан загружен с помощью нашего собственного загрузчика. Хоть классы одинаковы (и их байт-код идентичен), но загружены разными загрузчиками, и, как следствием, JVM считает их абсолютно разными.
Такую вот особенность, и далеко не единственную, которую нужно учитывать при написании собственного загрузчика классов.
Теперь, что же такое JRebel
JRebel – это плагин к JVM, который позволяет на лету перезагружать классы и другие ресурсы, которые были изменены с момента развёртывания приложения. При этом он не имеет тех проблем и особенностей, которые возникают при написании собственного загрузчика классов.
При загрузке класса в JVM, JRebel отслеживает соответствующий ему .class-файл и в случае его изменения подгружает изменившийся класс через расширенный загрузчик классов. При этом старые экземпляры классов и сам класс сохраняется, что позволяет приложению продолжать работу без потери данных. Новые же экземпляры класса будут создаются уже на основании обновленного байт-кода класса.
На данный момент JRebel поддерживает практически все изменения, которые могут осуществляться в исходном коде (JRebel Features):
- Changes to method bodies
- Adding/removing Methods
- Adding/removing constructors
- Adding/removing fields
- Adding/removing classes
- Adding/removing annotations
- Changing static field value
- Adding/removing enum values
- Changing interfaces
за исключением изменений связанных с иерархией классов:
- Replacing superclass
- Adding/removing implemented interfaces
Есть ли аналоги? Да есть, но им далеко до возможностей JRebel. Сравнительную табличку аналогичных технологий и более подробное описание JRebel можно посмотреть в статье Get to Production Sooner.
Что еще посмотреть/почитать по теме
- Отчет - “Java EE Productivity Report 2011″
- Видео с конференции и набор статей от автора JRebel, Евгения Кабанова - Reloading Java Classes: Technical Series
- Статья - “5 JRebel Features You Couldn’t Do In The JVM”
Также, компания ZeroTurnaround недавно выпустила свой новый продукт LiveRebel. И если основной целью JRebel было сократить потери времени разработчиков на т.н. “Turnaround”, то LiveRebel в первую очередь рассчитан на обновление приложений на лету в “продакшане” без необходимости остановки их работы и с гарантией сохранения сессии.
Будем ждать таких же успехом LiveRebel!
P.S. Если кто-то захочет написать небольшую статью о других проектах получивших награды “2011 Duke’s Choice Awards”, не стесняйтесь ;)
За помощь в подготовке данного материа спасибо Полянскому Дмитрию


Интересный пример с приведением объекта к типу того же класса из другого загрузчика. Буду иметь ввиду :)
Еще интересные примеры и ошибки связанные с загрузчиком классов приводит в своем выступлении на Jazoon Jevgeni Kabanov “Do you really get class loaders?” (http://zeroturnaround.com/blog/reloading-objects-classes-classloaders/)
хороший обзор, спасибо :)
Спасибо, Антон, за отзыв!
Как-нибудь и про LiveRebel напишем (очень доклад Евгения на Devoxx понравился ;))
@Anton Arhipov