Три года назад мы объявили о выходе CUBA 6. Та версия стала революционной: вместо закрытой проприетарной лицензии мы стали распространять фреймворк свободно, по лицензии Apache 2.0. В то время мы не могли даже и близко представить, насколько это отразится на развитии фреймворка в долгосрочной перспективе. Сообщество CUBA стало расти в геометрической прогрессии, и мы столкнулись со всеми возможными (а иногда и невозможными) способами применения фреймворка. Сейчас мы представляем вашему вниманию CUBA 7. Надеемся, что эта версия сделает разработку ещё проще и приятнее для всех членов сообщества: от начинающих, которые только познакомились с CUBA и Java, до опытных разработчиков, за плечами которых не один завершенный проект уровня большой компании.
Инструменты разработки
Значительной частью успеха CUBA мы обязаны CUBA Studio. Эта среда разработки существенно упростила выполнение типовых задач, которые делаются в каждом Java проекте, сведя их к созданию простых конфигураций в визуальных дизайнерах. Не нужно знать все атрибуты аннотаций Persistence API, синтаксис Gradle или тонкости конфигурации Spring для разработки законченного и многофункционального CRUD-приложения — CUBA Studio берет на себя создание типового кода.
Studio была отдельным веб-приложением, что вызывало ряд существенных ограничений:
- Во-первых, Studio не была полноценной IDE, поэтому разработчикам приходилось переключаться между Studio и IntelliJ IDEA или Eclipse, чтобы разрабатывать бизнес-логику и при этом пользоваться удобной навигацией, автодополнением кода и прочими необходимыми вещами, что несколько напрягало.
- Во-вторых, вся эта магическая простота была выстроена на огромном объеме работы, которую мы затратили на написание алгоритмов по разбору и генерации исходного кода. Реализация ещё более продвинутой функциональности означала бы переход к разработке полноценной IDE — слишком амбициозная затея для нас.
Мы решили положиться на другого гиганта, чтобы преодолеть эти ограничения и построили Studio на базе IntelliJ IDEA. Теперь вы можете установить Studio как в виде отдельного приложения (IntelliJ IDEA Bundle), так и в виде плагина для IDEA .
И это дает нам новые возможности:
- Поддержка других языков JVM (и, прежде всего, Kotlin)
- Улучшенный Hot deploy
- Интуитивно понятная навигация по всему проекту
- Более умные подсказки и генераторы кода
В настоящее время мы активно разрабатываем новую версию Studio — переносим функциональность из старой версии и добавляем новые вещи с использованием функциональности платформы IntelliJ. В ближайших планах — перевод специфичных для CUBA редакторов на компоненты IntelliJ и улучшение навигации по коду проекта.
Обновление технологического стека
По традиции, технологический стек в основе CUBA был обновлен до новых версий: Java 8/11, Vaadin 8, Spring 5.
По умолчанию новые проекты используют Java 8, но можно указать версию Java, добавив в файл build.gradle вот такой код:
subprojects {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
Особенно большой проблемой стало обновление до Vaadin 8, в котором сильно поменялся API привязки данных. К счастью, CUBA экранирует разработчиков от внутренних компонентов Vaadin, оборачивая их в собственный API. Команда CUBA проделала огромную работу по обновлению внутренних компонентов, не меняя CUBA API. Это означает, что совместимость полностью сохранена, и вы можете пользоваться всеми новыми возможностями Vaadin 8 сразу после миграции проекта на CUBA 7, не делая никакого рефакторинга.
Полный список обновленных зависимостей доступен в списке изменений.
Новый API экранов
Этот раздел мог бы называться «первый API для работы с экранами», поскольку у CUBA никогда не было официально заявленного API для работы с экранами в модуле веб-клиента. Так сложилось исторически, в том числе благодаря некоторым предпосылкам, которые возникли на начальном этапе:
- Декларативно-ориентированный подход — все, что можно описать декларативно, нужно было объявлять в дескрипторе экрана, а не делать это в коде контроллера.
- Стандартные экраны (браузер и редактор) предоставляли определенную общую функциональность, и изменять ее не было нужды.
Уже к моменту, когда к нашему сообществу присоединилась первая тысяча участников, мы поняли, сколько разных требований предъявляется к «стандартным» экранам CRUD. И все эти требования были далеко за пределами начальной функциональности. Тем не менее, в течение долгого времени мы могли удовлетворять запросы на реализацию нетипового поведения экрана не меняя API — благодаря еще одному архитектурному принципу, заложенному на начальном этапе: Open Inheritance. Фактически, Open Inheritance означает, что вы можете переписать любой публичный или защищенный метод основного класса, чтобы настроить его поведение согласно вашим нуждам. Это может показаться чудесной панацеей, но на деле вы не могли полагаться на это даже в краткосрочной перспективе. Что, если переопределенный метод будет переименован, удален или просто никогда не будет использован в будущих версиях фреймворка?
Так что в ответ на растущий спрос сообщества мы решили представить новый API экранов. Этот API предоставляет понятные (без всякой скрытой магии), гибкие и простые в использовании точки для расширения, которые гарантированно не будут меняться в течение длительного времени.
Декларация экранов
В CUBA 7 декларировать экраны предельно просто:
@UiController("new-screen") // screen id
public class NewScreen extends Screen {
}
На примере выше видно, что идентификатор экрана явно определен прямо над объявлением класса контроллера. Другими словами, идентификатор экрана и класс контроллера теперь соответствуют друг другу уникальным образом. Так что, у нас есть хорошая новость: теперь к экранам можно безопасно обращаться напрямую по типу контроллера:
@Inject
private ScreenBuilders screenBuilders;
@Subscribe
private void onBeforeClose(BeforeCloseEvent event) {
screenBuilders.screen(this)
.withScreenClass(SomeConfirmationScreen.class)
.build()
.show();
}
Дескриптор экрана становится опциональной частью экрана. UI может быть создан программно или объявлен как xml-дескриптор экрана, который определяется аннотацией @UiDescriptor
над классом контроллера. Это делает контроллеры и разметку намного проще для чтения и понимания — этот подход очень похож на тот, который используется в разработке Android.
Раньше также требовалось зарегистрировать дескриптор экрана в файле web-screens.xml и присвоить ему идентификатор. В CUBA 7 этот файл сохранен в целях совместимости, однако новый способ создания экранов не требует такой регистрации.
Жизненный цикл экрана
Новый API представляет простые и говорящие сами за себя события жизненного цикла экрана:
- Init
- AfterInit
- BeforeShow
- Aftershow
- BeforeClose
- AfterClose
На все события в CUBA 7 можно подписаться следующим образом:
@UiController("new-screen")
public class NewScreen extends Screen {
@Subscribe
private void onInit(InitEvent event) {
}
@Subscribe
private void onBeforeShow(BeforeShowEvent event) {
}
}
В сравнении со старым подходом, в новом API видно, что мы не перекрываем hook-методы, которые неявно вызываются при инициализации, а явно определяем логику для обработки конкретного, определенного события жизненного цикла экрана.
Обработка событий и функциональные делегаты
В предыдущем разделе мы узнали, как подписаться на события жизненного цикла, а что насчет других компонентов? Нужно ли все так же ссыпать в одну кучу все необходимые слушатели при инициализации экрана, в методе init(), как это было в версиях 6.x? Новый API довольно единообразен, поэтому подписаться на другие события можно так же, как на события жизненного цикла экрана.
Рассмотрим простой пример с двумя элементами UI: кнопкой и полем отображения суммы денег в определенной валюте; XML-дескриптор будет выглядеть так:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="msg://caption"
messagesPack="com.company.demo.web">
<layout>
<hbox spacing="true">
<currencyField id="currencyField" currency="$" currencyLabelPosition="LEFT"/>
<button id="calcPriceBtn" caption="Calculate Price"/>
</hbox>
</layout>
</window>
При нажатии на кнопку мы вызываем сервис из бэкэнда, который возвращает число, его мы записываем в поле суммы. Это поле должно изменить стиль в зависимости от значения цены.
@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {
@Inject
private PricingService pricingService;
@Inject
private CurrencyField<BigDecimal> currencyField;
@Subscribe("calcPriceBtn")
private void onCalcPriceBtnClick(Button.ClickEvent event) {
currencyField.setValue(pricingService.calculatePrice());
}
@Subscribe("currencyField")
private void onPriceChange (HasValue.ValueChangeEvent<BigDecimal> event) {
currencyField.setStyleName(getStyleNameByPrice(event.getValue()));
}
private String getStyleNameByPrice(BigDecimal price) {
...
}
}
В приведенном выше примере мы видим два обработчика событий: один вызывается при нажатии кнопки, а другой запускается, когда поле валюты меняет свое значение — все просто.
Теперь представим, что нам нужно проверить цену и убедиться, что ее значение положительное. Это можно сделать “в лоб” — добавить валидатор во время инициализации экрана:
@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {
@Inject
private CurrencyField<BigDecimal> currencyField;
@Subscribe
private void onInit(InitEvent event) {
currencyField.addValidator(value -> {
if (value.compareTo(BigDecimal.ZERO) <= 0)
throw new ValidationException("Price should be greater than zero");
});
}
}
В реальных приложениях метод инициализации через некоторое время будет представлять из себя кашу из инициализаторов, валидаторов, слушателей и т.д. Для решения этой проблемы у CUBA есть полезная аннотация @Install
. Давайте посмотрим, как она может помочь в нашем случае:
@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {
@Inject
private CurrencyField<BigDecimal> currencyField;
@Install(to = "currencyField", subject = "validator")
private void currencyFieldValidator(BigDecimal value) {
if (value.compareTo(BigDecimal.ZERO) <= 0)
throw new ValidationException("Price should be greater than zero");
}
}
Фактически, мы делегируем логику валидации поля валюты в метод currencyFieldValidator в экране. Это может показаться немного сложным на первый взгляд, однако разработчики удивительно быстро привыкли к такому методу добавления функциональности и сразу начали его использовать.
Построители экранов, нотификации, диалоги
В CUBA 7 есть набор полезных компонентов с удобным API:
- ScreenBuilders объединяет fluent factories для создания стандартных экранов для просмотра и редактирования сущностей, а также кастомных экранов. Пример ниже показывает, как вы можете открыть один экран из другого. Обратите внимание, что метод build() сразу возвращает экран нужного типа без необходимости приведения:
CurrencyConversions currencyConversions = screenBuilders.screen(this) .withScreenClass(CurrencyConversions.class) .withLaunchMode(OpenMode.DIALOG) .build(); currencyConversions.setBaseCurrency(Currency.EUR); currencyConversions.show();
- Компонент Screens обеспечивает более низкий уровень абстракции для создания и отображения экранов, в отличие от ScreenBuilders API. Он также предоставляет доступ к информации обо всех открытых экранах в вашем приложении CUBA (Screens#getOpenedScreens), если вдруг возникнет нужда пройтись по ним всем в одном цикле.
- Компоненты Notifications и Dialogs предоставляют удобный и буквально самодокументируемый API. Ниже приведен пример создания и отображения диалогового окна и уведомления:
dialogs.createOptionDialog() .withCaption("My first dialog") .withMessage("Would you like to thank CUBA team?") .withActions( new DialogAction(DialogAction.Type.YES).withHandler(e -> notifications.create() .withCaption("Thank you!") .withDescription("We appreciate all community members") .withPosition(Notifications.Position.MIDDLE_CENTER) .withHideDelayMs(3000) .show()), new DialogAction(DialogAction.Type.CANCEL) ) .show();
Привязка данных
CUBA обеспечивает чрезвычайно быструю разработку пользовательского интерфейса для back office не только за счет продвинутых визуальных инструментов разработки и мощной системы генерации кода, но и благодаря богатому набору компонентов, доступных прямо “из коробки”. Этим компонентам нужно просто знать, с какими данными они работают, а остальное будет делаться автоматически. Например, выпадающие списки, календари, таблицы со встроенными CRUD операциями и так далее.
До версии 7 привязка данных осуществлялась через так называемые datasource’ы — объекты, которые обертывают одну или несколько сущностей для их реактивного связывания с компонентами. Этот подход работал очень хорошо, однако с точки зрения реализации это был монолит. Монолитную архитектуру обычно проблематично настраивать, поэтому в CUBA 7 этот громадный булыжник был разделен на три компонента для работы с данными:
- Data loader (загрузчик данных) — это поставщик данных для data containers. Загрузчик не хранит данные, он просто передает все необходимые параметры запроса в хранилище данных и помещает итоговые данные в data containers.
- Data container (контейнер данных) сохраняет загруженные данные (одну или несколько сущностей) и предоставляет их компонентам данных: все изменения в этих сущностях передаются соответствующим компонентам, и наоборот, все изменения в компонентах приведут к соответствующим изменениям в сущностях, лежащих в data container.
- Datacontext (контекст данных) — это класс, который отслеживает изменения и сохраняет все измененные сущности. Отслеживаемые сущности помечаются как "грязные" при любом изменении их атрибутов, и DataContext сохраняет грязные экземпляры на Middleware при вызове его метода commit().
Таким образом, появилась гибкость при работе с данными. Искусственный пример: загрузчик может выбирать данные в UI из РСУБД, а контекст может сохранять изменения в REST сервис.
В CUBA 6.x вам для этого нужно будет писать собственный datasource, который умеет работать и с РСУБД, и с REST. В CUBA 7 вы можете взять стандартный загрузчик, который умеет работать с БД и написать только свою реализацию контекста для работы с REST.
Компоненты для работы с данными могут быть объявлены в дескрипторах экрана или созданы программным способом с помощью специализированной фабрики — DataComponents.
Прочее
Уфф… Наиболее значительные части нового экранного API описаны, поэтому кратко перечислим другие важные функции на уровне веб-клиента:
- История URL и навигация. Эта функция решает очень распространенную проблему SPA — не всегда корректное поведение кнопки «назад» в веб-браузере. Теперь предоставляется простой способ назначения маршрутов для экранов приложения и позволяет API отображать текущее состояние экрана в URL-адресе.
- Форма вместо FieldGroup. FieldGroup — это компонент для отображения и изменения полей одной сущности. Он выводит UI для поля в рантайме. Другими словами, если в сущности имеется поле Date, оно будет отображаться как DateField. Однако, если вы хотите работать с этим полем программно, вам нужно будет ввести его в контроллер экрана и вручную привести его к правильному типу (DateField в нашем примере). Если позже мы изменим тип поля на другой, то наше приложение вылетит во время выполнения. Форма решает эту проблему путем явного объявления типа поля. Более подробную информацию о компоненте можно найти здесь.
- Интеграция сторонних JavaScript компонентов значительно упрощена, читайте документацию по встраиванию кастомных JavaScript компонентов в приложение CUBA.
- HTML / CSS атрибуты теперь можно легко определить прямо из xml дескриптора экрана или установить программно. Более подробную информацию можно найти здесь.
Новые возможности бэкэнд модуля
Предыдущий раздел о новом экранном API получился больше, чем я ожидал, поэтому в этом разделе я буду краток.
Событие Entity Changed
Событие Entity Changed — это событие в приложении Spring, которое запускается, когда сущность попадает в хранилище данных, физически помещается и находится в шаге от фиксации. При обработке этого события можете настроить дополнительные проверки (например, проверить наличие товара на складе перед подтверждением заказа) и изменить данные (например, пересчитать итоги) прямо перед тем, как они будут видны для других транзакций (конечно, в случае, если у вас уровень изоляции read committed). Это событие может также быть последней возможностью прервать транзакцию, выбросив исключение, что может быть полезно в некоторых хитрых случаях.
Существует также способ обработать событие Entity Changed сразу после коммита.
Посмотреть на пример можно в этой главе документации.
Транзакционный менеджер данных
При разработке приложения мы обычно работаем с detached сущностями — теми, которые не находятся в контексте какой-либо транзакции. Однако работа с detached сущностями не всегда возможна, особенно когда нужно полностью соответствовать требованиям ACID — это тот случай, когда можно использовать транзакционный менеджер данных. Он очень похож на обычный менеджер, но отличается следующими аспектами:
- Он может присоединиться к существующей транзакции (будучи вызванным в контексте этой транзакции) или создать свою собственную транзакцию.
- У него нет метода commit, но есть метод save, который не приводит к немедленному коммиту, а ждет, пока текущая транзакция будет зафиксирована.
Вот пример его использования.
Обратные вызовы JPA
Наконец, CUBA 7 поддерживает JPA коллбэки. Чтобы не повторять известные материалы, для чего могут использоваться эти обратные вызовы, я просто оставлю здесь ссылку. В этом материале тема коллбеков полностью раскрыта.
Как насчет совместимости?
Справедливый вопрос при появлении любого мажорного релиза, особенно когда есть так много критических изменений! Мы разработали все эти новые функции и API с учетом обратной совместимости:
- Старый API поддерживается в CUBA 7 и под капотом реализуется через новый :)
- Мы также предоставили адаптеры для привязки данных через старый API. Эти адаптеры будут отлично работать для экранов, созданных по старой схеме.
Хорошие новости — процесс миграции с версии 6 на 7 должен быть довольно простым.
Заключение
Завершая технический обзор, хочу отметить, что есть и другие важные новшества, особенно в области лицензирования:
- Ограничение на 10 сущностей для Studio теперь снято
- Дополнения Reporting, BPM, Charts and Maps и Full text search теперь бесплатны и имеют открытый исходный код.
- Коммерческая версия Studio добавляет разработке удобства с помощью визуальных дизайнеров сущностей, экранов, меню и других элементов платформы, а бесплатная версия ориентирована на работу с кодом.
- Обратите внимание, что для версий 6.x и более ранних версий условия лицензирования Platform и Studio остаются прежними!
Наконец, позвольте мне еще раз поблагодарить участников сообщества за поддержку и обратную связь. Я надеюсь, что вы полюбите версию 7! Полная информация традиционно доступна в официальном списке изменений.