Java CDI 相关整理
2018-05-28 本文已影响38人
58bc06151329
文前说明
作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
1. 简介
- CDI(Contexts And Dependency Injection)是 JavaEE 6 标准中一个规范,将依赖注入 IOC/DI 上升到容器级别, 它提供了 Java EE 平台上服务注入的组件管理核心,简化是 CDI 的目标,让一切都可以被注解被注入。
- CDI 的思想来自 Spring,但是它的开发模式来自 Jboss seam。
- CDI 是为了解决 EJB、javabean 被 web 层组件引用困难的问题。
- 大目标是为了提供 Java Web 开发中一种通用、快捷的途径。
- Contexts and Dependency Injection,意思是 上下文依赖注入,依赖注入基本概念和 Spring 一样。
- 上下文就是熟知的 Context,对于 CDI 而言,具有上下文的特性,就认为具有管理 Bean 生命周期的能力。
- 因为 CDI 是为了 Web 开发而成,这种上下文概念就认为每个 Web Bean 是有状态的,它的生命周期将由客户端(http client)的状态所决定,和无状态组件模型(POJO,stateless EJB)或一个单例模型组件(如 Servlet 或单例 Bean)不同,一个 Web Bean 的不同客户端看到的 Web Bean 的状态是不同的。
- 客户端所见的状态取决于这个客户端拥有的是哪一个 Web Bean 实例的引用。
- 在 Maven 项目的 pom.xml 文件中进行引入包。
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
2. @Inject 注解
- CDI 主要使用 @Inject 注解来实现依赖注入,把受管理的 Bean 注入到由容器管理的其它资源中去。
2.1 构造器依赖注入
- 使用 @Inject 进行了注解的构造器时,这种情况下,容器会改用有注解的构造器而不是无参构造器,并且把通过构造器参数传入的依赖资源注入到 Bean 实例中来。
- 一个类只允许有 一个 @Inject 注解的构造器。
public class SomeBean {
private final Service service;
@Inject
public SomeBean(Service service){
this.service = service;
}
}
2.2 字段依赖注入
- 当容器初始化一个 SomeBean 类型的 Bean 时,它会把一个正确的 Service 实例注入给该字段,即使该字段是一个私有字段,并且不需要有任何 setter 方法。
public class SomeBean {
@Inject
private Service service;
}
2.3 初始化方法依赖注入
- 当容器初始化一个 SomeBean 类型的 Bean 时,会调用所有由 @Inject 注解了的方法,并且通过方法参数的方式把依赖注入进来。
public class SomeBean {
private Service service;
@Inject
public void setService(Service service) {
this.service = service;
}
}
3. @Any 修饰符
- 为了提供完全松耦合的应用,通常把接口注入到受管理的资源中。当有多个实现了给定接口的 Bean时,可以同时使用 @Any 和 Instance 接口,把所有该接口的实现 Bean 都注入进一个受管理的 Bean 中。
- @Any 修饰符告诉容器,任何可供使用的依赖都适用于该注入点,所以容器会把他们都注入进来。
- 如果接口的多个实现只注入了其中的一个,并且没有做任何排除工作,那么容器将会无法成功的初始化组件。
public class SomeBean {
@Inject
public void listServiceImplementations(
@Any Instance<Service> serviceList) {
for(Service service : serviceList){
System.out.println(service.getClass().getCanonicalName());
}
}
}
4. 注入到生产者方法中
- Java EE CDI 提出了生产者概念。
- 生产者可以用来创建或生产 Bean 的实例,用以被应用程序所消耗。
- 生产者也能够根据消费者的需求提供特定的接口实现,因此它们是支持 CDI 应用程序中 多态性 的有效方式。
- 生产者也有助于封装 Bean 初始化,甚至能够注入对象实例,这些对象实例本身不是 CDI 管理 Bean,而是在我们的应用程序的某些点中。
4.1 用例说明
4.1.1 服务
-
在示例中实现一个生产者,它将创建并向消费者提供一个示例消息发送服务。
- 假设定义一个服务实现,表示通过电子邮件发送消息。
- 再定义第二个服务实现,表示通过 SMS 代表消息发送。
-
定义消息发送服务接口
public interface MessageSender {
void sendMessage();
}
- 定义电子邮件消息发送实现。
public class EmailMessageSender implements MessageSender {
@Override
public void sendMessage() {
System.out.println("Sending email message");
}
}
- 定义 SMS 消息发送实现。
public class SmsMessageSender implements MessageSender {
@Override
public void sendMessage() {
System.out.println("Sending SMS message");
}
}
4.1.2 生成者
- 定义了一个 CDI 管理 Bean,其中包含一个用于创建 MessageSender 实例的生产者方法。
- 通过在 getMessageSender() 方法上添加 @Produces 注解,CDI 容器就能够生产 MessageSender 的实例。
- 再通过 @Inject 注入 MessageSender 的实例到容器中。
@SessionScoped
public class MessageSenderFactory implements Serializable {
private static final long serialVersionUID = 5269302440619391616L;
@Produces
public MessageSender getMessageSender(){
return new EmailMessageSender();
}
}
@Inject
private MessageSender messageSender;
4.1.3 多 Bean 的实现(多态)
- 可以修改生产方法 getMessageSender 用来支持多态。
package com.byteslounge.bean;
public enum MessageTransportType {
EMAIL, SMS;
}
@SessionScoped
public class MessageSenderFactory implements Serializable {
private static final long serialVersionUID = 5269302440619391616L;
private MessageTransportType messageTransportType;
@Produces
public MessageSender getMessageSender() {
switch (messageTransportType) {
case EMAIL:
return new EmailMessageSender();
case SMS:
default:
return new SmsMessageSender();
}
}
}
4.1.4 通过 @Qualifier 实现多态
- 定义一个包含 CDI @Qualifier 注解的新的注解 @MessageTransport。
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface MessageTransport {
MessageTransportType value();
}
- @MessageTransport 可以用于消除服务实现的歧义。
- 每个生产者方法将根据注入点中使用的 @MessageTransport 返回一个不同的消息服务实现。
@SessionScoped
public class MessageSenderFactory implements Serializable {
private static final long serialVersionUID = 5269302440619391616L;
@Produces
@MessageTransport(MessageTransportType.EMAIL)
public MessageSender getEmailMessageSender(){
return new EmailMessageSender();
}
@Produces
@MessageTransport(MessageTransportType.SMS)
public MessageSender getSmsMessageSender(){
return new SmsMessageSender();
}
}
- 注入消息发送服务时,同样可以通过 CDI 注解 @MessageTransport 来选择使用哪种实现方式。
@Inject
@MessageTransport(MessageTransportType.EMAIL)
private MessageSender messageSender;
@Inject
@MessageTransport(MessageTransportType.SMS)
private MessageSender messageSender;
4.1.5 生产 CDI 管理 Bean
- 如果生产一个 Bean,该 Bean 有自己的 CDI 依赖关系,那么这些关系在初始化的过程中应该交由容器进行处理。这种情况可以在生产者方法中使用参数注入。
- 应用程序中注入 MessageSender 实现时,CDI 容器将初始化一个 EmailMessageSender
实例并将其作为参数注入到生产者方法中。然后,该方法返回由 CDI 初始化的新服务实例。
- 应用程序中注入 MessageSender 实现时,CDI 容器将初始化一个 EmailMessageSender
@Produces
public MessageSender getEmailMessageSender(EmailMessageSender emailMessageSender){
return emailMessageSender;
}
4.1.6 生产方法的作用域
- 生产方法有着自己的作用域,它们不继承它们所生成的 Bean 的作用域。
- 默认情况下,生产者方法具有 @Dependent 作用域。每当我们获取某个生产者方法生成的实例时,总是会调用该方法。
- 将生产者方法定义为 @SessionScoped 会话作用域。这意味着每个 HTTP 会话只执行一次方法。返回的实例 EmailMessageSender 也是 @SessionScoped 会话作用域。
@Produces
@SessionScoped
public MessageSender getEmailMessageSender(
EmailMessageSender emailMessageSender){
return emailMessageSender;
}
- 向生产者注入一个与生产者本身有着不同作用域的 Bean。
- 如果 SomeService 参数是一个 Bean 的定义,例如它的是 @RequestScoped 请求作用域,它将被生产者方法设置为 @SessionScoped 会话作用域,然后返回给调用者。
- 这是一个问题。因为一旦当前的 HTTP 请求完成,SomeService 实例将被销毁,但是
因为 @SessionScoped 的缘故,CDI 将保留一个对 SomeService 的引用,从而会对进一步的服务注入产生不可预知的结果。
- 这是一个问题。因为一旦当前的 HTTP 请求完成,SomeService 实例将被销毁,但是
- 如果 SomeService 参数是一个 Bean 的定义,例如它的是 @RequestScoped 请求作用域,它将被生产者方法设置为 @SessionScoped 会话作用域,然后返回给调用者。
- 我们可以改变 Bean 的作用域,但是这样会影响 Bean 的所有其他消费者。
@Produces
@SessionScoped
public SomeService getService(SomeService someService){
return someService;
}
- 另外一种办法是使用新的注解。
- 使用新的注解 @New,CDI 将总是向生产者方法中注入一个 新 的依赖实例,然后将其安全地提升到生产者方法的 @SessionScoped 会话作用域,在这种情况下,再将它返回给调用方。
@Produces
@SessionScoped
public SomeService getService(@New SomeService someService){
return someService;
}
5. Bean 的作用域
- 当一个 Bean 被 CDI 初始化时,这个 Bean 通常会有自己的作用域。而赋予它的作用域通常就会决定了这个 Bean 的整个生命周期。
- CDI 提供以下 Bean 作用域
作用域 | 描述 |
---|---|
ApplicationScoped | 当 Bean 被定义为 Application 作用域时,意味着 Bean 中只有一个实例将存在于整个应用程序中。在 Bean 初始化后,每当客户端请求这个 Bean 的实例时,容器总是提供相同的 Bean 实例。 |
SessionScoped | SessionScoped 主要用于 Web 环境下应用的开发。当我们使用一个作用域为 Session 的 CDI Bean 时,表示 Bean 实例的生命周期取决于每个 HTTP 会话。 |
RequestScoped | RequestScoped 主要用于 Web 环境下应用的开发。当我们使用一个作用域为 Request 的 CDI Bean时,表示 Bean 实例的生命周期取决于每个 HTTP 请求。 |
ConversationScoped | ConversationScoped 主要用于 Web 环境下应用的开发。如同其名一样,ConversationScoped 标志客户端同服务端的一次交流 。他们可能被用于保持多个 Ajax 请求或者不同页面请求与服务端交互的状态信息。 |
- java EE 7 引入了一套新的 CDI Bean 的作用域。
作用域 | 描述 |
---|---|
TransactionScoped | Transaction 作用域的 Bean 将通过活动事务的持续时间生成其生命周期。一旦这种类型的 Bean 在事务中被第一次引用,CDI 容器将创建 Bean 的实例并在整个事务生命周期中重用它。如果 Bean 在相同事务中稍后再次引用,容器将提供相同的实例给调用者。 |
FlowScoped | 在 JSF 流中使用 Flow 作用域 Bean。流程用于表示工作单元,并由包含的视图集和 Bean 定义。一个流式作用域 Bean 将在其第一次引用的流的持续时间中产生它的生命周期。 |
ViewScoped | View 作用域 Bean 将在 JSF 视图的持续时间中产生它的生命周期。由于这种 Bean 在整个视图生命周期中保持它们的状态,因此它们以对话的方式在客户端和服务器之间建立 Ajax 交互变得很有作用。 |
- CDI 还提供了两个附加的伪作用域。
伪作用域 | 描述 |
---|---|
Singleton | 如同其名一样,Singleton Bean 标志着单实例,如果一个 Bean 被用作 Singleton 意味着从始至终仅仅有一个实例存在。 |
Dependent | Dependent scope Bean 的作用域与其被注入的 Bean 的作用域一样。如当一个定义为Dependent 的 Bean 被注入到一个会话 Bean 中,那么这个 Bean 的作用域和生命周期同这个会话Bean 一样,当会话结束此 Bean 的生命周期也就截止了。 |
5.1 代理
- 当一个被 CDI 容器管理的 Bean 被注入到其他 Bean 中时,CDI 容器并不是注入的 Bean 实例,而是注入的一个代理类。通常这个代理类会将客户端的相应请求再转发给相应的 Bean 去处理。
- 想想 SessionScoped Bean,如果不同的会话请求一个 SessionScoped 会话实例,这些请求都会被交给同一个代理处理。当这些请求访问这些被代理的对象时,代理对象知道如何找到相应的对象实例并为相应的会话提供这却的服务。
- 注意:单例的 pseduo-scope 是代理机制的例外。当一个客户端请求一个单例的 Bean 时,CDI 容器会将这个 Bean 的实例注入到相应的 Bean 中而不再是一个代理。
5.2 可序列化的作用域
- 一些被 CDI 容器管理的 Bean 可以保持很长时间的生命周期,如 SessionScoped 和
ConversationScoped,但是他们必须实现序列化接口。- 这是由于容器要释放一些资源,而这些资源往往需要连同 Bean 的类信息持久化到物理磁盘上,等到程序再次需要的时候能够保证容器再次将这些 Bean 的状态从物理磁盘中恢复出来。
5.3 Singleton pseudo-scope
- 我们知道当我们使用 Singlescoped 时,客户端会得到该对象实例的真正引用。因此当客户端请求的是序列化的,就必须保证这个实例是单例的。我们保证这个单例的 Bean 是一个正真的单例的方法有
- 正如 Java 序列化标准声明的那样,这个单例要实现 writeReplace() 和 readResolve() 方法。
- 维护单例的引用对象的瞬时状态,当容器反序列化该对像时,容器要保证重新注入该对象的引用。
5.4 Dependent pseudo-scope
- 如果没有特别声明的话,Dependent 是 CDI 默认的作用域。Dependent 意味着注入的 Bean 与被注入的 Bean 有相同的作用域。客户端不会共享实例,每个客户端拿到的将会是一个新的对象实例。
6. Bean 销毁
- CDI 生产者方法可用于创建在特定上下文中由应用程序消耗的资源。当应用程序不再需要这些资源时,这些资源可能需要被容器清理干净,这个清理过程是由 CDI 以处理方法的形式提供的。
6.1 用例说明
- 生成和处理由连接表示的简单资源。创建连接接口。
public interface Connection {
void connect();
void closeConnection();
}
- 连接接口实现
public class ConnectionImpl implements Connection {
@Override
public void connect() {
System.out.println("Connecting...");
}
@Override
public void closeConnection() {
System.out.println("Closing connection...");
}
}
- 自定义一个处理连接生产和销毁的类
- getConnection 方法可以生产连接对象,作用域为 @RequestScoped。
- 这里增加了销毁处理方法 closeConnection。
- 销毁处理方法必须与生产方法返回的类型匹配,即有一个带有 @Disposes 注解配置的参数,该参数类型必须与 @Produces 注释方法的返回类型一致。(这里为 Connection 类型),这个参数还必须具有与生产者返回类型一致的限定符注解(这里为 @TestConnection)。
public class ConnectionFactory {
@Produces
@RequestScoped
@TestConnection
public Connection getConnection(){
Connection conn = new ConnectionImpl();
conn.connect();
return conn;
}
public void closeConnection(
@Disposes
@TestConnection Connection connection){
connection.closeConnection();
}
}
- 生产者方法用 @RequestScoped 注解,意味着对于注入一个连接实例的单个 HTTP 请求,生产者方法将被调用一次,并且在该 HTTP 请求的生命期内容器将保持连接引用,并且进一步的使用这个连接注入点。
- 当 HTTP 请求完成,容器将调用 @Disposes 方法进行资源清理。
- 之所以使用 @TestConnection 注解是为了消除歧义,避免多个连接对象时,容器不知道注入哪一个连接对象。
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
public @interface TestConnection {
}
@Inject
@TestConnection
private Connection connection;