Pull to refresh

Java-ассемблер, мета-программирование и JPA

Reading time 20 min
Views 9.4K
В этом топике хочу поделиться первым опытом по написанию системы генерации кода «на лету». В коде реализуются некоторые идеи, описанные в предыдущем топике, а сам код используется в одной старой, но работающей системе управления сайтами.

Краткая постановка задачи:
  • Есть набор виртуальных «классов» в понятии бизнес-пользователя. Например, «сайт», «папка», «новость», и т.д. Каждый из таких классов имеет набор полей (аттрибутов).
  • Пока что у нас нет наследования классов, а поля ограничены примитивными String/Integer/Long/Enum/Boolean, даже без multiple, но с возможными заданными значениями по умолчанию
  • Каждый класс записывается в отдельную таблицу, например, objects_sites, objects_news, objects_folder, etc. Таблица всегда содержит ID объекта, а также колонки для полей.
  • Нужно сделать так, чтобы загрузка этих объектов работала через JPA (Hibernate), с использованием необходимого кэширования/транзакций/Lazy-loading'а и других вкусностей, которые нам даёт JPA.

Для выполнения данной задачи использовалось:
  • В качестве баз данных — MySQL 5.0, InnoDB, три схемы базы данных (разные типы могут лежать в разных схемах, чтобы отделить системные типы от пользовательских)
  • Sun JDK 6.0
  • Tomcat 6 + JOTM 2.1.9 + Hibernate 3.5.0-Final (patched)
  • Для создания классов использовалась связка CGLib 2.2 (входящая в Hibernate) и ASM 3.2 (в Hibernate входит 3.1)


Метамодель

С чего начать. Начинается всё с построения объектной мета-модели (не путать с бизнес-моделью). Пока решено остановиться на следующей модели:
- Configuration (complex types)
- Type
 -- Simple Type (simpletypetype - string/long/int/enum, enum class)
 -- ComplexType (attributes, some properties)
- Attribute (name, type, some properties, some JPA properties)

При этом для тестирования и вообще для красоты сделан класс, который позволяет построить эту конфигурацию из XML Schema (идеология которой и бралась за основу). Например из такой:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:meta="http://www.arptek.ru/core/meta"
  3.   targetNamespace="http://www.arptek.ru/core/meta/test">
  4.  
  5.   <xs:complexType name="Test">
  6.     <xs:annotation>
  7.       <xs:appinfo>
  8.         <meta:java.interface>ru.arptek.meta.SomeInterface</meta:java.interface>
  9.       </xs:appinfo>
  10.     </xs:annotation>
  11.     <xs:sequence>
  12.       <xs:element name="id" type="xs:int" meta:jpa.Id="true" />
  13.  
  14.       <xs:element name="string" type="xs:string" />
  15.       <xs:element name="boolean" type="xs:boolean" />
  16.       <xs:element name="integer" type="xs:int" />
  17.       <xs:element name="long" type="xs:long" />
  18.     </xs:sequence>
  19.   </xs:complexType>
  20.  
  21. </xs:schema>
* This source code was highlighted with Source Code Highlighter.


Кстати, обратите внимание на использование дополнительных атрибутов и элементов в пространстве имён «http: // www.arptek.ru/core/meta». Так и только так — дополнительные атрибуты всегда должны быть в отдельном пространстве имён, а элементы — так и вообще помещены в xs:annotation / xs:appinfo. Код, который преобразует эту схему в конфигурацию можно увидеть тут.

Генерация классов-бинов

Код по генерации выполняет три задачи:
  • Генерирует get/set методы для всех атрибутов, с учётом нужных типов. Это нужно для доступа к данным как со стороны Hibernate, так и со стороны кода, который знает о структуре конкретных типов
  • Также генерирует внутренние классы Fields, экземпляры которых помещаются в коллекцию для каждого объекта. В результате можно работать со списком полей не зная заранее, какие именно поля есть у объекта. Это удобно для написания редакторов объектов (одного на почти все типы).
  • Также код нужно не забыть снабдить аннотациями для корректной работы JPA

Некоторые замечания перед началом разбора кода:
  • Java является средой исполнения байт-кода. Байт-код является единственным кодом, который понимает Java-машина (почти). Он записан в двоичном виде в файлах классов (*.class). Обычно он получается из Java-файлов (*.java), которые компилируются в байт-код компилятором javac.
  • Байт код представляет собой набор инструкций, управляющих работой стековой машины. Инструкции включают загрузку локальных переменных, констант, полей класса в стек, операции над ними (арифметические, логические, преобразования типов), вызов методов классов и интерфейсов.
  • Любая операция выполняется над тем, что уже есть в стеке. Вы хотите сложить два числа? они должны быть помещены в стек. Вызвать метод объекта? Ссылка на объект и его параметры должны быть помещены в стек.
  • Сложные объекты помещаются в «кучу», а в стек кладётся лишь указатель на объект. Надеюсь, вы хорошо учили C/C++ :-)

Нам потребуется знание того, как выполнить простые операции, соответствующие тому или иному Java-коду на языке ассемблера. Например, команда
return this.a;

должна быть преобразована в следующий код:
    0  aload_0
    1  getfield имякласса.a
    4  lreturn

При этом конкретные коды операций зависят от того, какие типы имеют аргументы. Для int/boolean/long используются разные коды операций. Но хорошо то, что CGLib и ASM скрывают от нас данные особенности реализации Java-машины и сами контролируют, какие именно типы имеют аргументы и какие коды операций нужно использовать. Например, если мы определили переменную:
 ce.declare_field(Constants.ACC_PROTECTED, fieldName, type, null);

То CGLib/ASM запомнят её тип, и, когда мы будет генерировать код с помощью команд
    codeEmitter.load_this(); 
    codeEmitter.getfield(constantName); 
    codeEmitter.return_value(); 

Сгенерированный код будет иметь именно те операнды, которые работают с заданным нами типом переменной. Итак, возврат значения поля класса было выше. Точнее, этот код можно разбить на две части. Помещение значения поля в стек:
    codeEmitter.load_this(); 
    codeEmitter.getfield(constantName); 

и возврат верхнего значения стека:
    codeEmitter.return_value(); 

Помещение в стек первого аргумента функции (для дальнейшего присваивания чему-либо):
    codeEmitter.load_arg(0);

НО! Проблема в том, что при выполнении операции putfield сверху стека должно быть значение, а ниже — ссылка на объект-владелец поля. Поэтому сначала мы получаем ссылку на объект, потом добавляем аргумент в стек, а уже потом — присваиваем значение полю:
         codeEmitter.load_this(); 
         codeEmitter.load_arg(0); 
         codeEmitter.putfield(fieldName); 
         codeEmitter.return_value(); 

Вызов метода. Для этого в стеке должны быть ссылка на объект и аргументы (аргументы сверху):
                codeEmitter.invoke_virtual(parentClass, new Signature(
                        getterName, fieldValueType, TYPES_EMPTY));

При этом для вызова статических методов используется invoke_static, для вызовов методов, которые определены в интерфейсе (parentClass — интерфейс) используется invoke_interface, а для вызова конструктора — invoke_special (где имя метода — "<init>"). Кстати, создание объекта и вызов конструктора — это разные операции, и они вполне могут быть разделены чем-нибудь. Вот, например, создание LinkedHashMap:
        Local local = e.make_local(TYPE_LINKED_HASH_MAP);

        e.new_instance(TYPE_LINKED_HASH_MAP);
        e.dup();
        e.push(fieldClasses.size());
        e.push(10);
        e.push(1f);
        e.invoke_constructor(TYPE_LINKED_HASH_MAP, SIG_MAP_INIT_INT_FLOAT);
        e.store_local(local);

Подготавливаем локальную переменную, где будем хранить map. После чего создаём новый объект в куче (выделяем ему память). Далее дублируем верхний аргумент стека операцией dup() — так как после вызова конструктора одна из ссылок уйдёт, а нам ещё сохранять значение нужно. Помещаем также в стек аргументы вызова конструктора (чётко соблюдая тип, автоконвертация int->float тут не сработает), и вызываем конструктор с помощью invoke_constructor (он будет преобразован в invoke_special). В последней строке — сохраняем результат в локальную переменную. После этого у нас стек — пустой.

Итак, генерация класса:
  1. ClassWriter cw = new DebuggingClassWriter(
  2.     ClassWriter.COMPUTE_FRAMES);
  3. ClassEmitter ce = new ClassEmitter(cw);
  4.  
  5. Type[] interfaces = getInterfacesTypes(complexType);
  6.  
  7. ce.begin_class(Constants.V1_6, Constants.ACC_PUBLIC, className,
  8.     Type.getType(complexType.getSuperClass()), interfaces,
  9.     Constants.SOURCE_FILE);
  10.  
  11. if (complexType.isJpaEntity()) {
  12.   ce.visitAnnotation(ANNOTATION_DESCRIPTOR_ENTITY, true)
  13.       .visitEnd();
  14. }
  15.  
  16. if (StringUtils.isNotEmpty(complexType.getJpaTableName())) {
  17.   final AnnotationVisitor av = ce.visitAnnotation(
  18.       ANNOTATION_DESCRIPTOR_TABLE, true);
  19.   av.visit("name", complexType.getJpaTableName());
  20.   av.visitEnd();
  21. }
  22.  
  23. {
  24.   if (options.generateFieldAccessors) {
  25.     final String fieldsMapFieldName = "$fields$";
  26.  
  27.     ce.declare_field(Constants.ACC_PRIVATE
  28.         | Constants.ACC_FINAL, fieldsMapFieldName,
  29.         TYPE_MAP, null);
  30.  
  31.     generateConstructorWithFieldsInit(mainClass, ce,
  32.         fieldClasses, fieldsMapFieldName);
  33.     generateGetFieldsMethod(ce, fieldsMapFieldName);
  34.     generateGetFieldMethod(ce, fieldsMapFieldName);
  35.   } else {
  36.     EmitUtils.null_constructor(ce);
  37.   }
  38.  
  39.   for (Attribute attribute : complexType.getAttributes()) {
  40.     processAttribute(ce, attribute);
  41.   }
  42. }
  43. ce.end_class();
  44.  
  45. {
  46.   byte[] bs = cw.toByteArray();
  47.   log.info("Generated class '" + className + "' of " + bs.length
  48.       + " bytes");
  49.   result.put(className, bs);
  50. }
* This source code was highlighted with Source Code Highlighter.

Первые и последняя строки организуют цепочку ClassEmitter -> ClassWriter -> ByteArrayOutputStream -> bytes. При генерации кода класса мы используем ClassEmitter, для методов — CodeEmitter, а для аннотаций — подобного нет и используется AnnotationVisitor (важно не забывать его также закрывать через visitEnd()).

Для создания класса выбираем ему имя, версию, родительских класс, интерфейсы и прочее, как будто мы пишем обычный класс, но на другом языке. Нужно однако не забывать, что в отличии от обычного Java-кода подобный код вполне может скомпилироваться, но не заработать — ибо вы что-то забыли или не так сделали. Например, забыли добавить код возврата из процедуры… класс сгенерируется, но работать не будет. То есть ошибки в подобном коде отследить на этапе компиляции невозможно и нужно обязательно делать тест-кейсы. Хотя те, кто уже готов к восприятию ассемблера уже знают об этом :)

Строки 11-21 добавляют к классу JPA аннотации Entity и Table. Важное замечание, которое бы могло сэкономить мне много времени — метод visitAnnotation принимает строку, но не имя класса аннотации, а его description во внутреннем формате Java. Например, для javax.persistence.Entity это будет «Ljavax/persistence/Entity;». Но их можно легко получить с помощью класса Type (он используется чуть ли не в каждой строке):
  1     private static final String ANNOTATION_DESCRIPTOR_ENTITY = Type.getType(
  2             Entity.class).getDescriptor();

Строка 36 нужна для добавления простого конструктора (другой случай чуть позже). Ну а в цикле 39-41 мы добавляем новые поля, методы get/set и константы значений по умолчанию для каждого из аттрибутов:
  1. if (attribute.getType() instanceof EmptyType)
  2.   return;
  3.  
  4. String fieldName = attribute.getFieldName();
  5. String getterName = attribute.getGetterName();
  6. String setterName = attribute.getSetterName();
  7.  
  8. final String javaMethodNamePart = WordUtils.capitalize(
  9.     attribute.getAttrName(), new char[] { '_' }).replace("_", "");
  10.  
  11. final Type type = getTypeFromXmlSchemaTypeName(attribute.getType());
  12.  
  13. if (fieldName == null)
  14.   fieldName = "m$" + StringUtils.uncapitalize(javaMethodNamePart);
  15. if (getterName == null)
  16.   getterName = "get" + javaMethodNamePart;
  17. if (setterName == null)
  18.   setterName = "set" + javaMethodNamePart;
  19.  
  20. final boolean jpaId = attribute.isJpaId();
  21.  
  22. final String strDefaultValue;
  23. if (!jpaId) {
  24.   strDefaultValue = attribute.getStrDefaultValue();
  25. } else {
  26.   strDefaultValue = null;
  27. }
  28.  
  29. if (strDefaultValue == null) {
  30.   ce.declare_field(Constants.ACC_PRIVATE, fieldName, type, null);
  31.  
  32.   {
  33.     // create getter
  34.     CodeEmitter codeEmitter = ce.begin_method(Constants.ACC_PUBLIC,
  35.         new Signature(getterName, type, TYPES_EMPTY),
  36.         TYPES_EMPTY);
  37.  
  38.     proceedAttributeJpaColumnName(attribute, codeEmitter);
  39.     proceedAttributeJpaLob(attribute, codeEmitter);
  40.     proceedAttributeJpaId(attribute, codeEmitter);
  41.  
  42.     codeEmitter.load_this();
  43.     codeEmitter.getfield(fieldName);
  44.     codeEmitter.return_value();
  45.     codeEmitter.end_method();
  46.   }
  47.  
  48.   {
  49.     // create null safe setter
  50.     CodeEmitter codeEmitter = ce.begin_method(Constants.ACC_PUBLIC,
  51.         new Signature(setterName, Type.VOID_TYPE,
  52.             new Type[] { type }), TYPES_EMPTY);
  53.     codeEmitter.load_this();
  54.     codeEmitter.load_arg(0);
  55.     codeEmitter.putfield(fieldName);
  56.     codeEmitter.return_value();
  57.     codeEmitter.end_method();
  58.   }
  59. } else {
  60.   final Object defaultValue = toValue(type, strDefaultValue);
  61.   final String constantName = "DEFAULT_" + fieldName;
  62.  
  63.   ce.declare_field(Constants.ACC_PROTECTED | Constants.ACC_FINAL
  64.       | Constants.ACC_STATIC, constantName, type, null);
  65.  
  66.   {
  67.     CodeEmitter staticHook = ce.getStaticHook();
  68.     EmitUtils.push_object(staticHook, defaultValue);
  69.     staticHook.putfield(constantName);
  70.   }
  71.  
  72.   ce.declare_field(Constants.ACC_PROTECTED, fieldName, type, null);
  73.  
  74.   {
  75.     // create getter
  76.     final Signature signature = new Signature(getterName, type,
  77.         TYPES_EMPTY);
  78.     final CodeEmitter codeEmitter = ce.begin_method(
  79.         Constants.ACC_PUBLIC, signature, TYPES_EMPTY);
  80.  
  81.     proceedAttributeJpaColumnName(attribute, codeEmitter);
  82.     proceedAttributeJpaLob(attribute, codeEmitter);
  83.  
  84.     codeEmitter.load_this();
  85.     codeEmitter.getfield(fieldName);
  86.  
  87.     Label ifNull = codeEmitter.make_label();
  88.  
  89.     codeEmitter.ifnull(ifNull);
  90.     {
  91.       codeEmitter.load_this();
  92.       codeEmitter.getfield(fieldName);
  93.       codeEmitter.return_value();
  94.     }
  95.     codeEmitter.mark(ifNull);
  96.     {
  97.       codeEmitter.load_this();
  98.       codeEmitter.getfield(constantName);
  99.       codeEmitter.return_value();
  100.     }
  101.     codeEmitter.end_method();
  102.   }
  103.  
  104.   {
  105.     // create setter
  106.     final Signature signature = new Signature(setterName,
  107.         Type.VOID_TYPE, new Type[] { type });
  108.     final CodeEmitter codeEmitter = ce.begin_method(
  109.         Constants.ACC_PUBLIC, signature, TYPES_EMPTY);
  110.  
  111.     codeEmitter.load_this();
  112.     codeEmitter.load_arg(0);
  113.     codeEmitter.putfield(fieldName);
  114.     codeEmitter.return_value();
  115.     codeEmitter.end_method();
  116.   }
  117.  
  118. }
* This source code was highlighted with Source Code Highlighter.

Проверка 1-2 нужна на случай, если поле «пустое». Такие поля позволяют сделать включить в класс атрибут, который не будет сохраняться в той же таблице, а, например, будет сам управлять своим состоянием. В строках 4-18 идёт инициализация переменных и определяются будущие имена для внутренних переменных и методов. Обратите внимание, что для методов и переменных, которые генерируются компьютером, принято использовать знак "$", тогда как в пользовательском коде его рекомендуется избегать. Разумеется, методы get/set оставлены без этого знака, так как они будут вызываться из Hibernate и бизнес-кода.

Для JPA ID не используются значения по умолчанию, поэтому на всякий случай проверим, что его не задали, в строках 23-27. Генерация кода для случая, когда значение по умолчанию задано и нет, различна. Случай, когда его нет, разумеется проще и прямолинейнее — само поле добавляется в класс в строке 30, метод get в строках 32-46, set — 48-58.

Случай же со значениями по умолчанию требует:
  • Определить константу со значением по умолчанию (строки 60-61)
  • Причём для сложных типов (а к ним относится и Boolean/Integer/Long) нужно использовать static-код для инициализации констант. Когда мы пишем обычный код это не нужно — ибо компилятор делает это за нас. Но тут нужно сделать это вручную. В строках 66-70 мы и добавляем в StaticHook новые строки. Потом, когда мы закончим работать с классом, все строки из StaticHook будут перенесены в блок static инициализации класса (ну, почти… интересующиеся деталями могут дизассемблировать и посмотреть, что на самом деле там происходит.)
  • Метод set достаточно простой (строки 104-116)
  • А вот в методе get я добавил ветвление. Его может и не быть — если вы в конструкторе класса инициализируете переменные значениями по умолчанию (как работать с конструктором — ниже). Но мне хотелось, что если переменная внутри класса установлена в null, то возвращалось бы значение константы. Для организации ветвления мы создаём новую ссылку (label). По ней мы будем осуществлять переход, если значение равно нулю. Потом используем команду условного перехода (строка 89). Ещё раз — мы не проверяем условие прямо сейчас — мы добавляем проверку этого условия в код метода. Если условие выполнено — происходит переход до того места в коде, которое помечего (mark) меткой (строка 95). Если не выполнено — выполняется обычная ветка 91-93. При этом каждая из веток исполнения кода содержит код возврата результирующего значения.


Доступ к набору полей

Код выше успешно (надеюсь) сгенерировал «бин» — класс с полями и методами get/set. Но ещё нужно сгерировать два метода: getField(name) и getFields(). Эти методы должны давать доступ к полям объекта через коллекцию, но при этом не должно быть никакого reflection'а, чтобы не замедлять работу системы вызовом native-методов.
Для этого для каждого аттрибута объекта мы дополнительно сгенерируем класс, который содержит два метода — getValue() и setValue(), которые делегируют эти вызовы основному классу. Генерация такого класса достаточно простая, но интересная тем, что теперь конструктор класса не пустой, а принимает единственный параметр — ссылку на родительский объект:
  1.     final String holderFieldName = "$parent$";
  2.  
  3.     ce.declare_field(Constants.ACC_PRIVATE | Constants.ACC_FINAL,
  4.         holderFieldName, parentClass, null);
  5.  
  6.     {
  7.       CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC,
  8.           new Signature("<init>", Type.VOID_TYPE,
  9.               new Type[] { parentClass }), null);
  10.       e.load_this();
  11.       e.super_invoke_constructor();
  12.  
  13.       e.load_this();
  14.       e.load_arg(0);
  15.       e.putfield(holderFieldName);
  16.  
  17.       e.return_value();
  18.       e.end_method();
  19.     }
* This source code was highlighted with Source Code Highlighter.


Сама ссылка сохраняется в переменной $parent$. Методы get/set делаются достаточно просто:
  1.     {
  2.       // create getter
  3.       CodeEmitter codeEmitter = ce.begin_method(Constants.ACC_PUBLIC,
  4.           new Signature("getValue", fieldValueType, TYPES_EMPTY),
  5.           TYPES_EMPTY);
  6.  
  7.       codeEmitter.load_this();
  8.       codeEmitter.getfield(holderFieldName);
  9.  
  10.       codeEmitter.invoke_virtual(parentClass, new Signature(
  11.           getterName, fieldValueType, TYPES_EMPTY));
  12.  
  13.       codeEmitter.return_value();
  14.       codeEmitter.end_method();
  15.     }
  16.  
  17.     {
  18.       // create null safe setter
  19.       CodeEmitter codeEmitter = ce.begin_method(Constants.ACC_PUBLIC,
  20.           new Signature("setValue", Type.VOID_TYPE,
  21.               new Type[] { fieldValueType }), TYPES_EMPTY);
  22.       codeEmitter.load_this();
  23.       codeEmitter.getfield(holderFieldName);
  24.       codeEmitter.load_arg(0);
  25.  
  26.       codeEmitter.invoke_virtual(parentClass, new Signature(
  27.           setterName, Type.VOID_TYPE,
  28.           new Type[] { fieldValueType }));
  29.  
  30.       codeEmitter.return_value();
  31.       codeEmitter.end_method();
  32.     }
* This source code was highlighted with Source Code Highlighter.


Эти классы для каждого атрибута генерируются в цикле и сохраняются в Map<Attribute, String=имя класса>:
  1.         ClassWriter cw = new DebuggingClassWriter(
  2.             ClassWriter.COMPUTE_FRAMES);
  3.         ClassEmitter ce = new ClassEmitter(cw);
  4.  
  5.         String fieldClassName = processAttributeField(className,
  6.             mainClass, ce, attribute);
  7.  
  8.         fieldClasses.put(attribute, ce.getClassType());
* This source code was highlighted with Source Code Highlighter.


Зачем? Чтобы потом в конструкторе родительского класса создать экземпляры дочерних объектов и поместить их во внутреннее поле $fields$:
  1.   public void generateConstructorWithFieldsInit(final Type mainClass,
  2.       ClassEmitter ce, Map<Attribute, Type> fieldClasses,
  3.       final String fieldsMapFieldName) {
  4.     CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, TypeUtils
  5.         .parseConstructor(""), null);
  6.     e.load_this();
  7.     e.super_invoke_constructor();
  8.  
  9.     Local local = e.make_local(TYPE_LINKED_HASH_MAP);
  10.  
  11.     e.new_instance(TYPE_LINKED_HASH_MAP);
  12.     e.dup();
  13.     e.push(fieldClasses.size());
  14.     e.push(1f);
  15.     e.invoke_constructor(TYPE_LINKED_HASH_MAP, SIG_MAP_INIT_INT_FLOAT);
  16.     e.store_local(local);
  17.  
  18.     for (Entry<Attribute, Type> fieldTypes : fieldClasses.entrySet()) {
  19.       e.load_local(local);
  20.       e.push(fieldTypes.getKey().getAttrName());
  21.  
  22.       // = new <...>Field(this);
  23.       e.new_instance(fieldTypes.getValue());
  24.       e.dup();
  25.       e.load_this();
  26.       e.invoke_constructor(fieldTypes.getValue(), new Signature("<init>",
  27.           Type.VOID_TYPE, new Type[] { mainClass }));
  28.  
  29.       // $fields$.put(fieldName, field);
  30.       e.invoke_virtual(TYPE_LINKED_HASH_MAP, SIG_MAP_PUT);
  31.     }
  32.  
  33.     e.load_this();
  34.     e.load_local(local);
  35.     e.invoke_static(TYPE_COLLECTIONS, SIG_COLLECTIONS_UNMODIFIABLEMAP);
  36.     e.putfield(fieldsMapFieldName);
  37.  
  38.     e.return_value();
  39.     e.end_method();
  40.   }
* This source code was highlighted with Source Code Highlighter.


В первых строках мы создаём LinkedHashMap (при этом на параметризацию с помощью Generic мы забиваем — её почти нет в ассемблере). Потом наполняем её дочерними объектами Fields, ну а потом, на всякий случай, оборачиваем в Collections.unmodifiableMap и сохраняем в final поле. Кстати, обратите внимание, что последний метод вызывается с помощью invoke_static, и даже конструктор завершается операцией return_value, хотя в соответствии со спецификацией (строки 4-5) возвращает VOID.

Ну и напоследок создадим методы getField(name) и getFields(), не забыв снабдить их аннотацией Transient, чтобы JPA не думал, что это поля объекта:
  1.   public void generateGetFieldMethod(ClassEmitter ce,
  2.       final String fieldsMapFieldName) {
  3.     CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC,
  4.         SIG_FIELDSHOLDER_GETFIELD, null);
  5.     e.visitAnnotation(ANNOTATION_DESCRIPTOR_TRANSIENT, true).visitEnd();
  6.  
  7.     e.load_this();
  8.     e.getfield(fieldsMapFieldName);
  9.  
  10.     e.load_arg(0);
  11.  
  12.     e.checkcast(TYPE_OBJECT);
  13.     e.invoke_interface(TYPE_MAP, SIG_MAP_GET);
  14.     e.checkcast(TYPE_FIELD);
  15.  
  16.     e.return_value();
  17.     e.end_method();
  18.   }
  19.  
  20.   public void generateGetFieldsMethod(ClassEmitter ce,
  21.       final String fieldsMapFieldName) {
  22.     CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC,
  23.         SIG_FIELDSHOLDER_GETFIELDS, null);
  24.     e.visitAnnotation(ANNOTATION_DESCRIPTOR_TRANSIENT, true).visitEnd();
  25.  
  26.     e.load_this();
  27.     e.getfield(fieldsMapFieldName);
  28.     e.return_value();
  29.     e.end_method();
  30.   }
* This source code was highlighted with Source Code Highlighter.


Загрузка полученных классов

После генерации byte[] для каждого класса нужно их загрузить в память. Это делается с помощью определения своего ClassLoader'а (ну или можно сохранить на диск и загрузить оттуда с помощью какого-нибудь URLClassLoader'а):
  1.   protected Map<String, Class<?>> loadClasses(
  2.       final Map<String, byte[]> generated) {
  3.  
  4.     final Thread currentThread = Thread.currentThread();
  5.     final ClassLoader classLoader = currentThread.getContextClassLoader();
  6.     final Map<String, Class<?>> classes = new ClassLoader(classLoader) {
  7.       protected Map<String, Class<?>> loadClasses() {
  8.         Map<String, Class<?>> result = new LinkedHashMap<String, Class<?>>(
  9.             generated.size());
  10.         for (Entry<String, byte[]> entry : generated.entrySet()) {
  11.           final String className = entry.getKey();
  12.           final byte[] byteCode = entry.getValue();
  13.  
  14.           log.info("Defining class " + className + "...");
  15.           final Class<?> cls = defineClass(className, byteCode, 0,
  16.               byteCode.length);
  17.           result.put(cls.getName(), cls);
  18.         }
  19.  
  20.         return result;
  21.       }
  22.     }.loadClasses();
  23.     return classes;
  24.   }
* This source code was highlighted with Source Code Highlighter.

Кстати забывать про этот ClassLoader не стоит — он вам понадобится позже, для загрузки этих классов в Hibernate, ибо он не будет работать, если текущий ClassLoader не сможет загрузить те классы, что передаются ему в качестве параметров.

Использование на практике

Некоторые ссылки, которые могут быть интересны для дальнейшего изучения:

В результате

  • Операциями с данными теперь занимается Hibernate. Сам заботится о загрузке и сохранении. Есть поддержка batch-операций по загрузке, поддержка кешей и транзакций. Меньше велосипедов это всегда хорошо.
  • Кешированием также занимается Hibernate (хотя и на основе нашего clustered soft reference cache). Наблюдать за состоянием кешей можно из JConsole.
  • Производительность повысилась за счёт оптимизации загрузки данных (возможно batch-загрузки из базы данных сразу нескольких объектов одним SQL-ем)
  • Бизнес-код стало писать намного удобнее и приятнее :)
Tags:
Hubs:
+24
Comments 24
Comments Comments 24

Articles