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 扩展来自动创建并注入 Flyway
和 DataSource
实例。为了演示方便,我们使用 H2 嵌入式数据库,并通过 @DataSourceDefinition
注解提供配置。
5. CDI 容器初始化事件流程
CDI 容器启动过程中的关键事件如下:
- 加载并实例化所有 CDI 扩展
- 扫描并注册扩展中的事件监听方法
- 触发
BeforeBeanDiscovery
- 扫描类路径中的 Bean,每发现一个类型就触发
ProcessAnnotatedType
- 触发
AfterTypeDiscovery
- 进行 Bean 发现
- 触发
AfterBeanDiscovery
- 执行 Bean 校验,检测定义错误
- 触发
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 上找到。