Java EE 6 中的上下文和依赖注入

作者:Adam Bien

了解如何以及何时在 Java EE 6 中使用上下文和依赖注入模型。

2011 年 4 月发布


下载:
下载 Java EE 6

简介

Java Platform, Enterprise Edition (Java EE) 5 将具有惯例优于配置的依赖注入 (DI) 引入 Enterprise JavaBeans (EJB) 3.0 中。除了已有的 @EJB 批注之外,Java EE 6 还引入了灵活、强大的 @Inject 依赖注入模型(JSR-330 和 JSR-299)。那么何时使用此模型呢?

配置 EJB 3 依赖注入

EJB 3 依赖注入的使用极其简单。使用 @EJB 批注即可注入一个已声明的 bean。将 MessageSession bean 注入 Servlet 3.0 中如下所示:

@WebServlet(name="Messenger", urlPatterns={"/Messenger"})
public class Messenger extends HttpServlet {   
    @EJB
    MessageSession session;
// }

注入的 MessageSession bean 可以是接口视图 bean,也可以是无接口视图 bean。只要仅存在接口的一个实现,便可注入该 bean 而无需任何形式。需要配置只是为了明确选择。

@Stateless
@Local(MessageSession.class)
public class PersistentMessageSession implements MessageSession{

    @Override
    public String getReceivedMessage() {
        return "From persistent. Received at: " + new Date();
    }
}

@Stateless
@Local(MessageSession.class)
public class TransientMessageSession implements MessageSession {
//...implementation
}


清单 1:MessageSession 接口存在两个实现打破了惯例

MessageSession @Local 接口存在两个实现,这打破了惯例,因此将在部署期间抛出异常:

Exception while deploying the app: java.lang.RuntimeException: Cannot resolve reference Remote ejb-ref name=[...]because there are 2 ejbs  in the application with interface com.abien.di.messenger.MessageSession

使用 beanName 属性增强 @EJB 批注之后,该问题迎刃而解。beanName 属性的值是所需 bean 的简单名称 (getSimpleName)。

@EJB(beanName="TransientMessageSession")
MessageSession session;


依赖注入还可在部署描述文件(XML 配置)中而不是批注中配置,但仍基于字符串。在此,除了字符串匹配之外没有其他实现选择。

什么是 EJB 引用

在无接口视图的情况下,注入的实例既不是 @Local 接口的实现,也不是 bean 本身。Java EE 规范没有规定注入类的特性,但它暗示了使用间接方式。

EJB 容器可以透明地处理事务、并发性、安全性以及诸如拦截器、管理和监视之类的自定义特性。但这些特性只能通过间接方式实现。

@Local 接口注入的情况下,GlassFish Server 开源版将在运行时生成一个名为 $Proxy202 的类。该类是一个动态代理,并且“@Proxy”之后的数值不断增加。

@EJB(beanName="TransientMessageSession")
MessageSession session;


清单 2:@Local 接口注入

动态代理仅适用于接口。直接注入 @LocalBean:@EJB PersistentMessageSession 将导致 GlassFish 生成另一种类型的代理:

com.abien.di.messenger.ejb.__EJB31_Generated__PersistentMessageSession__Intf____Bean__

无论在哪种情况下,都可以将代理看作修饰器,因为它通过可重用的横切关注点增强了业务接口。

EJB 3 与上下文无关

EJB 3 依赖注入很简单。批注 @Stateless@Stateful bean 的名称从客户端的角度描述 bean 的行为。在使用 @Stateless 的情况下,代理和实际 bean 实例之间不存在已定义的关系,因此客户端和实际 bean 实例之间也不存在已定义的关系。几个客户端可以在方法调用之间“共享”一个 EJB 实例。另一方面,贪婪的客户端可能会导致服务器超载,并通过一个代理同时与几个实例通信。客户端和 bean 实例之间存在 N:M 关系。M 的大小取决于服务器配置和请求/事务负载。一个请求始终由单个 bean 实例完全处理。几个请求决不会同时共享一个实例。客户端不可能释放实例或与该实例“断开连接”或“解除关联”。

@Stateful bean 甚至更简单。客户端与关联的 bean 实例之间精确的 1:1 关系定义了有状态特性。一个客户端仅与一个 bean 实例通信。几个客户端无法(黑客攻击除外)共享一个实例。@Stateless@Stateful 会话 bean 可采用单线程方式开发:绝不会出现多个线程同时访问 bean 实例的情况。

客户端绝对不能控制 @Stateless 会话 bean 的生命周期。生命周期完全由容器管理。对 @Stateful bean 而言,情况恰好相反。客户端必须管理整个生命周期。@Stateful bean 由容器在注入期间创建。客户端通过调用一个带有 @Remove 批注的业务方法的或者一个容器特定的超时来启动销毁。

@Stateful bean 可注入 @Stateless bean 中,但结果不可预测。注入的 @Stateful 实例将直接绑定到给定的 @Stateless bean 实例。有状态会话 bean 实例的数量取决于池化的无状态 bean 的数量。HttpSession 和 @Stateful 实例之间的自动关联无法通过 EJB 透明地实现。开发人员需要手动将 bean 与 HttpSession 进行关联。Bean 对 HttpSession 以及 Servlet 规范中的其他概念一无所知。

CDI 中的“C”

上下文和依赖注入 (CDI) 是新的 Java EE 6 规范,它不仅定义了功能强大、类型安全的依赖注入,而且还引入“上下文”引用或作用域的概念。

CDI 中的“C”是 EJB bean 和托管 CDI bean 之间的主要区别。CDI 托管的 bean 是上下文 bean,而 EJB bean 不是。CDI 中的托管 bean 具有定义明确的作用域。它们由容器根据需要创建和销毁。CDI 已经附带了预定义的作用域和批注:@RequestScoped、@SessionScoped、@ApplicationScoped@ConversationScoped

CDI 容器自动管理作用域内的所有 bean。当 HttpSessionHttpRequest 结束时,将自动销毁与该作用域关联的所有实例,进行垃圾回收。

这种行为与有状态会话 bean 的行为大不相同。有状态会话 bean 实例需要由客户端通过调用带有 @Remove 批注的方法来显式删除。它不会由容器自动销毁,也没有绑定到任何上下文。如果将有状态会话 bean 与 HttpSession 关联,则在 HttpSession 结束或超时时,还必须关注该 bean 的可靠销毁。

CDI 的上下文特性可以更自然、更方便地使用来自不同作用域的 bean。您甚至可以对作用域进行混搭,并注入来自不同作用域的 bean。容器仍将关注正确的生命周期管理。

在不丢失上下文的情况下混合作用域

作用域可以进行任意混合。@SessionScoped bean 可以注入 @RequestScoped@ApplicationScoped bean 中,反之亦然。尽管 @SessionScoped 的上下文比 @RequestScoped 的上下文“更广泛”,但在调用期间,用户与其会话之间的关联不会丢失。容器将在 @RequestScoped bean 内选择正确的会话。在注入的代理中,该操作在后台执行。

在清单 3 中,需要在多个视图之间传递 inputText JavaServer Faces (JSF) 2 组件的值。这通常通过将该值存储在 HttpSession 中实现。

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:body>
        <h:form>
            Name:<h:inputText value="#{index.sessionInfo.name}"/>
            <h:commandButton value="Next" action="display"/>
        </h:form>
    </h:body>
</Html>

清单 3:Index.xhtml:在 HttpSession 中存储字符串名称

Index 辅助 bean 使用了 @Model 进行批注。@Model 是一个宏 (@Stereotype),它扩展到 @RequestScoped、@Named 批注。
import com.abien.di.messenger.SessionInfo;
import javax.enterprise.inject.Model;
import javax.inject.Inject;

@Model
public class Index {
    @Inject
    SessionInfo sessionInfo;
    public SessionInfo getSessionInfo(){
        return sessionInfo;
    }
}

清单 4:@RequestScoped 辅助 Bean

SessionInfo bean 是实际状态持有者(参见清单 5)。index.xhtml JSF 2 组件使用 EL 表达式 #{index.sessionInfo.name} 间接访问 SessionInfo 辅助 bean。首先访问 Index 辅助 bean,然后访问 SessionInfo(参见清单 5)。EL 表达式中的最后一部分负责访问 name 特性。

import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
@Named
@SessionScoped

public class SessionInfo implements Serializable{
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}


清单 5:状态持有者:@SessionScoped 辅助 Bean

由于 SessionInfo 没有绑定到 Index 辅助 bean(而不是当前的 HttpSession),因此用户将访问由 SessionInfo 实例表示的状态。

正确的作用域解析甚至适用于 @ApplicationScoped 辅助 bean,因此也适用于单体。尽管每个应用程序和 Java 虚拟机 (JVM) 只有一个 Display 辅助 bean 实例(参见清单 6),但容器仍会在 HttpSession 实例之间正确地分派注入的 SessionInfo 实例。不同的用户将始终查看他们的 SessionInfo 实例。

import com.abien.di.messenger.SessionInfo;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
@Named
@ApplicationScoped

public class Display {
    @Inject
    SessionInfo sessionInfo;
    public SessionInfo getSessionInfo() {
        return sessionInfo;
    }

}

清单 6:将 @SessionScoped Bean 注入一个“单体”中

也可以从视图中直接访问 SessionInfo 实例而无需任何注入(参见清单 7)。在这种情况下,SessionInfo 实例直接用作实际辅助 bean。注意,只有当前活动的 HttpSession 中的 SessionInfo 实例将值绑定到视图。

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
     <h:body>
        <h:form>
            Name:<h:inputText value="#{sessionInfo.name}"/>
            <h:commandLink action="display" value="back"/>
        </h:form>
    </h:body>
</Html>

清单 7:直接访问 SessionInfo 实例

字符串限定符已过时

注入时,在 beanName 属性中使用接口实现的简单名称 @EJB(beanName="TransientMessageSession") 可解析引起歧义的 EJB 依赖。

CDI 还支持通过纯 String 解析此类歧义。它甚至自带了内置限定符以实现此目的。可以使用 @Named 通过纯 String 对实现以及注入点进行批注。如果 string 值在注入点以及在实现时匹配,则会注入该值。该名称甚至可以从属性名称中获得。下面是注入 persistent 属性的示例:

@Inject @Named
   MessageSession persistent;

上述代码解析了使用 @Named("persistent") 批注的 CdiPersistentMessageSession 实现的注入(参见清单 8):

@Named("persistent")
public class CdiPersistentMessageSession implements MessageSession{
@Override
    public String getReceivedMessage() {
        return "From CDI persistent. Received at: " + new Date();
    }
}

清单 8:通过 @Named 解析歧义

还支持显式使用实现名称:

@Inject @Named("persistent")
MessageSession something;

@Inject@Named 的组合非常类似于 @EJB(beanName="...") 注入样式。与 EJB 不同,该规范版本不赞成使用 @Named 作为限定符。通过字符串限定依赖不是类型安全的。如果名称出现简单的拼写错误,将导致难以发现的错误。CDI 规范 (JSR-299) 不鼓励使用基于字符串的依赖解析:

“……不建议使用 @Named 作为注入点限定符,但与使用基于字符串的名称标识 bean 的原有代码进行集成的情况除外……”[http://www.jcp.org/en/jsr/summary?id=299,第 32 页]。

正确的方法

限定和配置依赖的推荐方法是使用带有可选属性的类型化批注,而非纯字符串。这些批注称为限定符。它是一个用 @Qualifier annotation 标记的标准批注。为了实现注入,限定符批注使用 FIELDTYPE 作为目标(参见清单 9)。

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE})
public @interface Session {
Type value();
    enum Type{
     TRANSIENT, PERSISTENT  
    }
}


Listing 9:用于消除依赖注入歧义的自定义限定符

配置相当容易。注入点使用自定义限定符进行批注,该限定符必须与所需的实现相匹配(参见清单 10)。不仅比较限定符的类型,而且还比较包含的所有属性。

@WebServlet(name="CDIMessenger", urlPatterns={"/CDIMessenger"})
public class CDIMessenger extends HttpServlet {   
@Inject @Session(Session.Type.TRANSIENT)
MessageSession persistent;
//...
}


清单 10:使用限定符配置注入点

使用批注属性配置注入将不需要引入其他批注。您可以使用单个批注并通过更改其属性来选择实现。

@Session(Session.Type.TRANSIENT)
public class CdiTransientMessageSession implements MessageSession {
//...
}


清单 11:使用限定符标记实现

如果无法比较批注属性,甚至 @Named 批注也无法奏效。它包括一个 String 类型的属性,该属性用于比较加了批注的元素。除了其语义表达匮乏之外,@Named 只是一个普通的限定符(参见清单 12)。

@Qualifier
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Named {
    public String value() default "";
}


清单 12:@Named 限定符剖析

不建议使用 @Named 配置注入,因为它太通用。@Named 不提供任何其他信息,并且 String 属性很容易出现拼写错误。

总结

大多数情况下,注入点和实现之间具有一对一关系。由于惯例优于配置,这种情况下您不必对任何内容进行配置或批注。EJB 3.1 和 CDI 注入模型的工作和行为方式类似。您要实现的系统越复杂,CDI 就会变得越有趣。配置 EJB bean 的 DI 类似于使用 @Named 限定符。CDI 远不止如此,并不限于纯 String 匹配。可以轻松引入自定义限定符,然后使用它们以类型安全的方式配置依赖注入。

EJB bean 与 CDI 的最大不同在其于不具备上下文功能。对于分别使用 @Stateless@Stateful 进行批注的 bean,注入的 EJB 代理仅指向实例池或单个 bean 实例。HttpRequestHttpSession 与给定的 EJB 实例之间没有自动关联。

对 CDI 而言,恰好相反。托管 bean 在其作用域内进行管理。容器知道当前作用域并同时管理 bean 实例。具备这两个条件之后,容器就能够选择正确的实例并将其持续注入限定作用域的 bean 中。无法针对 EJB bean 实现上述操作,因为 EJB bean 的作用域和 HTTP 基础架构均不可知。

另请参见

关于作者

顾问兼作家 Adam Bien 是 Java EE 6、EJB 3.1 和 JPA 2.0 JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并在几个大型项目中使用了 servlet/EJB 1.0,目前是 Java SE、Java EE 和 JavaFX 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是 《Real World Java EE Patterns—Rethinking Best Practices》一书的作者。Adam 还是 Java Champion 和 JavaOne 2009 Rock Star。