1. 概述

在这篇文章中,我们将深入探讨 CDI(Contexts and Dependency Injection)的一个高级特性:CDI 可移植扩展(Portable Extension)

我们会先了解其工作原理,然后通过编写一个扩展来集成 Flyway,实现 CDI 容器启动时自动执行数据库迁移。本文假设你已经对 CDI 有一定了解。如果你还不熟悉,可以先阅读 这篇文章

2. 什么是 CDI 可移植扩展?

简单来说,CDI 可移植扩展是一种在标准 CDI 容器之上添加自定义功能的机制

在应用启动过程中,CDI 容器会扫描类路径并构建元数据。在此期间,它会触发一系列初始化事件,而这些事件只能被 CDI 扩展所监听。

CDI 可移植扩展的作用就是监听这些事件,从而修改或增强容器生成的元数据。

3. Maven 依赖配置

我们先添加 CDI API 的依赖,这足以支持我们开发一个空的扩展:

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

你可以前往 Maven Central 检查是否有更新版本的 API实现

4. 在非 CDI 环境下运行 Flyway

在集成 CDI 之前,我们先看看如何在普通 Java 环境中使用 Flyway。以下是一个官方示例:

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

可以看到,Flyway 的使用非常简单,只需要一个 DataSource 实例即可。

我们的目标是通过 CDI 扩展来自动创建并注入 FlywayDataSource 实例。为了演示方便,我们使用 H2 嵌入式数据库,并通过 @DataSourceDefinition 注解提供配置。

5. CDI 容器初始化事件流程

CDI 容器启动过程中的关键事件如下:

  1. 加载并实例化所有 CDI 扩展
  2. 扫描并注册扩展中的事件监听方法
  3. 触发 BeforeBeanDiscovery
  4. 扫描类路径中的 Bean,每发现一个类型就触发 ProcessAnnotatedType
  5. 触发 AfterTypeDiscovery
  6. 进行 Bean 发现
  7. 触发 AfterBeanDiscovery
  8. 执行 Bean 校验,检测定义错误
  9. 触发 AfterDeploymentValidation

⚠️ CDI 扩展的核心作用就是监听这些事件,从而干预元数据的构建过程。

6. 编写 CDI 可移植扩展

接下来我们通过实战来编写一个 CDI 扩展,实现 Flyway 与 CDI 的集成。

6.1. 实现 SPI 提供者

CDI 扩展本质上是一个实现了 javax.enterprise.inject.spi.Extension 接口的 Java SPI 提供者。

首先创建扩展类:

public class FlywayExtension implements Extension {
}

然后在 META-INF/services/javax.enterprise.inject.spi.Extension 文件中注册:

com.baeldung.cdi.extension.FlywayExtension

这样,CDI 容器启动前就会加载这个扩展,并注册其中的事件监听方法。

6.2. 监听初始化事件

注册 Flyway 类型

我们首先在扫描开始前将 Flyway 类注册到容器中:

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

这样,Flyway 就像被容器扫描到的类一样,可以参与后续的 Bean 构建流程。

将 Flyway 标记为托管 Bean

接下来,我们监听 ProcessAnnotatedType<Flyway> 事件,为 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);
}

这段代码的作用等价于我们手动写下面这个类:

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

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

提取 DataSource 配置

我们需要从注解中提取 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);
}

启动时执行迁移

最后,在部署验证阶段获取 Flyway 实例并执行迁移:

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 上找到。


原始标题:CDI Portable Extension and Flyway