среда, 18 января 2012 г.

Знакомимся с 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. Что еще посмотреть/почитать по теме
Также, компания ZeroTurnaround недавно выпустила свой новый продукт LiveRebel. И если основной целью JRebel было сократить потери времени разработчиков на т.н. "Turnaround", то LiveRebel в первую очередь рассчитан на обновление приложений на лету в "продакшане" без необходимости остановки их работы и с гарантией сохранения сессии. Будем ждать таких же успехом LiveRebel!  

P.S. Если кто-то захочет написать небольшую статью о других проектах получивших награды "2011 Duke's Choice Awards", не стесняйтесь ;)
 За помощь в подготовке данного материа спасибо Полянскому Дмитрию

Комментариев нет:

Отправить комментарий