Перейти к основному содержимому

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

· 5 мин. чтения

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. Запускает событие BeforeBeanDiscovery перед началом процесса сканирования
  2. Выполняет обнаружение типа, в ходе которого сканирует архивные компоненты, и для каждого обнаруженного типа запускает событие ProcessAnnotatedType .
  3. Запускает событие AfterTypeDiscovery
  4. Выполняет обнаружение компонента
  5. Запускает событие AfterBeanDiscovery
  6. Выполняет проверку бина и обнаруживает ошибки определения
  7. Запускает событие AfterDeploymentValidation

Затем переносимое расширение 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.foreach.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.

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

@ApplicationScoped
@FlywayType
public class Flyway {

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

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

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

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;
});
}

Как мы видим, нам нужно 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);
}

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

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

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

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

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