Введение
Генерация кода - обычное явление в современных фреймворках. Причины внедрения генерации могут быть различными: от избавления от бойлерплейт кода до замены рефлексии и создания сложных программ на основе формального языка описания предметной области.
Как и у всякой технологии, у генерации кода есть свои области применения и ограничения. В этой статье мы посмотрим, как используется генерация кода в CUBA Platform сейчас и обсудим, как в будущем эта технология будет развиваться в рамках фреймворка.
Что генерируется в CUBA?
CUBA Platform построена на основе хорошо известного Spring Framework. По большому счету, каждое CUBA приложение - это Spring приложение с дополнительными API и библиотеками, которые были добавлены, чтобы облегчить разработку типовой функциональности бизнес-приложений.
CUBA предоставляет фреймворк на базе Vaadin для быстрой разработки пользовательского интерфейса. И эта библиотека использует декларативное связывание для отображения данных. Таким образом, это дает возможность отображать разные значения свойств объектов, просто переназначая привязку компонента к свойству во время выполнения приложения.
Это означает, что каждый объект, который необходимо отобразить в интерфейсе, должен предоставлять возможность получения значения свойства по названию свойства. Поскольку в ядре CUBA используется Spring, то можно использовать рефлексию для получения значений свойств.
Дополнение сущностей
Рефлексия - мощная вещь, но все равно она достаточно медленная, несмотря на все усилия разработчиков JVM по оптимизации вызовов. И если мы говорим о пользовательском интерфейсе, особенно когда идет речь об отображении больших таблиц с данными, то мы приходим к выводу, что вызовы через рефлексию будут производиться достаточно часто. Например, отображение 20 строк с 10 свойствами приводит к 200 вызовам. А теперь умножим это на количество пользователей и примем во внимание, что все эти вызовы будут выполняться на сервере приложений (так работает Vaadin), и получим достаточно серьезную нагрузку на сервер.
Значит, для каждого объекта с данными (сущности), нужно определить простой метод, который будет вызывать геттер (сеттер) по имени свойства. Для этого подойдет даже простой оператор switch.
Также экраны пользовательского интерфейса должны “знать”, поменялся объект или нет. Это нужно для того, чтобы пользователь мог подтвердить изменения, если значение свойства поменяется.
И в дополнение к методу, который записывает или читает значение свойства по имени, нужно поменять каждый сеттер и вызывать слушатель, чтобы пометить объект как измененный.
Этот метод тоже не очень сложен, фактически односточник. Но это будет не очень честно, просить разработчиков делать эту занудную работу - добавлять и обновлять пачку очень простых методов для каждого свойства сущности. И это как раз тот случай, когда кодогенерация показывает себя во всей красе.
Под капотом CUBA использует EclipseLink ORM. И этот фреймворк решает некоторые задачи, которые мы обозначили ранее. Как сказано в документации: “The EclipseLink JPA persistence provider uses weaving to enhance both JPA entities and Plain Old Java Object (POJO) classes for such things as lazy loading, change tracking, fetch groups, and internal optimizations.”
В CUBA механизмы EclipseLink по дополнению сущностей вызываются во время сборки проекта (по умолчанию в EclipseLink это делается во время выполнения), это делает CUBA плагин для сборки.
Но вызов слушателей для фиксации изменений в сущностях - все ещё задача для CUBA фреймворка. И это делается во время сборки проекта тем же плагином. Если вы откроете .class
файл сущности, вы увидите несколько методов, которых не было в вашем исходном коде. И то, как выглядят сеттеры в вашем коде, может оказаться сюрпризом. Например, вместо
public void setName(String name) {
this.name = name;
}
В декомпилированном коде вы увидите:
public void setName(String name) {
String __prev = this.getName();
this._persistence_set_name(name);
Object var5 = null;
String __new = this.getName();
if (!InstanceUtils.propertyValueEquals(__prev, __new)) {
this.propertyChanged("name", __prev, __new);
}
}
Это смесь кода, сгенерированного EclipseLink и плагином сборки CUBA. Как видите, в CUBA скомпилированный код сущностей будет отличаться от того, что вы пишете в IDE.
Сообщения об ошибках проверок в бинах
CUBA Platform поддерживает вывод сообщений на нескольких языках для сообщений об ошибках в бинах. В JPA аннотациях проверок можно ссылаться на строки в файлах .properties
вместо того, чтобы прописывать сам текст сообщения напрямую в параметры аннотации.
В коде это будет выглядеть примерно так:
@NotNull(message = "{msg://hr_Person.name.validation.NotNull}")
@Column(name = "NAME", nullable = false, unique = true)
private String name;
Файлы ресурсов с переводами должны быть в том же пакете, что и сущности. То есть чтобы упростить загрузку значений из файла, нужно записать ещё и имя пакета в строке сообщения. Это простое действие, с понятным алгоритмом, поэтому было решено использовать кодогенерацию.
Плагин CUBA преобразовывает ссылки на сообщения в следующий формат:
@NotNull(message = "{msg://com.company.hr/hr_Person.name.validation.NotNull}")
@Column(name = "NAME", nullable = false, unique = true)
private String name;
И теперь у нас есть имя пакета, следовательно, процесс получения значения из файла через метод getResourceAsStream()
становится сильно проще.
А что нас ждет в будущем?
На текущий момент CUBA не очень интенсивно использует генерацию кода. Но проект продолжает развиваться, и команда разработчиков планирует добавить генерацию и в других областях.
Общие методы сущностей
В текущей версии структура сущностей в CUBA достаточно гибкая, но она основана на использовании интерфейсов. Следовательно, нужно реализовывать методы, которые определены в этих интерфейсах. Например, если вы хотите, чтобы ваша сущность поддерживала “логическое” удаление, нужно реализовать следующий интерфейс:
public interface SoftDelete {
Date getDeleteTs();
String getDeletedBy();
//More methods here
}
Конечно, есть реализации с функциональностью “по умолчанию”, например com.haulmont.cuba.core.entity.StandardEntity
. Вы можете унаследоваться от этой сущности, чтобы использовать реализованную функциональность.
Но было бы сильно проще использовать не имена методов, которые жестко прописаны, а отмечать свойства класса, которые бы хранили дату удаления и имя пользователя, который удаление произвел. В этом случае, можно было бы сгенерировать метод, показанный выше, и перенаправлять вызовы геттеров и сеттеров на нужные. Давайте посмотрим на сущность:
@Entity
public class Account {
//Other fields
@DeletedDate
private Date disposedAt;
@DeletedBy
private String disposedBy;
public Date getDisposedAt() {
return disposedAt;
}
public String getDisposedBy() {
return disposedBy;
}
}
В этой сущности можно видеть специальные поля, которые хранят данные об удалении сущности. А что если мы применим немного кодогенерации к этой сущности?
@Entity
public class Account implements SoftDelete {
//Other fields
@DeletedDate
private Date disposedAt;
@DeletedBy
private String disposedBy;
public Date getDisposedAt() {
return disposedAt;
}
public String getDisposedBy() {
return disposedBy;
}
//Generated
@Override
public Date getDeleteTs() {
return getDisposedAt();
}
//Generated
@Override
public String getDeletedBy() {
return getDisposedBy();
}
}
Теперь мы можем проверить, что сущность поддерживает “логическое” удаление, используя оператор instanceof
. Тем самым обеспечивается общий подход для такого рода удаления во фреймворке, когда мы полагаемся только на интерфейсы и их методы, вместо того, чтобы определять наличие аннотаций во время выполнения.
Этот подход добавляет гибкости в определения сущностей, особенно это полезно при создании модели из существующей БД.
В будущих версиях CUBA мы планируем добавить немного кодогенерации ещё в нескольких местах, чтобы сделать жизнь разработчика ещё проще.
Генерация во время сборки или во время выполнения?
Если вы не заметили, то в CUBA генерация кода производится во время сборки. Есть за и против такого подхода, давайте немного об этом поговорим.
Генерация кода во время сборки позволяет вам находить проблемы на более ранних этапах жизненного цикла программы. Когда вы генерируете код, то нужно принимать во внимание большое количество “переменных”. Например, если API EclipseLink поменяется, то все вызовы, сгенерированные CUBA, станут неверными. Аналогичная ситуация может случиться, если поменяется API JDK. При генерации кода во время сборки мы полагаемся на компилятор Java, чтобы выявить эти ошибки на ранних стадиях. И ошибки компиляции обычно проще найти, чем ошибки выполнения, потому что исходный код статичен. Даже если это сгенерированный код.
Но генерация во время сборки требует дополнительного инструментария, который не является частью проекта - плагин для сборки. А добавление дополнительных инструментов означает добавление новых точек сбоя. Разработчик теперь зависит не только от компилятора, но и от инструмента для кодогенерации. И если любой из этих инструментов содержит ошибку - это проблема, потому что разработчик не может их поменять.
Во время выполнения нам не нужен отдельный инструмент для кодогенерации, библиотеки генератора - часть фреймворка. Но генерация во время исполнения программы зависит от состояния среды исполнения этой программы. Например, иногда кодогенерация может внезапно завершиться с ошибкой, потому что программе не хватило памяти, или по любой другой причине. Довольно сложно полностью контролировать состояние JVM.
Таким образом, для CUBA Platform мы выбрали генерацию кода во время сборки. Количество генерируемого кода не очень велико, набор классов, для которых производится кодогенерация, ограничен сущностями, так что для нашего конкретного случая генератор довольно простой, и до сих пор не было никаких серьезных проблем с ним.
Инструменты генерации
В Java существует стандартных подход к кодогенерации, который появился, начиная с Java 5 - обработка аннотаций (annotation processing). Идея очень простая - вы создаете процессор, который может генерировать новый код на основе аннотаций в существующем коде. И можно сгенерировать код с аннотациями, которые будут обработаны в следующем цикле процессинга.
Стандартный процесс обработки аннотаций ограничен - нельзя обновлять существующий код, можно только создавать новый. Поэтому для CUBA мы выбрали библиотеку Javassist.
Эта библиотека позволяет обновлять существующий код, а также при этом можно использовать обычные строки. Например, этот код сохраняет предыдущее значение свойства класса перед вызовом сеттера:
ctMethod.insertBefore(
"__prev = this." + getterName + "();"
);
Javassist содержит свою собственную, ограниченную версию Java компилятора для проверки корректности кода. Использование строк при генерации кода не обеспечивает безопасности типов, и можно добавить ошибок, просто опечатавшись. Но это значительно проще, чем использовать библиотеку с типизированной моделью кодогенерации, например ByteBuddy, потому что вы буквально можете видеть код, который добавляется в ваши классы.
Заключение
Генерация кода - это очень мощный инструмент, который помогает разработчикам:
- Избежать нудной работы по написанию простого повторяющегося кода
- Автоматизировать обновление некоторых методов при изменении исходного кода
Минус в том, что теперь ваша программа - это не то, что вы написали. Массовое применение кодогенерации может кардинально поменять ваш исходный код. В итоге если что-то пойдет не так, то вы будете отлаживать не свой код, а чей-то.
Вдобавок, вы становитесь зависимым от генераторов кода, и, в случае обнаружения в них ошибок, вам нужно будет ждать обновлений.
В CUBA области генерации кода ограничены сущностями, и мы планируем медленно расширять эти области, чтобы упростить работу разработчикам и добавить в фреймворк гибкости.
И если вы планируете сделать свой собственный новый фреймворк или кодогенератор для существующего, помните - это мощный, но очень хрупкий инструмент. Старайтесь генерировать простой код и документируйте все шаги генерации, потому что любое изменение в API потенциально может полностью сломать всю генерацию.