Расширение функциональности

Функциональность подсистем и дополнений фреймворка может быть расширена и изменена приложением или другими дополнениями, расположенными ниже по иерархии.

Для декларативно созданных элементов, таких как сущности модели данных и XML-компоновки экранов UI, Jmix предлагает собственный механизм расширения. Бизнес-логика, определяемая бинами Spring, может быть расширена с использованием стандартных функций Java и Spring.

Расширение модели данных

Рассмотрим пример расширения модели данных дополнения.

Предположим, у нас есть следующие сущности, определенные в дополнении base:

extension diagram

Их исходный код:

@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:

extension diagram 2

Исходный код расширенной сущности:

@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, ссылающийся на дескриптор базового экрана:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">

После этого можно добавить компоненты для отображения расширенных атрибутов:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">
    <layout>
        <groupTable id="departmentsTable">
            <columns>
                <column id="description"/>
                <column id="manager"/>
            </columns>
        </groupTable>
    </layout>
</window>

Вам не нужно повторять все элементы и атрибуты базового экрана, требуется только измененная часть. Результирующий XML будет объединен из базовых и расширенных дескрипторов — подробнее об этом см. ниже.

В нашем случае один из расширенных атрибутов (manager) является ссылкой на другую сущность. Эта ссылка будет загружаться по запросу из-за автоматической ленивой загрузки, но вы можете включить ссылку на фетч-план экрана, чтобы избежать возможной проблемы "запросов N+1":

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">
    <data>
        <collection id="departmentsDc"
                    class="modularity.sample.ext.entity.ExtDepartment">
            <fetchPlan extends="_base">
                <property name="manager" fetchPlan="_base"/>
            </fetchPlan>
        </collection>
    </data>
    <layout>
        <groupTable id="departmentsTable">
            <columns>
                <column id="description"/>
                <column id="manager"/>
            </columns>
        </groupTable>
    </layout>
</window>

Контроллер расширенного экрана будет унаследован от базового класса контроллера:

@UiController("base_Department.browse")
@UiDescriptor("ext-department-browse.xml")
public class ExtDepartmentBrowse extends DepartmentBrowse {

Обратите внимание, что аннотация @UiController имеет то же значение, что и на базовом экране. Это важно, потому что на самом деле нам нужно переопределить базовый экран. А это означает, что расширенный экран в системе повсеместно будет использоваться вместо базового, как это делается для замененных сущностей.

Общедоступные и защищенные методы базового контроллера можно переопределить, чтобы при необходимости расширить логику экрана.

Правила расширения XML-дескрипторов

Расширение дескрипторов XML не учитывает семантику экрана и работает исключительно на уровне XML. Оно объединяет два файла XML в соответствии со следующими правилами:

  1. Если в расширяющем дескрипторе есть определенный элемент, соответствующий элемент будет искаться в родительском дескрипторе по следующему алгоритму:

    1. Если переопределяющий элемент имеет атрибут id, будет найден соответствующий элемент с таким же id.

    2. Если поиск успешен, найденный элемент переопределяется.

    3. В противном случае фреймворк определяет, сколько элементов с указанным путем и именем содержится в родительском дескрипторе. Если есть только один элемент, он переопределяется.

    4. Если поиск не дает результата и в родительском дескрипторе имеется ноль или более одного элемента с заданным путем и именем, добавляется новый элемент.

  2. Текст для переопределенного или добавленного элемента копируется из расширяющего элемента.

  3. Все атрибуты из расширяющего элемента копируются в переопределенный или добавленный элемент. Если имена атрибутов совпадают, значение берется из расширяющего элемента.

  4. По умолчанию новый элемент добавляется в конец списка соседних элементов. Для того чтобы добавить новый элемент в начало или с произвольным индексом, сделайте следующее:

    1. Определите дополнительное пространство имен в расширяющем дескрипторе: xmlns:ext="http://jmix.io/schema/ui/window-ext".

    2. Добавьте атрибут 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");
    }
}

Его можно переопределить в приложении следующим образом:

  1. Создайте его подкласс в проекте приложения:

    public class ExtDepartmentService extends DepartmentService {
    
        @Override
        public void sayHello() {
            super.sayHello();
            System.out.println("Hello from ext");
        }
    }

    Определите бин с аннотацией @Primary в основном классе приложения или в любом классе @Configuration:

    @SpringBootApplication
    public class ExtApplication {
    
        @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();
}