技术:Java EE 6

发表于
Oracle Magazine
2011 年 1/2 月刊

  

简单源于设计

作者:Adam Bien

 

利用新的 Java EE 6 特性构建简单、易于维护的应用程序。

2006 年推出的 Java Platform Enterprise Edition (Java EE) 5 显著简化了企业应用程序开发。2009 年发布的 Java EE 6 进一步简化了设计和架构任务。Java EE 6 非常适于快速构建小范围应用程序,不会产生任何开销。本文讨论可帮助开发人员构建高效、简单、易于维护的应用程序的各种 Java EE 6 架构和设计方法。

Java EE 6 包括一组随 Java EE 一同发布的独立 API。虽然这些 API 是独立的,但它们可以很好地进行协作。对于给定的应用程序,您可以只使用 JavaServer Faces (JSF) 2.0,您可以使用 Enterprise JavaBeans (EJB) 3.1 实现事务服务,也可以结合使用上下文和依赖注入 (CDI)、Java Persistence API (JPA) 2.0 及 Bean Validation 模型来实现事务。

从实用的角度混合一组可用的 Java EE 6 API,您可以完全消除实现某些基础架构服务的需求,例如,应用程序中的事务、线程、限制或监视。真正的挑战在于选择正确的 API 子集,以确保最大限度地降低开销和复杂性,同时避免重新编写自定义代码。一般来说,应尽量使用现有的 Java SE 和 Java EE 服务,然后再扩展搜索范围查找备选方案。

CDI:标准粘合剂

Java EE 6 中引入的 CDI 充当 Java EE 6 规范不同部分间的粘合剂,可管理传统 Java 对象 (POJO) bean 的生命周期并使用一种类型安全的机制实现依赖注入。CDI 还引入了许多强大的特性,如事件、拦截器、修饰器、标准化扩展点和服务提供程序接口。

由于新推出的 CDI 设计为集成层,因此与以往的技术之间仍存在一些共同之处。虽然您可以继续直接使用 EJB 3.1 注入或 JSF 托管 bean,但在可能的情况下应考虑使用 CDI。CDI 功能更强,并且您可以使用一个 API 来简化应用程序。

CDI 使用批注执行依赖注入。最重要的批注是 javax.inject.Inject。清单 1 中的示例显示了如何使用该批注将一个 POJO 注入到 servlet 中。您只需声明一个字段并为其添加 @Inject 批注。执行该代码时,容器会在执行任何业务方法之前自动初始化添加了 @Inject 批注的字段。

代码清单 1: 使用 @Inject 将 POJO 注入到 servlet 中

@WebServlet(name="HelloWorldService", urlPatterns={"/HelloWorldService"})
public class HelloWorldHTTPService extends HttpServlet {
   
    @Inject
    private Hello hello;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        out.println(hello.helloWorld());
        out.flush();
        out.close();
    } 
}

 

除了必须包含一个默认构造函数外,对所注入的类没有任何其他特殊要求:

public class Hello {
    public String helloWorld(){
        return "Hello World";
    }
}

 

要让以上示例正常运行,您还需要一个空的 beans.xml 部署描述文件,其内容如下:<beans></beans>。WEB-INF 文件夹中这个配置文件的存在可以激活 CDI 功能。

请注意,Hello 类是一个 POJO,而不是 EJB。它无需进行声明或配置 — @Inject 批注将确保正确的创建和生命周期管理。在实际情况中,您很少会将 POJO 注入到 servlet 中;您可能会使用 UI 框架(如 JSF 2),或者会通过表示状态传输 (REST) 来公开服务。在这种情况下,使用 CDI 可以带来更大的好处。

为便于演示,请考虑一个在数据库中存储消息字符串的简单 MessageMe 应用程序。JSF 2 标记由两部分组成:inputText 和 commandButton。如清单 2 所示,inputText 的值绑定到 index 类的 message 特性的 content 属性上。commandButtons 的 action 属性绑定到一个名为 index 的辅助 bean 的 save 方法上。

代码清单 2: index.xhtml:将值绑定至 CDI 辅助 bean

    <h:body>
        <h:form>
            Content:<h:inputText value="#{index.message.content}"/>
            <br/>
            <h:commandButton value="Save" action="#{index.save}"/>
        </h:form>
    </h:body>

 

清单 3 显示了作为请求范围的 CDI bean 实现的辅助 bean,它使用 @RequestScoped 批注处理请求。JSF 2 托管 bean(使用 @ManagedBean 批注)也可实现此任务,但 CDI 功能更强。使用 CDI 可以简化架构,所有应用层只需一个粘合 API。

代码清单 3: 包含注入 EJB 的 CDI 辅助 bean

package com.abien.messageme.presentation;
import com.abien.messageme.business.messaging.boundary.Messaging;
import com.abien.messageme.business.messaging.entity.Message;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
@Named
@RequestScoped
public class Index {
    @Inject
    Messaging ms;
    private Message message = new Message();
    public Message getMessage() {
        return message;
    }
    public void save(){
        ms.store(message);
    }
}

 

@Named 批注(在 JSR 330 规范中指定,在 Guice 和 Spring 中实现)令 index 辅助 bean 在所有表达式语言 (EL) 标记中均为可见。它依照“惯例优于配置”的原则工作:JSF 2 中的辅助 bean 的名称是从类名派生的。第一个字母未采用大写。

Message 类作为 JPA 2 实体实现,如清单 4 所示。

代码清单 4: 通过 Bean Validation 验证的 JPA 2 实体

package com.abien.messageme.business.messaging.entity;
@Entity
public class Message {
    @Id
    @GeneratedValue
    private Long id;
    @Size(min=2,max=140)
    private String content;

    public Message(String content) {
        this.content = content;
    }
    public Message() { /*required by JPA */}
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Long getId() {
        return id;
    }
}

 

本示例中的下一个类是 Messaging 类,它作为一个 EJB 3.1 会话 bean 实现。该类代表了“CDI 无处不在”规则在实际应用中的一个例外。EJB 提供了许多功能,如事务、池化、Java Management Extensions (JMX) 监视和异步执行 — 只需添加一个 @Stateless 批注即可实现这一切。在未来的 Java EE 版本中,这些功能可能会从 EJB 中抽象出来在 CDI 中提供。但在 Java EE 6 中,业务组件的边界或外观以无状态会话 bean 来实现最为有效。

清单 5 中的 @Asynchronous 批注特别有趣。它支持方法的异步事务执行,仅适用于 EJB。请注意,Messaging EJB 通过 @Inject 注入,而非 @EJB。实际上,这两种批注都行得通,几乎没有任何区别。使用 @Inject 功能会稍微强一些,并且支持继承。另一方面,@EJB 批注则仅适用于 EJB。

代码清单 5: 作为 EJB 会话 bean 实现的边界

package com.abien.messageme.business.messaging.boundary;
import com.abien.messageme.business.messaging.control.MessageStore;
import com.abien.messageme.business.messaging.entity.Message;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.inject.Inject;
@Stateless
public class Messaging {
    @Inject
    MessageStore messageStore;
    @Asynchronous
    public void store(Message message){
        messageStore.store(message);
    }
}

 

清单 6 中的 MessageStore 类是一个封装了对 EntityManager 访问的数据访问对象 (DAO)。

代码清单 6: 控制层的 CDI bean

package com.abien.messageme.business.messaging.control;
import com.abien.messageme.business.messaging.entity.Message;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
public class MessageStore {
    @PersistenceContext
    EntityManager em;
    public void store(Message message){
        em.persist(message);
    }
}

  

ECB:有效的分而治之

如果回头看看上述的应用程序打包,您会注意到三个独立的程序包:边界、控件和实体。这种打包方法是实体-控制-边界 (Entity Control Boundary - ECB) 模式的一种实现。边界层是外观,控制层负责实现独立于流程和实体的逻辑,实体层则包含丰富的域对象。

借助 Java EE 6,尤其是可用的 JPA 2、CDI 和 EJB,实现所有这三层会产生空的委托代码。例如,许多基于 CRUD 的用例都可通过充当访问多个实体的外观的单个边界得以高效实现。

但 ECB 模式中的概念与组件内程序包之间的直接一对一关系仍然非常有帮助。如果保持这些程序包相互独立,那么就可以更轻松地使用静态分析工具来测量程序包之间的依赖关系。此外,OSGi 和 Jigsaw 等框架还依赖独立的程序包来公开公共 API。

在 Java EE 6 中,边界始终通过 EJB 实现。控制层可以包含 CDI 或 EJB,实体层可以包含 JPA 2 实体或临时的非托管实体。不必提前最终决定在控制层中使用 CDI 还是 EJB。您可以从 CDI 开始,一段时间之后再使用 @Stateless 批注将其转换为 EJB。某些情况下,您可能需要使用 EJB,例如,当您需要通过 @RequiresNew 启动一个后续事务时,当您需要异步执行方法时,或者当您需要通过调用 SessionContext.setRollbackOnly() 回滚当前事务时。

另一方面,CDI 更适于集成原有代码或者实现 Strategy、Factory 或 Observer 软件设计模式。所有这些功能都已经内置在 CDI 中,并且代码要比使用 Java SE 对应项时少很多。

使用 ECB 模式开发应用程序时,ECB 分层应该迭代推进,不应强制采用自上而下的方式。您应该从持久性(实体)层开始,执行单元测试,然后实现边界层。构建单元测试时,需要手动创建和管理 EntityManager 及相关事务(如清单 7 所示)。

代码清单 7: 独立 JPA 单元测试

package com.abien.messageme.business.messaging.entity;
import javax.persistence.*;
import org.junit.Test;

    @Test
    public void mappingSmokeTest() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.persist(new Message("duke"));
        tx.commit();
    }

 

还必须调整 persistence.xml 文件以处理独立执行。具体来说,事务类型应更改为 RESOURCE_LOCAL,并且必须显式配置一个 JDBC 连接(而非数据源),如清单 8 所示。

代码清单 8: 用于独立 JPA 单元测试的 persistence.xml

<persistence>
  <persistence-unit name="test" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>com.abien.messageme.business.messaging.entity.Message</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:derby:
./sample;create=true"/>
      <property name="javax.persistence.jdbc.password" value="app"/>
      <property name="javax.persistence.jdbc.driver" value="org.apache.derby
.jdbc.EmbeddedDriver"/>
      <property name="javax.persistence.jdbc.user" value="app"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</Persistence>

 

构建控制层时,请注意其内容将是实体层和边界层重构的结果。边界层的可重用、关联度不高的部分(如查询、算法或验证)以及实体层的横切关注点将被提取到控制层的 CDI 托管 bean 中。

使用 CEC 模式

ECB 模式中边界的主要作用是明确分离业务逻辑和表示逻辑。根据定义,边界需要独立于表示逻辑。即便会对架构造成许多影响,必须将业务和 UI 技术明确分开。实际上,UI 逻辑往往比业务逻辑变化得更加频繁。通常会生成可供 Web 客户端(如 JSF 2)、富客户端(如 Swing 或 Eclipse RCP)和 REST 同时访问的业务逻辑。

对于 JSF 2,CDI 再次成为实现控制器或表示器的最简单选择。CDI 托管 bean 可通过 EL 直接绑定到 JSF 2 视图,边界 (EJB 3.1) 可直接注入到表示器中。可使用 @Stereotype 批注直接捕获表示器(或控制器)。其工作方式类似于宏 — 您可以将 CDI 批注置于其中,然后在批注的时候会被展开。Stereotype 是一个常规 Java 批注,用 @Stereotype 表示:

@Named
@RequestScoped
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Presenter {}

 

可以用这个自定义 stereotype 取代 @Named 和 @RequestScoped — 类似于宏。随后,所有识别表示器模式的 CDI 批注均可替换为

@Presenter
public class Index {
//
}

 

表示器的目的是实现表示逻辑。表示器的结构与视图之间存在紧密联系,这是因为视图中 JSF 组件的状态将映射为表示器中的特性。特性可以是值(使用值绑定)或组件实例本身(使用组件绑定)。在简单情况下,视图和表示器之间存在一对一的关系。表示器包含视图的数据以及所有表示逻辑。将边界注入到表示器中需要使用 @Inject 批注。

随着表示器内部的表示逻辑不断增加,对代码的维护和测试会变得更加困难。借助 CDI,可以非常轻松地将单体表示器拆分为独立数据和表示逻辑部分。例如,以下代码显示了如何通过将 save 方法移入新建的 IndexPresenter bean 中来重构之前示例中的辅助 bean。复制表示器批注并将其重命名为 @View,并将该 bean 重命名为 IndexView:

@View
public class IndexView {
    private Message message = new Message();
    public Message getMessage() {
        return message;
    }
}

 

IndexPresenter bean 获取原来的 @Presenter 批注。如以下代码所示,IndexPresenter bean 在本示例中的唯一作用是实现表示逻辑。

@Presenter
public class IndexPresenter {
    @Inject
    Messaging boundary;
    @Inject
    IndexView indexView;
    public void save(){
        boundary.store(indexView
.getMessage());
    }
}

 

由于边界和视图已注入到 IndexPresenter 中,因此可以轻松模拟它们。在单元测试环境中,可直接使用模拟来设置这两个字段,而在生产环境中,容器将执行注入并设置实际依赖。由于单元测试和 IndexPresenter 位于同一个程序包中,因此可以直接设置默认可见字段。可以使用包含公共 setter 的私有字段,但程序包级字段足以胜任大多数情况并且可以减小代码规模。

清单 9 显示了如何通过模拟 IndexView 以及边界 Messaging 类来测试表示逻辑。如果仅调用 store 方法一次并且 IndexView 返回 Message-Instance,则调用 IndexPresenter.save() 方法的测试成功。验证调用意味着将模拟传递给 Mockito.verify() 方法。通过模拟 IndexView 来操作返回值,而不必与 JSF 呈现进行交互。

代码清单 9: IndexPresenterTest — 使用模拟视图和边界

package com.abien.messageme.presentation;
//...other imports
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.*;

public class IndexPresenterTest {
    private IndexPresenter cut;
    @Before
    public void initialize(){
        this.cut = new IndexPresenter();
    }
    @Test
    public void save() {
        this.cut.boundary = mock(Messaging.class);
        this.cut.indexView = mock(IndexView.class);
        Message expected = new Message("duke");
        when(this.cut.indexView.getMessage()).thenReturn(expected);
        cut.save();
        verify(this.cut.boundary,times(1)).store(expected);
    }
}

 

模拟 Messaging 边界的原因有所不同,是为了验证是否实际调用了预期的方法:

public void save(){
    boundary.store(indexView
.getMessage());
    }

 

JSF 2 表示的设计类似于富 Swing 应用程序的设计。模型-视图-控制器及其改进版(监督控制器和被动视图)等常用模式也适用于 JSF 2。JSF 和富客户端技术之间的主要区别在于呈现视图的方式上。在 Swing 中,开发人员使用 Java 实现视图,而在 JSF 2 中,开发人员使用 XHTML 标记。在 JSF 2 中,组件的值可以直接绑定到对应的类,而在 Swing 中,它们通常存储在视图或模型中。

对于 CRUD 这样的数据驱动用例的实现,监督控制器是比被动视图更好的选择。在监督控制器模式中,表示逻辑和视图状态都由一个辅助 bean (IndexView) 负责管理。在更为复杂的用例中,被动视图的变体可能更加适用。在被动视图模式中,辅助 bean 拆分为视图和表示逻辑,表示逻辑从 IndexView 提取到 IndexPresenter 中。

CDI 最适于实现表示层。由于内置的横切关注点(如事务、并发性、异步执行、监视和限制),业务逻辑的边界将作为 EJB 实现。业务组件可以作为 EJB 或 CDI 实现。通常,您可以从 CDI 开始,一段时间后,在特殊情况下将托管 bean 替换为 EJB。对 Java EE 6 而言,CDI-EJB-CDI (CEC) 模式是最简单、最实用的选择。

让接口发挥效用

EJB 3.0(在 Java EE 5 中)需要针对 bean 类的单独接口。为了避免命名冲突,开发人员经常不得不采用 XyzLocal/XyzRemote 和 XyzBean 这些定义明确的命令约定。在 Java EE 6 中,EJB 和 CDI 的接口现在是可选的。公共 EJB 或 CDI 方法现在可以公开一个“非接口”视图,不会损失任何功能。

这种新功能令接口再次变得意义重大。与早期版本的强制不明确使用接口相比,Java EE 6 中的接口可用于实现策略模式、实现公共 API 或者严格分离模块,从而使得代码更具表现力。接口还可以表示系统的“受保护的变化”,类之间的直接依赖可用于那些很少发生变化的代码。

后续步骤


 详细了解
CDI
受保护的变化模式
监督控制器和被动视图模式
 

您一开始不使用任何接口也可以很安全,随后可以根据需要引入接口。这种方法与 Java EE 5 中的方法完全不同。与 2003 年推出的 Java 2 Platform Enterprise Edition (J2EE) 相比,Java EE 6 代码更简单,因为它消除了一些层、间接手段和抽象。与 J2EE 不同,Java EE 6 由添加了批注的类构成,丝毫不依赖于平台。使用此方法,无需将业务逻辑与基础架构相分离,并且大多数 J2EE 模式和最佳实践都没必要使用。在 Java EE 6 中,可以使用两个层来解决简单用例:表示层和业务逻辑层。EntityManager 已经是一个足够好的底层持久性抽象,因此不需要其他间接手段。

应根据 YAGNI(适可而止的设计)、DRY(不重复原则)和 KISS(最简法则)这些原则编写易于维护的 Java EE 6 应用程序。应采用自下而上(而非自上而下)的方式引入设计模式和最佳实践。模式始终受功能和非功能需求驱动,而不是受平台的不足驱动。这种方法是 Java EE 6 和之前的 J2EE 版本之间的最大区别。在 J2EE 中,许多设计决策都是根据 J2EE 平台依赖性提前制定的。

相反,Java EE 6 开发流程则侧重于功能:

  1. 编写可直接解决业务问题的简单代码。
  2. 通过单元测试验证业务逻辑。
  3. 通过重构减少冗余并改善设计。
  4. 对应用程序执行压力测试。
  5. 返回第 1 步。

设计和架构由具体需求驱动,而不是由通用的架构最佳实践驱动。通过对应用程序持续执行压力测试(至少每周一次),您可以更轻松地通过确切事实来证明简单设计的效用,从而洞察处于压力下的系统行为。

  



Adam Bien
(blog.adam-bien.com) 是一位 Java Champion、顾问、讲师、演讲家、软件架构师、开发人员,还撰写了一些 Java 图书和文章,包括《Real World Java EE Patterns:Rethinking Best Practices》(lulu.com,2009 年)。他被 Oracle Magazine 评选为 2010 年年度最佳 Java 开发人员。

 

 

将您的意见发送给我们