«1. Обзор

В этом руководстве мы рассмотрим интересную функцию CDI (внедрение контекста и зависимостей), называемую переносимым расширением CDI.

Сначала мы начнем с понимания того, как это работает, а затем посмотрим, как написать расширение. Мы пройдем шаги по внедрению модуля интеграции CDI для Flyway, чтобы мы могли запустить миграцию базы данных при запуске контейнера CDI.

Это руководство предполагает базовое понимание CDI. Взгляните на эту статью для введения в CDI.

2. Что такое портативное расширение CDI?

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

Во время этого процесса сканирования контейнер CDI запускает множество событий инициализации, которые могут наблюдаться только расширениями. Именно здесь в игру вступает портативное расширение CDI.

Расширение CDI Portable наблюдает за этими событиями, а затем изменяет или добавляет информацию в метаданные, созданные контейнером.

3. Зависимости Maven

Начнем с добавления необходимой зависимости для CDI API в pom.xml. Этого достаточно для реализации пустого расширения.

<dependency>
    <groupId>javax.enterprise</groupId>
    <artifactId>cdi-api</artifactId>
    <version>2.0.SP1</version>
</dependency>

А для запуска приложения мы можем использовать любую совместимую реализацию CDI. В этой статье мы будем использовать реализацию Weld.

<dependency>
    <groupId>org.jboss.weld.se</groupId>
    <artifactId>weld-se-core</artifactId>
    <version>3.0.5.Final</version>
    <scope>runtime</scope>
</dependency>

Вы можете проверить, были ли выпущены какие-либо новые версии API и реализации в Maven Central.

4. Запуск Flyway в среде, отличной от CDI

Прежде чем мы начнем интегрировать Flyway и CDI, мы должны сначала рассмотреть, как запустить его в среде, отличной от CDI.

Итак, давайте взглянем на следующий пример, взятый с официального сайта Flyway:

DataSource dataSource = //...
Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
flyway.migrate();

Как мы видим, мы используем только экземпляр Flyway, которому нужен экземпляр DataSource.

Наше портативное расширение CDI позже создаст bean-компоненты Flyway и Datasource. В этом примере мы будем использовать встроенную базу данных H2 и предоставим свойства DataSource через аннотацию DataSourceDefinition.

5. События инициализации контейнера CDI

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

  1. Fires BeforeBeanDiscovery event before the scanning process begins
  2. Performs the type discovery in which it scans archive beans, and for each discovered type it fires the ProcessAnnotatedType event
  3. Fires the AfterTypeDiscovery event
  4. Performs the bean discovery
  5. Fires the AfterBeanDiscovery event
  6. Performs bean validation and detect definition errors
  7. Fires the AfterDeploymentValidation event

Назначение переносимого расширения CDI состоит в том, чтобы наблюдать за этими событиями, проверять метаданные об обнаруженных bean-компонентах, изменять эти метаданные или добавлять к ним.

В портативном расширении CDI мы можем только наблюдать за этими событиями.

6. Написание переносимого расширения CDI

Давайте посмотрим, как мы можем подключиться к некоторым из этих событий, создав собственное переносимое расширение CDI.

6.1. Реализация провайдера SPI

Портативное расширение CDI — это провайдер Java SPI интерфейса javax.enterprise.inject.spi.Extension. Взгляните на эту статью для введения в Java SPI.

Во-первых, мы начнем с реализации расширения. Позже мы добавим методы наблюдателя к событиям начальной загрузки контейнера CDI:

public class FlywayExtension implements Extension {
}

Затем мы добавим имя файла META-INF/services/javax.enterprise.inject.spi.Extension со следующим содержимым:

com.baeldung.cdi.extension.FlywayExtension

В качестве SPI это расширение загружается до начальной загрузки контейнера. Таким образом, методы наблюдателя для событий начальной загрузки CDI могут быть зарегистрированы.

6.2. Определение методов Observer для событий инициализации

В этом примере мы делаем класс Flyway известным CDI-контейнеру до того, как начнется процесс сканирования. Это делается в методе наблюдателя registerFlywayType():

public void registerFlywayType(
  @Observes BeforeBeanDiscovery bbdEvent) {
    bbdEvent.addAnnotatedType(
      Flyway.class, Flyway.class.getName());
}

Здесь мы добавили метаданные о классе Flyway. С этого момента он будет вести себя так, как если бы он был просканирован контейнером. Для этого мы использовали метод addAnnotatedType().

Далее мы рассмотрим событие ProcessAnnotatedType, чтобы сделать класс Flyway управляемым компонентом CDI:

public void processAnnotatedType(@Observes ProcessAnnotatedType<Flyway> patEvent) {
    patEvent.configureAnnotatedType()
      .add(ApplicationScoped.Literal.INSTANCE)
      .add(new AnnotationLiteral<FlywayType>() {})
      .filterMethods(annotatedMethod -> {
          return annotatedMethod.getParameters().size() == 1
            && annotatedMethod.getParameters().get(0).getBaseType()
              .equals(javax.sql.DataSource.class);
      }).findFirst().get().add(InjectLiteral.INSTANCE);
}

«

«Сначала мы аннотируем класс Flyway аннотациями @ApplicationScoped и @FlywayType, затем ищем метод Flyway.setDataSource(DataSource dataSource) и аннотируем его @Inject.

@ApplicationScoped
@FlywayType
public class Flyway {
 
    //...
    @Inject
    public void setDataSource(DataSource dataSource) {
      //...
    }
}

Окончательный результат вышеописанных операций будет таким же, как если бы контейнер сканировал следующий bean-компонент Flyway:

Следующим шагом будет сделать bean-компонент DataSource доступным для внедрения, поскольку наш bean-компонент Flyway зависит от Компонент DataSource.

void afterBeanDiscovery(@Observes AfterBeanDiscovery abdEvent, BeanManager bm) {
    abdEvent.addBean()
      .types(javax.sql.DataSource.class, DataSource.class)
      .qualifiers(new AnnotationLiteral<Default>() {}, new AnnotationLiteral<Any>() {})
      .scope(ApplicationScoped.class)
      .name(DataSource.class.getName())
      .beanClass(DataSource.class)
      .createWith(creationalContext -> {
          DataSource instance = new DataSource();
          instance.setUrl(dataSourceDefinition.url());
          instance.setDriverClassName(dataSourceDefinition.className());
              return instance;
      });
}

Для этого мы выполним регистрацию Bean-компонента DataSource в контейнере и воспользуемся событием AfterBeanDiscovery:

Как мы видим, нам нужно DataSourceDefinition, предоставляющее свойства DataSource.

@DataSourceDefinition(
  name = "ds", 
  className = "org.h2.Driver", 
  url = "jdbc:h2:mem:testdb")

Мы можем аннотировать любой управляемый компонент следующей аннотацией:

public void detectDataSourceDefinition(
  @Observes @WithAnnotations(DataSourceDefinition.class) ProcessAnnotatedType<?> patEvent) {
    AnnotatedType at = patEvent.getAnnotatedType();
    dataSourceDefinition = at.getAnnotation(DataSourceDefinition.class);
}

Чтобы извлечь эти свойства, мы наблюдаем событие ProcessAnnotatedType вместе с аннотацией @WithAnnotations:

void runFlywayMigration(
  @Observes AfterDeploymentValidation adv, 
  BeanManager manager) {
    Flyway flyway = manager.createInstance()
      .select(Flyway.class, new AnnotationLiteral<FlywayType>() {}).get();
    flyway.migrate();
}

И, наконец, мы слушаем AfterDeploymentValidation, чтобы получить требуемый bean-компонент Flyway из контейнера CDI, а затем вызвать метод migrate():

7. Заключение

Поначалу создание переносимого расширения CDI кажется жизненный цикл инициализации контейнера и SPI, предназначенный для расширений, он становится очень мощным инструментом, который мы можем использовать для создания фреймворков поверх Jakarta EE.