1. 概述

CDI(Contexts and Dependency Injection)是 Jakarta EE 平台的标准依赖注入框架。

在本教程中,我们将深入了解 CDI 2.0,看看它是如何在 CDI 1.x 强大的类型安全注入机制基础上,新增了一个功能完善的事件通知模型(Event Notification Model)

2. Maven 依赖

我们从一个简单的 Maven 项目开始。

需要一个兼容 CDI 2.0 的容器,而 Weld 是 CDI 的参考实现,非常适合使用:

<dependencies>
    <dependency>
        <groupId>javax.enterprise</groupId>
        <artifactId>cdi-api</artifactId>
        <version>2.0.SP1</version>
    </dependency>
    <dependency>
        <groupId>org.jboss.weld.se</groupId>
        <artifactId>weld-se-core</artifactId>
        <version>3.1.6.Final</version>
    </dependency>
</dependencies>

一如既往,我们可以在 Maven Central 上获取最新版本的 cdi-apiweld-se-core

3. 自定义事件的监听与处理

简单来说,CDI 2.0 的事件通知模型是经典的 观察者模式 实现,基于 @Observes 注解来实现方法参数的监听。

它允许我们轻松定义监听方法,这些方法会在一个或多个事件触发时自动被调用。

例如,我们可以定义一个或多个 Bean 来触发特定事件,而其他 Bean 会监听这些事件并作出响应。

为了更清楚地展示其工作原理,我们构建一个简单的示例,包括一个基础服务类、一个自定义事件类,以及一个监听并处理该事件的方法。

3.1. 基础服务类

我们先创建一个简单的 TextService 类:

public class TextService {

    public String parseText(String text) {
        return text.toUpperCase();
    }
}

3.2. 自定义事件类

接下来定义一个事件类,构造器接受一个 String 参数:

public class ExampleEvent {
    
    private final String eventMessage;

    public ExampleEvent(String eventMessage) {
        this.eventMessage = eventMessage;
    }
    
    // getter
}

3.3. 使用 @Observes 定义监听方法

定义完服务类和事件类之后,我们使用 @Observes 注解来创建一个监听 ExampleEvent 的方法:

public class ExampleEventObserver {
    
    public String onEvent(@Observes ExampleEvent event, TextService textService) {
        return textService.parseText(event.getEventMessage());
    } 
}

乍一看,onEvent() 方法的实现非常简单,但其实它通过 @Observes 注解封装了强大的功能。

可以看到,该方法是一个事件处理器,接受 ExampleEventTextService 两个参数。

注意:所有在 @Observes 注解后的参数都是标准的注入点。CDI 会自动创建并注入这些实例到监听方法中。

3.4. 初始化 CDI 2.0 容器

现在我们已经定义好了服务类、事件类和监听方法,那如何在运行时让 CDI 自动注入这些实例呢?

这时候事件通知模型就展现出它的强大之处了。我们只需初始化新的 SeContainer 实现,并通过 fireEvent() 方法触发事件即可:

SeContainerInitializer containerInitializer = SeContainerInitializer.newInstance(); 
try (SeContainer container = containerInitializer.initialize()) {
    container.getBeanManager().fireEvent(new ExampleEvent("Welcome to Baeldung!")); 
}

⚠️ 注意:我们使用 SeContainerInitializerSeContainer 是因为当前是在 Java SE 环境中使用 CDI,而非 Jakarta EE。

ExampleEvent 被触发时,所有绑定的监听方法都会收到通知。

由于所有在 @Observes 注解后声明的参数都会被自动初始化,CDI 会负责构建完整的 TextService 对象图,并将其注入到 onEvent() 方法中。

简而言之,我们不仅拥有了类型安全的 IoC 容器,还具备了功能丰富的事件通知机制

4. ContainerInitialized 事件

在前面的例子中,我们通过自定义事件将事件传递给监听方法,并获取了初始化完成的 TextService 实例。

这在需要在应用多个地方传播事件时非常有用。

但有时候,我们只是想在容器启动时获取一些已经初始化好的对象,而不需要额外实现事件。

为此,CDI 2.0 提供了 ContainerInitialized 事件类,该事件会在 Weld 容器初始化时自动触发

我们可以通过监听该事件将控制权交给 ExampleEventObserver 类:

public class ExampleEventObserver {
    public String onEvent(@Observes ContainerInitialized event, TextService textService) {
        return textService.parseText(event.getEventMessage());
    }    
}

⚠️ 注意:ContainerInitialized 是 Weld 特有的事件类,如果更换 CDI 实现,需要调整监听方法。

5. 条件监听方法

目前我们定义的 ExampleEventObserver 类中的监听方法是无条件的,也就是说 无论当前上下文中是否存在该类的实例,监听方法都会被触发

但我们可以使用 notifyObserver=IF_EXISTS 参数将监听方法定义为条件监听方法:

public String onEvent(@Observes(notifyObserver=IF_EXISTS) ExampleEvent event, TextService textService) { 
    return textService.parseText(event.getEventMessage());
}

使用条件监听方法时,只有在当前上下文中存在该类实例时,监听方法才会被调用

6. 事务监听方法

我们也可以在事务中触发事件,例如数据库更新或删除操作。为此,可以通过在 @Observes 注解中添加 during 参数来定义事务监听方法。

during 参数支持以下事务阶段:

  • BEFORE_COMPLETION
  • AFTER_COMPLETION
  • AFTER_SUCCESS
  • AFTER_FAILURE

如果我们在事务中触发 ExampleEvent,就需要调整 onEvent() 方法来处理指定阶段的事件:

public String onEvent(@Observes(during=AFTER_COMPLETION) ExampleEvent event, TextService textService) { 
    return textService.parseText(event.getEventMessage());
}

事务监听方法只在匹配的事务阶段才会被触发

7. 监听方法排序

CDI 2.0 的事件通知模型还支持对监听方法的调用顺序进行控制。

我们可以通过在 @Observes 后使用 @Priority 注解来定义监听方法的优先级。

为了演示该功能,我们再定义一个监听类:

public class AnotherExampleEventObserver {
    
    public String onEvent(@Observes ExampleEvent event) {
        return event.getEventMessage();
    }   
}

默认情况下,这两个监听方法具有相同的优先级,CDI 调用顺序是不确定的

我们可以通过 @Priority 明确指定优先级:

public String onEvent(@Observes @Priority(1) ExampleEvent event, TextService textService) {
    // ... implementation
}
public String onEvent(@Observes @Priority(2) ExampleEvent event) {
    // ... implementation
}

优先级值越小,越先被调用。如果多个方法具有相同的优先级,则调用顺序仍不确定。

8. 异步事件

前面的所有示例都是同步触发事件。但 CDI 2.0 也支持异步事件,监听方法可以在不同线程中处理事件。

我们可以通过 fireAsync() 方法异步触发事件:

public class ExampleEventSource {
    
    @Inject
    Event<ExampleEvent> exampleEvent;
    
    public void fireEvent() {
        exampleEvent.fireAsync(new ExampleEvent("Welcome to Baeldung!"));
    }   
}

Event 是一个接口,Bean 可以像普通 Bean 一样注入它

要处理异步事件,我们需要使用 @ObservesAsync 注解定义异步监听方法:

public class AsynchronousExampleEventObserver {

    public void onEvent(@ObservesAsync ExampleEvent event) {
        // ... implementation
    }
}

9. 总结

在本教程中,我们学习了如何使用 CDI 2.0 提供的增强事件通知模型。

一如既往,本文中的所有代码示例都可以在 GitHub 上找到。


原始标题:Introduction to the Event Notification Model in CDI 2.0