下载本文的 PDF 版本 PDF

解开企业 Java 的谜团

一种新型框架有助于消除横切关注点。

Chris Richardson,顾问

关注点分离是计算机科学中最古老的概念之一。这个术语是 Dijkstra 在 1974 年提出的。1 它很重要,因为它简化了软件,使其更易于开发和维护。关注点分离通常通过将应用程序分解为组件来实现。然而,存在横切关注点,它们跨越(或横切)多个组件。这些类型的关注点无法通过传统的模块化形式来处理,并且会使应用程序更加复杂且难以维护。

企业 Java 应用程序中横切关注点的示例包括事务管理、安全性、持久性和应用程序组装。然而,将处理这些关注点的代码分散在多个组件中是不可取的。这是因为这样做会使每个组件更加复杂。此外,在多个模块中复制代码可能会导致维护问题。因此,近年来涌现出大量活动,旨在开发框架和新型模块化形式,以试图从应用程序的业务逻辑中解开这些横切关注点。

在本文中,我们着眼于解决横切关注点的企业 Java 框架的演变。我们探讨了对第一代框架(基于 EJB(Enterprise JavaBeans)编程模型)的不满如何促使开发出明显更好的框架。这些新一代框架基于 POJO(Plain Old Java Object)编程模型。即使本文的重点是 Java,它也为使用其他语言编写框架和应用程序的开发人员提供了许多有益的经验教训。

对 Enterprise JavaBeans 的幻想破灭

EJB2 是用于编写基于组件的分布式业务应用程序的标准 Java 框架。它是一个用于构建业务逻辑组件(称为企业 Java Bean(或简称 EJB))的框架,并处理编写企业应用程序中最耗时的某些方面。EJB 提供诸如事务管理、授权、持久性和应用程序组装之类的服务。在本节中,我们将探讨哪些方面有效,哪些方面无效,以及这如何导致下一代企业 Java 框架的开发。

使用 EJB 框架分离关注点

在 EJB 可用之前,Java 开发人员负责自己编写事务管理、授权和持久性代码。除了编写起来容易出错且耗时之外,这些代码通常还与业务逻辑交织在一起。相比之下,通过使用 EJB 框架,使应用程序组件具有事务性、安全性和持久性,仅需使用 XML 配置文件(称为部署描述符)中的单独元数据声明这些特性即可。

如图 1 所示,EJB 框架(也称为 EJB 容器)读取部署描述符并实现所需的行为,通常是通过拦截对组件的调用并执行处理横切关注点的额外代码来实现的。例如,EJB 应用程序通常使用声明式事务,从而无需编写与每个组件的业务逻辑纠缠在一起的事务管理代码。EJB 框架拦截对 EJB 的调用,并开始、提交和回滚事务。

类似地,EJB 框架通过使声明式保护组件成为可能,简化了安全应用程序的开发。EJB 的部署描述符指定哪些用户可以访问组件或组件方法。EJB 框架拦截对 EJB 的调用,并验证调用者是否被授权访问该组件。

EJB 还支持持久性组件,称为实体 Bean。实体 Bean 的部署描述符描述了其属性如何映射到数据库模式。部署描述符将简单值映射到列,并将关系映射到外键和连接表。EJB 框架使用此映射生成 SQL 语句以查询和更新数据库。

EJB 在将横切关注点与业务逻辑分离方面部分成功。处理这些关注点的责任从业务逻辑组件转移到了 EJB 框架。然而,事实证明,该框架的前两个版本(EJB 1.0 和 EJB 2.0)以一种根本上存在缺陷的方式做到了这一点。

EJB 问题

早期 EJB 规范的主要缺陷(今天在企业 Java 社区中普遍存在)是它们对实现这些组件的类提出了苛刻的要求。EJB 1.0 和 2.0 组件必须实现 EJB 框架定义的接口,并且通常必须调用 EJB 框架 API。这会将组件紧密耦合到 EJB 框架,从而导致以下问题。

第一个问题是关注点分离是一种错觉。即使诸如安全性、事务管理和持久性之类的关注点与代码分离并在部署描述中配置,但在开发业务逻辑时,您也无法忽略它们。例如,没有数据库就无法轻松测试持久性 EJB 组件。在测试组件的业务逻辑时,您被迫考虑数据库模式设计。EJB 阻止您一次处理一个关注点。

业务逻辑与框架的紧密耦合还会导致令人恼火的长编辑-编译-调试周期。在 EJB 容器中部署 EJB 组件是一个耗时的操作,通常会打断您的思路。通常,重新部署组件的时间会超过 10 秒阈值,此时您可能会想做其他事情,例如浏览网页或通过 IM 与朋友聊天。当进行测试驱动开发时,对生产力的影响尤其令人沮丧,在测试驱动开发中,最好每隔一两分钟运行一次测试。测试驱动开发和单元测试是 Java 开发中常见的最佳实践,但由于开发 EJB 组件所需的基础设施而变得困难。

EJB 规范的这个缺陷更加糟糕,因为业务逻辑在框架版本之间不可移植。尽管 EJB 是一个标准,但在其短暂的历史中,它已经以不兼容的方式迅速发展。EJB 1.0 和 EJB 2.0 之间,以及 EJB 2.0 和 EJB 3.0 之间存在重大且不兼容的更改。要充分利用每个规范版本的新的和改进的功能,您必须重写组件。如果您负责维护生命周期超过几年的 EJB 应用程序,这可能会非常具有挑战性。

这些问题促使企业 Java 社区寻找更好的方法来解开横切关注点。许多创新来自开源 Java 框架(如 Spring 和 Hibernate)的开发人员,这些框架支持基于 POJO 的截然不同的编程模型。

使用 POJO 编程

如今,企业 Java 社区的共识是使用 POJO 构建业务逻辑组件。这种方法比旧式 EJB 方法更简单,但功能更强大。POJO 是一个 Java 对象,它不实现任何特殊接口(例如 EJB 框架定义的接口)或调用任何框架 API。这个名称是由 Martin Fowler 和其他人3 创造的,目的是给普通的 Java 对象起一个听起来令人兴奋的名字,并鼓励开发人员使用它们。这个简单的想法有一些令人惊讶的重要好处,包括显着更好的关注点分离。

非侵入式框架

当然,仅靠 POJO 是不够的。您仍然需要先前由 EJB 框架提供的服务,例如事务和安全性。解决方案是使用所谓的“非侵入式”框架,这些框架为 POJO 提供这些服务。此类框架的流行示例包括 Spring,4 它提供声明式事务管理;Hibernate5 和 JDO (Java Data Objects),6 它们提供持久性;以及 Acegi 安全性,7 它是 Spring 的扩展,为 POJO 提供身份验证和授权。此外,最新版本的 EJB 规范(EJB 3.0)也是基于 POJO 的。

与 EJB 2.0 一样,使用这些框架涉及编写描述组件应如何运行的元数据。但是,与 EJB 2.0 不同,这些框架提供事务管理、安全性、持久性和应用程序组装,而无需需要这些服务的应用程序类来实现框架接口或调用框架 API。它们实际上最多只对应用程序类施加最小的约束。这些框架通常仅由创建、查找和删除持久对象的少量应用程序组件直接调用。

如图 2 所示,您的应用程序由 POJO 组成,这些 POJO 独立于使其具有事务性、安全性或持久性的框架。

事务管理和安全框架拦截对 POJO 组件的调用,检查调用者是否已获得授权,并管理事务。持久性框架负责将持久性组件的状态存储在数据库中。

这些框架与 EJB 2.0 之间的另一个重要区别是 XML 元数据通常更用户友好且更简洁。此外,某些框架允许您以 Java 5 注释的形式而不是 XML 编写元数据。注释是一种 Java 语言构造,它提供有关程序元素的声明性信息。注释的普及程度不断提高,主要是因为它们比 XML 更简洁,因为它们嵌入在源代码中,紧邻它们描述的类、字段或方法。它们还避免了源代码和 XML 元数据之间脆弱链接的问题。

然而,注释可能是一把双刃剑。由于注释是源代码的一部分,因此您的应用程序最终可能会与框架紧密耦合。稍后,我将展示这些框架支持的元数据示例。

POJO 和非侵入式框架的优势

POJO 的概念非常简单,但使用它们可以显着改进开发。POJO 和非侵入式框架具有以下优势

需要记住的一件事是,POJO 和非侵入式框架背后的关键思想也适用于其他编程语言。以 Java 以外的语言编写的业务逻辑可以从独立于提供必要服务的基础架构框架中受益。例如,.NET 社区中的一些开发人员谈论 Plain Old .NET Objects (PONO),并使用 .NET 版本的 Hibernate,称为 NHibernate。Spring 框架也有 .NET 版本。

EJB 3.0 是朝着正确方向迈出的一步

开源开发人员对企业 Java 社区的近期创新负有主要责任。然而,EJB 标准并非一成不变。Sun 的规范设计人员倾听开发人员的意见,并相应地修改 EJB 规范。

新的 EJB 3.0 标准拥抱 POJO 编程模型。EJB 3.0 组件不再实现 EJB 接口,并且很少需要调用 EJB API。因此,EJB 组件与 EJB 3.0 框架的耦合度降低,并且开发比 EJB 2.0 容易得多。然而,Spring 和 Hibernate 等开源框架具有更丰富的功能集。

现在我们已经了解了基本的 POJO 编程概念,现在是时候了解一些细节了。本文的其余部分展示了非侵入式框架如何处理三个重要的关注点:事务管理、持久性和应用程序组装。

使用 POJO 进行事务管理

事务管理是企业应用程序中重要的横切关注点。事务对于确保存储在企业信息系统中的数据的完整性至关重要。经典的例子是将资金从一个银行帐户转移到另一个银行帐户。贷记和借记必须在事务中原子地执行,以防止资金消失。

每个业务逻辑组件都可以通过调用事务管理 API 来启动、提交或回滚事务来以编程方式管理事务。然而,通常最好通过使用声明式事务管理将事务管理关注点与业务逻辑分离。这种方法不易出错,大大简化了代码,并且更易于维护。

POJO 应用程序可以使用 Spring 框架或 EJB 3.0 进行事务管理。当使用这些框架之一时,您可以通过以 XML 或 Java 5 注释的形式编写元数据来配置事务行为。然后,框架将自动在事务中执行该方法。

考虑一下汇款示例。您可以使用如下所示的代码来实现此行为

public interface MoneyTransferService {
BankingTransaction transfer(String fromAccountId,
  String toAccountId, double amount);
}
public class MoneyTransferServiceImpl implements
  MoneyTransferService {
  }

MoneyTransferService 接口定义了一个 transfer() 方法,该方法由 MoneyTransferServiceImpl 类实现。transfer() 方法负责执行汇款,需要在事务中执行。在图 3 中,清单 1 显示了如何使用 Spring 框架使此方法具有事务性,清单 2 显示了如何使用 EJB 3.0 执行相同的操作。

Spring 版本使用 XML 元数据来指定 MoneyTransferService 定义的每个方法都在事务中执行。<aop:advisor> 元素告诉 Spring 如何拦截方法调用并处理横切关注点。此元素具有 pointcut 属性,该属性指定何时应运行事务管理逻辑,以及 advice-ref 属性,该属性指定在这些点上应执行的操作。advice-ref 属性引用由 <tx:advice> 元素定义的“txAdvice”。此元素配置事务管理,在此示例中,“*”通配符指定所有方法都应具有事务性。

在底层,Spring 框架使用通用的 AOP(面向方面编程)机制来实现事务管理。8 AOP 能够以模块化的方式实现影响应用程序许多部分的横切关注点,而无需将与该关注点相关的代码分散到其他模块中。在这种特定情况下,XML 元数据指定何时管理事务,但 Spring 应用程序通常使用 AOP 来处理各种其他横切关注点,包括安全性、日志记录和缓存。

EJB 3.0 示例使用 @Local@Stateless 注释使 MoneyTransferService 具有事务性。默认情况下,MoneyTransferService 定义的每个方法都将具有事务性。EJB 3.0 应用程序可以使用其他注释来指定各个方法的事务属性。

我忽略了细节,但需要注意的关键是,尽管这两个示例中的两组元数据非常不同,但它们有一个共同的特征:没有对任何事务管理 API 的调用。此示例中未显示,但您甚至可以编写元数据来指定哪些异常应导致事务回滚。事务完全由框架处理,框架读取和处理元数据。

持久化 POJO

数据库访问是另一个关键的横切关注点,因为企业 Java 应用程序几乎总是必须访问数据库。例如,汇款服务必须读取和更新帐户数据库表。业务逻辑可以通过简单地执行 SQL 语句来访问数据库。然而,这种方法可能既耗时又容易出错。编写可跨数据库移植的 SQL 也可能很困难。由于这些缺点,对于许多应用程序来说,更好的方法是通过使用透明持久性对象将持久性与业务逻辑分离。

当使用持久性对象时,开发人员编写元数据,指定对象模型如何映射到数据库。他们描述类如何映射到表,简单值如何映射到列,以及对象之间的关系如何映射到外键或连接表。持久性框架(又名对象/关系映射框架)使用映射元数据生成 SQL 语句,以加载和存储持久性对象。

如果使用得当,这种方法可以显着减少需要编写的数据库访问代码量。应用程序操作对象,所有数据库访问都在后台由持久性框架完成。持久性框架跟踪应用程序对对象所做的更改,并将任何修改后的对象自动写回数据库。

例如,银行帐户可以用以下类表示

public class Account {
 private double balance;
 private String accountId;
 public Account(String accountId,
  double initialBalance) {
  this.accountId = accountId;
  this.balance = initialBalance;
  }
 public double getBalance() { return balance; }
 public String getAccountId() { return accountId; }
 public void debit(double amount) { balance -= amount; }
 public void credit(double amount) { balance += amount; }
  }

此类具有余额和帐户 ID 字段;以及各种方法,包括 debit()(用于从帐户中扣款)、credit()(用于向帐户中存款)和 getBalance()(用于返回当前余额)。

持久化帐户类的一种方法是将该类映射到 ACCOUNT 表,并将每个字段映射到该表的列。在图 4 中,清单 1 显示了如何使用 EJB 3.0 完成此操作,清单 2 显示了如何使用 Hibernate 3 持久化该类。

EJB 3.0 示例使用注释来定义类及其字段如何映射到 ACCOUNT 表,以及 Hibernate 3 如何使用 XML。这两个框架都使用此元数据生成 SQL,以加载、存储和删除此类的实例。Account 类不知道它是持久性的。

应用程序中唯一知道持久性框架的部分是那些调用持久性框架来创建、查找和删除持久性对象的组件。幸运的是,这些组件通常仅占应用程序的一小部分。此外,它们使用接口封装。例如,银行业务示例有一个 AccountDAO,它定义了用于创建和查找帐户的方法。这是它的接口

public interface AccountDAO {
  Account findAccount(String accountId);
 Account createAccount(String accountId, 
  double initialBalance);
  }

此接口隐藏了持久性框架,从而将使用 AccountDAO 的组件与持久性框架解耦。

组装应用程序

应用程序组装是另一个横切关注点。实际应用程序几乎总是由多个相互依赖的组件组成。在运行时,组件必须能够获取对其他组件的引用。例如,在银行业务应用程序中,需要在帐户之间转账的组件将需要引用前面提到的 MoneyTransferService。一种常见的方法是使用 Service Locator 模式,9 该模式通常由基础架构框架提供。例如,企业 Java 应用程序可以使用 JNDI(Java 命名和目录接口),10 它使组件能够按名称查找对另一个组件的接口。Service Locator 模式促进了组件之间的松耦合,但它的缺点是使每个组件都负责查找自己的依赖项。这也将每个组件耦合到基础架构框架。

对于许多应用程序来说,更好的方法是使用依赖注入,11 它将依赖项的查找与应用程序组件分离。使用这种方法,组件的依赖项使用元数据以声明方式指定。在实例化组件时,框架会查找组件的依赖项(如果需要,则递归实例化它们),并将它们作为构造函数参数或 setter 方法参数传递给组件。这消除了应用程序组件中的任何依赖项查找代码。

Spring 框架使用 XML 元数据来指定如何将依赖项注入到组件中。类似地,当使用 EJB 3.0 时,您可以注释 EJB 的字段或 setter 以指定要传入的依赖项。例如,这是一个访问 MoneyTransferService 的 EJB 3.0 服务

class SomeServiceImpl {
  @EJB 
  private MoneyTransferService transferService;
  }

@EJB 注释告诉 EJB 容器使用对 MoneyTransferService 的引用来初始化 transferService 字段。

使用依赖注入有两个主要好处。它通过消除查找逻辑来简化应用程序组件。组件只是使用传递给它的依赖项,而无需知道它们是如何获得的。此外,由于组件不再调用 Service Locator,因此依赖注入有助于将组件与基础架构框架解耦。依赖注入是 POJO 编程模型的关键使能机制。

总结

企业 Java 社区已经开发了各种框架,用于将业务逻辑与横切关注点(例如事务管理、安全性、持久性和应用程序组装)分离。早期的尝试仅部分成功。EJB 2.0 标准过于复杂,未能提供真正的关注点分离。然而,今天,企业 Java 组件技术已得到显着改进。EJB 2.0 标准已被更简单但更强大的框架所取代,这些框架支持 POJO 编程模型。

POJO 是不实现特殊 API 或调用基础架构框架的对象。横切关注点由非侵入式框架(如 Spring、Hibernate 和 EJB 3.0)处理。这些框架提供服务,而无需 POJO 实现特定接口或调用框架。例如,POJO 可以是事务性的,而无需调用事务管理 API,并且 POJO 可以是持久性的,而无需调用持久性框架 API。

将 POJO 与非侵入式框架结合使用具有许多重要的好处。开发更容易,因为您可以一次专注于一个关注点。开发速度更快,因为您可以在没有基础架构框架或数据库的情况下测试组件。使用 POJO 还使您的应用程序能够利用新的和改进的企业 Java 框架。您可以通过解开横切关注点来真正改进您的应用程序。问

致谢

我要感谢匿名审稿人以及以下个人对本文草稿提出的有益意见:Azad Bolour、Jonas Bonér、Adrian Colyer 和 David Vydra。

参考文献

  1. Dijkstra, E.W. 1982. 论科学思想的作用。载于《计算机科学精选文集:个人视角》,60-66 页。施普林格出版社。
  2. JSR 153:Enterprise JavaBeans 2.1; http://www.jcp.org/en/jsr/detail?id=153; JSR 220:Enterprise JavaBeans 3.0; http://www.jcp.org/en/jsr/detail?id=220
  3. Fowler, M. https://martinfowler.com.cn/bliki/POJO.html
  4. Spring 框架; http://www.springframework.org
  5. Hibernate 对象/关系映射框架; http://www.hibernate.org/
  6. JSR 243:Java 数据对象 2.0—JDO 规范的扩展。
  7. Spring 的 Acegi 安全系统; http://acegisecurity.sourceforge.net
  8. Laddad, R. 2003. AspectJ in Action. 格林威治,CT:曼宁。
  9. Fowler, M. https://martinfowler.com.cn/articles/injection.html
  10. 10. JNDI; http://java.sun.com/products/jndi/
  11. 11. 请参阅参考文献 9。

Chris Richardson ([email protected]) 是一位拥有 20 多年经验的开发人员和架构师。他的咨询公司专门帮助企业 Java 开发人员提高生产力和取得成功。他曾在 Insignia、BEA 等公司担任技术主管。他拥有英国剑桥大学计算机科学学位,居住在加利福尼亚州奥克兰市。网站和博客: www.chrisrichardson.net

acmqueue

最初发表于 Queue 第 4 卷,第 5 期
数字图书馆 中评论本文





更多相关文章

Satnam Singh - 使用容器对容器进行集群级日志记录
本文展示了如何使用开源工具实现集群级日志记录基础设施,并使用与用于组合和管理正在记录的软件系统相同的抽象来部署。收集和分析日志信息是运行生产系统以确保其可靠性并提供重要的审计信息的重要方面。已经开发了许多工具来帮助聚合和收集在特定服务器(例如 Fluentd 和 Logstash)上运行的特定软件组件(例如 Apache Web 服务器)的日志。


Peter Kriens - OSGi 如何改变了我的生活
在 20 世纪 80 年代初,我发现了 OOP(面向对象编程),并全心全意地爱上了它。像往常一样,这种爱意味着说服管理层投资这项新技术,最重要的是,送我去参加酷炫的会议。所以我向我的经理推销了这项技术。我向他描绘了美好的未来,有一天我们将从现成的类创建应用程序。我们将从存储库中获取这些类,将它们放在一起,瞧,一个新的应用程序就诞生了。今天,我们或多或少地认为对象是理所当然的,但如果我说实话,我在 1985 年向我的经理推销的产品从未真正实现。


Len Takeuchi - ASP:集成挑战
使用 ASP 和向 ASP 提供增值产品的第三方供应商的组织需要与它们集成。ASP 通过提供基于 Web 服务的 API 来实现这种集成。通过 Internet 与 ASP 集成和与本地应用程序集成之间存在显着差异。在与 ASP 集成时,用户必须考虑许多问题,包括延迟、不可用性、升级、性能、负载限制和缺乏事务支持。


Michi Henning - CORBA 的兴衰
根据确切的开始计数时间,CORBA 大约有 10-15 年的历史。在其生命周期中,CORBA 从早期采用者的前沿技术,发展成为流行的中间件,再到成为相对默默无闻的利基技术。值得研究的是,为什么 CORBA(尽管曾经被誉为“电子商务的下一代技术”)遭受了这种命运。CORBA 的历史是计算行业多次见证的历史,而且似乎当前的中间件努力,特别是 Web 服务,将重演类似的历史。





© 保留所有权利。

© . All rights reserved.