Расширение функциональности
Функциональность подсистем и дополнений фреймворка может быть расширена и изменена приложением или другими дополнениями, расположенными ниже по иерархии.
Для декларативно созданных элементов, таких как сущности модели данных и XML-компоновки экранов UI, Jmix предлагает собственный механизм расширения. Бизнес-логика, определяемая бинами Spring, может быть расширена с использованием стандартных функций Java и Spring.
Расширение модели данных
Рассмотрим пример расширения модели данных дополнения.
Предположим, у нас есть следующие сущности, определенные в дополнении base
:
Их исходный код:
@JmixEntity
@Table(name = "BASE_DEPARTMENT")
@Entity(name = "base_Department")
public class Department {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@InstanceName
@Column(name = "NAME")
private String name;
// getters and setters
@JmixEntity
@Table(name = "BASE_EMPLOYEE", indexes = {
@Index(name = "IDX_BASE_EMPLOYEE_DEPARTMENT", columnList = "DEPARTMENT_ID")
})
@Entity(name = "base_Employee")
public class Employee {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@Column(name = "FIRST_NAME")
private String firstName;
@InstanceName
@Column(name = "LAST_NAME")
private String lastName;
@JoinColumn(name = "DEPARTMENT_ID")
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// getters and setters
В приложении ext
, которое использует дополнение base
, нам нужно добавить атрибуты description
и manager
к сущности Department
. Очевидно, что мы не можем изменить исходный код дополнения, поэтому нам нужно определить в приложении другую сущность и сделать так, чтобы другие сущности ссылались на нее вместо Department
:
Исходный код расширенной сущности:
@JmixEntity
@Entity
@ReplaceEntity(Department.class) (1)
public class ExtDepartment extends Department { (2)
@InstanceName
@Column(name = "DESCRIPTION")
private String description; (3)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MANAGER_ID")
private User manager; (3)
// getters and setters
1 | В аннотации @ReplaceEntity указано, что эта сущность должна полностью заменить родительскую сущность, указанную в значении аннотации. Studio добавляет эту аннотацию, если вы установите флажок Replace parent в дизайнере сущностей. |
2 | Стандартное наследование сущностей JPA. В данном случае базовый класс не определяет стратегию наследования JPA, поэтому атрибуты расширения будут храниться в той же таблице BASE_DEPARTMENT . |
3 | Атрибуты, добавленные расширением. |
После введения сущности ExtDepartment
с аннотацией @ReplaceEntity
в проект приложения, она будет возвращаться вместо сущности Department
повсеместно в коде доступа к данным. Например, вы можете безопасно приводить ссылки к ExtDepartment
классу:
Employee employee = dataManager.load(Employee.class).id(employeeId).one();
ExtDepartment department = (ExtDepartment) employee.getDepartment();
String description = department.getDescription();
Кроме того, мета-класс сущности ExtDepartment
будет возвращаться метаданными API для обоих классов Java ExtDepartment
и Department
. Исходный мета-класс сущности можно получить с помощью бина ExtendedEntities
.
Расширение UI
Если вы замените сущность дополнения расширенной версией, скорее всего, вам также потребуется переопределить экраны пользовательского интерфейса этой сущности для отображения расширенных атрибутов. Ниже мы рассмотрим пример переопределения экрана списка сущности Department
, замененной на ExtDepartment
как описано в предыдущем разделе .
Чтобы расширить и переопределить экран, предоставленный дополнением, выберите шаблон Override an existing screen в мастере создания экранов Studio. Studio создаст новые файлы XML-дескриптора и контроллера. XML-дескриптор будет содержать атрибут extends
, ссылающийся на дескриптор базового экрана:
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
После этого можно добавить компоненты для отображения расширенных атрибутов:
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
<layout>
<dataGrid id="departmentsDataGrid">
<columns>
<column property="description"/>
<column property="manager"/>
</columns>
</dataGrid>
</layout>
</view>
Вам не нужно повторять все элементы и атрибуты базового экрана, требуется только измененная часть. Результирующий XML будет объединен из базовых и расширенных дескрипторов — подробнее об этом см. ниже.
В нашем случае один из расширенных атрибутов (manager
) является ссылкой на другую сущность. Эта ссылка будет загружаться по запросу из-за автоматической ленивой загрузки, но вы можете включить ссылку на фетч-план экрана, чтобы избежать возможной проблемы "запросов N+1":
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
<data>
<collection id="departmentsDc"
class="com.company.sample.ext.entity.ExtDepartment">
<fetchPlan extends="_base">
<property name="manager" fetchPlan="_base"/>
</fetchPlan>
</collection>
</data>
<layout>
<dataGrid id="departmentsDataGrid">
<columns>
<column property="description"/>
<column property="manager"/>
</columns>
</dataGrid>
</layout>
</view>
Контроллер расширенного экрана будет унаследован от класса базового экрана:
@Route(value = "departments", layout = MainView.class)
@ViewController("base_Department.list")
@ViewDescriptor("ext-department-list-view.xml")
public class ExtDepartmentListView extends DepartmentListView {
Обратите внимание, что аннотация |
Публичные и защищенные методы базового контроллера можно переопределить, чтобы при необходимости расширить логику экрана.
Правила расширения XML-дескрипторов
Расширение дескрипторов XML не учитывает семантику экрана и работает исключительно на уровне XML. Оно объединяет два файла XML в соответствии со следующими правилами:
-
Если в расширяющем дескрипторе есть определенный элемент, соответствующий элемент будет искаться в родительском дескрипторе по следующему алгоритму:
-
Если переопределяющий элемент имеет атрибут
id
, будет найден соответствующий элемент с таким жеid
. Некоторые элементы анализируются также по другим атрибутам, которые выступают в качестве уникальных идентификаторов, вместоid
:-
Для элемента
button
: атрибутaction
. -
Для элемента
column
: атрибутыproperty
иkey
. -
Для элемента
property
: атрибутname
.
-
-
Если поиск успешен, найденный элемент переопределяется.
-
В противном случае фреймворк определяет, сколько элементов с указанным путем и именем содержится в родительском дескрипторе. Если есть только один элемент, он переопределяется.
-
Если поиск не дает результата и в родительском дескрипторе имеется ноль или более одного элемента с заданным путем и именем, добавляется новый элемент.
-
-
Текст для переопределенного или добавленного элемента копируется из расширяющего элемента.
-
Все атрибуты из расширяющего элемента копируются в переопределенный или добавленный элемент. Если имена атрибутов совпадают, значение берется из расширяющего элемента.
-
По умолчанию новый элемент добавляется в конец списка соседних элементов. Для того чтобы добавить новый элемент в начало или с произвольным индексом, сделайте следующее:
-
Определите дополнительное пространство имен в расширяющем дескрипторе:
xmlns:ext="http://jmix.io/schema/flowui/view-ext"
. -
Добавьте атрибут
ext:index
с нужным индексом, например:ext:index="0"
к расширяемому элементу.
-
Переопределение Spring Beans
Все подсистемы Jmix используют бины Spring по их типу, а не по имени бина. Следовательно, бины можно переопределить, просто предоставив альтернативные реализации того же или расширенного типа. Мы рекомендуем следовать этому соглашению в ваших дополнениях и приложениях.
Чтобы переопределить бин Spring, определенный в дополнении, создайте его подкласс (или реализуйте тот же интерфейс) и объявите бин этого нового типа в конфигурации Java, добавив аннотацию @Primary
к новому бину.
Например, предположим, что в дополнении base
есть следующий бин:
@Component("base_DepartmentService")
public class DepartmentService {
public void sayHello() {
System.out.println("Hello from base");
}
}
Его можно переопределить в приложении следующим образом:
-
Создайте его подкласс в проекте приложения:
public class ExtDepartmentService extends DepartmentService { @Override public void sayHello() { super.sayHello(); System.out.println("Hello from ext"); } }
Определите бин с аннотацией
@Primary
в основном классе приложения или в любом классе@Configuration
:@SpringBootApplication public class ExtApplication implements AppShellConfigurator { @Primary @Bean ExtDepartmentService extDepartmentService() { return new ExtDepartmentService(); }
После этого контейнер Spring всегда будет возвращать ExtDepartmentService
вместо DepartmentService
, поэтому любой вызов метода sayHello()
даже из дополнения base
будет выводить сообщения "Hello from base" и "Hello from ext". Конечно, можно не вызывать super()
в переопределяющих методах и, следовательно, полностью заменить унаследованное поведение.
В редком случае, когда вам нужно переопределить бин, у которого уже есть подкласс, отмеченный как @Primary
, вы можете использовать свойство приложения jmix.core.exclude-beans для удаления других первичных бинов из контейнера.
API-модули
Бин JmixModules
позволяет получать информацию о модулях, используемых в вашем приложении: список всех модулей, последний модуль в списке (обычно это приложение), дескриптор модуля по идентификатору модуля. Метод getPropertyValues()
возвращает список значений, определенных для свойства каждым модулем.
Бин JmixModulesAwareBeanSelector
предназначен для выбора эффективной реализации некоторого интерфейса из заданного списка. Он возвращает бин, принадлежащий самому низкому модулю в иерархии. Например, если вы знаете, что несколько дополнений определяют свои собственные реализации интерфейса AmountCalculator
, и вы хотите использовать тот, который определен в самом нижнем модуле иерархии, это можно сделать следующим образом:
@Autowired
ApplicationContext applicationContext;
@Autowired
JmixModulesAwareBeanSelector beanSelector;
BigDecimal calculate() {
Map<String, AmountCalculator> calculators = applicationContext.getBeansOfType(AmountCalculator.class);
AmountCalculator calculator = beanSelector.selectFrom(calculators.values());
return calculator.calculate();
}