CDI Portable Extension и Flyway

CDI Портативное расширение и Flyway

1. обзор

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

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

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

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

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

During this scanning process, the CDI container fires many initialization events which can be only observed by extensions. Именно здесь в игру вступает портативное расширение CDI.

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

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

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


    javax.enterprise
    cdi-api
    2.0.SP1

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


    org.jboss.weld.se
    weld-se-core
    3.0.5.Final
    runtime

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

4. Запуск пролетного пути в среде без CDI

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

Итак, давайте посмотрим на следующий пример, взятый изhttps://flywaydb.org/getstarted/firststeps/api:

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. Запускает событиеBeforeBeanDiscovery перед началом процесса сканирования

  2. Выполняет обнаружение типов, при котором сканирует архивные компоненты, и для каждого обнаруженного типа запускает событиеProcessAnnotatedType

  3. Запускает событиеAfterTypeDiscovery

  4. Выполняет обнаружение бина

  5. ЗапускаетAfterBeanDiscovery седьмой

  6. Выполняет проверку бина и обнаруживает ошибки определения

  7. Запускает событиеAfterDeploymentValidation

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

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

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

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

6.1. Реализация поставщика SPI

Портативное расширение CDI - это провайдер Java SPI интерфейсаjavax.enterprise.inject.spi.Extension.. Взгляните наthis article, чтобы познакомиться с Java SPI.

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

public class FlywayExtension implements Extension {
}

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

com.example.cdi.extension.FlywayExtension

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

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

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

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

Здесь мы добавили метаданные о классеFlyway. From now on, it’ll behave as if it was scanned by the container. Для этого мы использовали методaddAnnotatedType().

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

public void processAnnotatedType(@Observes ProcessAnnotatedType patEvent) {
    patEvent.configureAnnotatedType()
      .add(ApplicationScoped.Literal.INSTANCE)
      .add(new AnnotationLiteral() {})
      .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.

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

@ApplicationScoped
@FlywayType
public class Flyway {

    //...
    @Inject
    public void setDataSource(DataSource dataSource) {
      //...
    }
}

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

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

void afterBeanDiscovery(@Observes AfterBeanDiscovery abdEvent, BeanManager bm) {
    abdEvent.addBean()
      .types(javax.sql.DataSource.class, DataSource.class)
      .qualifiers(new AnnotationLiteral() {}, new AnnotationLiteral() {})
      .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;
      });
}

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

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

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

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

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

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

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

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

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

Как обычно, все примеры кода, показанные в этой статье, можно найтиover on GitHub.