保持领先
VMware 提供培训和认证,助您快速提升。
了解更多昨晚,我参加了新英格兰 Java 用户组 (NEJUG) 的一个会议,Reza Rahman 在会上展示了 EJB 3 和 Spring 的“对比分析”(EJB 3 and Spring)。Reza 是《EJB 3 实战》(EJB 3 in Action) 一书的作者之一。我很高兴认识 Reza,并非常尊重他敢于呈现这个可能被认为有争议的话题。我也很欣赏他确实尝试阐述 EJB 3 和 Spring 的优缺点。然而,我感到有必要澄清他在介绍 Spring 时的一些不够准确的观点,这些观点让我(和其他与会者)认为这次展示带有偏向 EJB 3 的动机。公平地说,与固定的规范版本不同,Spring 在不断发展,我这里要指出的某些内容是新功能。另一方面,有些是 Spring 2.0 的功能,已经存在一年多了。我个人认为,“对比分析”必须考虑所比较产品最新稳定版本的最新功能集。我想不言而喻,我也可能带有一点偏见,但我这里的动机是提供一个完全客观的回应,以便这次展示可以或许被修订,以反映更“同类比较”的分析。我将对展示中的 10 个“主题”提供简要回应。
有人提到 Spring 开始支持更多注解,但这需要“一段时间”。然而,Spring 2.0 版本提供了完整的 JPA 集成,使用 @PersistenceContext 注入 EntityManager,以及使用 Spring 的 @Transactional 注解进行注解驱动的事务管理(支持与带有默认 REQUIRED 传播行为的 @Stateless EJB 相同的语义)。我尤其失望的是,这次比较没有在双方都包含 JPA(见下面的第 3 点)。Spring 2.0 还引入了完整的基于注解的 AspectJ 支持 (@Aspect, @Before, @After, @Around) 和“stereotype”注解的概念。例如,@Repository 注解使得直接使用 JPA 或 Hibernate API 的数据访问代码能够实现非侵入性的异常转换(无需使用 Spring 的模板)。Spring 甚至早在 1.2 版本就提供了注解支持,例如 @ManagedResource,用于透明地将任何 Spring 管理的对象导出为 JMX MBean。
现在,这个问题对我来说之所以排在第一位,是因为那个“需要一段时间”的评论。作为 Spring 2.5 注解驱动配置支持的主要开发者之一,我必须说 Spring 的元数据模型极其灵活,因此我们能够比预期更快地提供全面的基于注解的模型。事实上,Spring 2.5 支持 JSR-250 注解:@Resource, @PostConstruct, 和 @PreDestroy - 以及 @WebServiceRef 和 @EJB。特别值得关注的是 @Resource,因为它是 EJB 3 中用于依赖注入的主要注解。在 Spring 中,@Resource 注解不仅支持 JNDI 查找(与 EJB 3 一样),还支持注入任何 Spring 管理的对象。这有效地结合了本次展示中提到的 Spring 的主要优势(Spring 支持任何类型对象的依赖注入)和 EJB 3 的主要优势(使用注解代替 XML)。Spring 2.5 还引入了基于 @Autowired 和(可扩展的)@Qualifier 注解的更细粒度的注解驱动依赖注入模型。Spring 2.5 还将“stereotype”注解扩展到包括 @Service 和 @Controller。每个 stereotype 注解都通过将其作为元注解应用来扩展泛型的 @Component 注解。通过应用相同的技术,@Component 注解为用户定义的 stereotype 提供了一个扩展点。Spring 甚至可以自动检测这些带注解的组件,作为 XML 配置的替代方案。例如,下面这个摘录来自 PetClinic 示例应用的 2.5 版本
<context:component-scan base-package="org.springframework.samples.petclinic.web" />
对于 web 控制器来说,无需额外的 XML,因为它们使用注解驱动的依赖注入和注解进行请求映射。我指出这一点是因为这次展示特别强调了 web 层的配置冗余。
@Controller
public class ClinicController {
private final Clinic clinic;
@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
...
有关 Spring 注解支持的最新信息,请参阅:The Server Side 上的 Spring 2.5 介绍,或 Spring 参考手册的最新版本 - 特别是基于注解的配置部分。此外,请继续关注本博客和Spring Framework 主页,了解即将发布的关于 2.5 版本的一些文章和博客。
这一点实际上被呈现为 Spring 的一个优点,但着重强调了配置开销。事实是,任何认真对待测试和敏捷开发的项目都需要支持“多种部署环境”。换句话说,这个特定主题经常被扭曲,仿佛它只适用于多种生产环境。实际上,在每个开发和测试周期都需要部署到应用服务器是敏捷性的一个主要障碍。通常,Spring 用户会对其配置进行模块化,使得“基础设施”配置(例如 DataSource、TransactionManager、JMS ConnectionFactory)是分开的,而动态属性是外部化的。由于 Spring 支持根据外部化属性替换 '${placeholders}',因此包含不同的属性文件通常会成为一个透明的问题。
我必须承认,这一点最让我感到困扰。在对比幻灯片中,EJB 3 示例展示了通过 entityManager 进行数据访问的 JPA,并且 entityManager 实例通过 @PersistenceContext 注解提供。另一方面,Spring 示例使用了 Hibernate,并展示了 Hibernate SessionFactory 的 setter 注入。在我看来,这违反了真正“对比分析”的第一条规则:在比较的双方使用最相似的功能。在这个特定的例子中,Spring 确实支持直接使用 JPA API(即 JpaTemplate 是完全可选的;直接使用 'entityManager' 仍然参与 Spring 事务等),并且 Spring 也识别 @PersistenceContext 注解。这个支持自 Spring 2.0 版本(最终版本已发布一年多)以来就已经可用,所以我不知道为什么这次比较没有在 Spring 端也使用 JPA。这次比较的其他部分显然是基于 Spring 2.0 的,所以这给人留下了选择性过时并透露偏见的印象。如果这个特定的例子被修改为“同类比较”,它就会削弱总体主要主题之一:即 Spring 需要更多配置,而 EJB 3 依赖于标准注解。
现在,尽管我认为在 Spring 端使用 Hibernate 而非 JPA 扭曲了比较,但这同时也揭示了 Spring 的一个优势。如果您确实想直接使用 Hibernate API 而不是依赖 JPA API,Spring 也能做到这一点,并且在 Spring 事务管理和异常转换方面以一致的方式进行。这也就带来了使用超出 JPA 限制的 Hibernate 功能的机会,例如 Hibernate 的“Criteria”查询 API。同样,如果您想在 ORM 过度的情况下添加一些直接 JDBC 进行数据访问,Spring 也支持这一点 - 即使在与 Hibernate 或 JPA 数据访问相同的事务中调用。
一个具体的例子是事务管理器的定义。有人说您必须理解容器供应商级别的东西才能配置 Spring 集成。这是不正确的。例如,下面的 bean 定义不包含任何容器特定的信息,但 Spring 会在所有 Java EE 应用服务器中自动检测事务管理器
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
如果您确实想利用容器特定的功能,例如按事务隔离级别,那么 Spring 也提供了几个专用的实现:WebLogicJtaTransactionManager、WebSphereUowTransactionManager 和 OC4JJtaTransactionManager。在这些实现之间切换只需更改这个单一的定义即可。
此外,Spring 的配置幻灯片 unnecessarily verbose。我担心这可能也是为了强调 EJB 不同于 Spring 依赖智能默认设置而动。例如,幻灯片显示了
<tx:annotation-driven transaction-manager="transactionManager"/>
实际上,如果在一个 Spring 上下文中只定义了一个 'transactionManager',那么 'annotation-driven' 元素上就不需要显式提供该属性。该属性仅用于在同一个应用程序中必要时启用多个事务管理器的使用。这些“自动检测”和“智能默认”的技术在 Spring 中随处可见,例如消息监听器的 JMS 'connectionFactory'(在下面 #6 的示例中是隐式的)以及自动定位现有 MBean 服务器或 RMI 注册表。
积极的一点是,Spring 允许“本地”事务管理被实际提及为一个优势。虽然 EJB 要求 JTA 进行事务管理,但许多应用程序不需要跨两阶段提交资源的分布式事务。在这种情况下,Spring 允许使用更简单、开销更小的事务管理器:DataSourceTransactionManager(用于 JDBC)、HibernateTransactionManager 或 JpaTransactionManager。如果目标是准确描述优缺点,我会期望听到关于这个特定 Spring 优势的更多细节。例如,这对于在容器外部进行测试或在 Eclipse 或 IDEA 等轻量级 IDE 环境中进行开发来说是一个巨大的好处。
此外,如果您确实需要 JTA 进行分布式事务,但想在像 Tomcat 或 Jetty 这样的轻量级容器中运行,Spring 很容易支持像 Atomikos 和 JOTM 这样的独立 JTA 提供程序。当然,Spring 的事务管理器设置需要配置一个 bean 定义,但这确实是一次性的成本——而且非常值得回报。
无状态服务层的优势已作为最佳实践被广泛认可,Spring 也拥抱这一理念。然而,Spring 也提供了除单例之外的其他作用域。Spring 的“prototype”作用域为每次注入或查找提供了一个独立的实例,并且 Spring 2.0 引入了 web 作用域:“request”和“session”。作用域机制本身甚至是可以扩展的;可以定义并映射一个自定义作用域到对话的概念。Spring 还通过 CommonsPoolTargetSource 支持简单的对象池,但对象池很少是状态管理的最佳解决方案。
更重要的是,Spring 通过 Spring Web Flow 为 Web 应用程序提供了非常强大、高度可配置的状态管理。在那里,会话状态是透明地管理的,这与本次演示声称开发人员必须直接与 HTTP 会话交互来管理 Spring 应用程序中的状态相反。此外,Repository 配置是可插拔的,因此可以使用各种策略进行状态的物理存储(session、client、后端缓存等)。最后,Spring Web Flow 的最新发展包括对扩展持久化上下文的支持以及对 JSF 的完全集成支持。
Spring 2.5 提供了一个新的 'jms' 命名空间,极大地简化了消息监听器的配置。请注意,对于每个监听器,没有单独的容器配置。多个监听器共享配置,并广泛使用智能默认设置。
<jms:listener-container>
<jms:listener destination="queue.confirm" ref="logger" method="log"/>
<jms:listener destination="queue.order" ref="tradeService" method="placeOrder"/>
</jms:listener-container>
此外,还提到线程管理总是每个容器的问题。然而,事实并非如此。消息监听器容器实际上使用了 Spring 的 TaskExecutor 抽象,并且有许多实现可用。例如,如果在 Java 5+ 上运行,您可以配置一个线程池执行器,甚至可以配置一个 CommonJ WorkManager 执行器。如果需要,执行器可以很容易地在多个监听器容器之间共享。事实上,'task-executor' 属性在 'listener-container' 元素上可用(如上所示),它在逻辑上只设置 1 次,但由每个内部创建的监听器定义对应的容器实例共享。
好吧,这确实是当晚最奇怪的时刻。代码幻灯片展示了一个完全无状态的 MessageListener 实现(本应如此!),然后配置幻灯片显示 'maxConcurrentConsumers' 的值设置为 1。此时,有人表示将该值设置为除 1 以外的任何值都会导致线程安全问题。很抱歉地说,这是彻头彻尾的误导信息。并发消费者设置决定了可用于接收消息的线程数量,而 'maxConcurrentConsumers' 决定了消费者池在负载重的情况下可以增长到何种程度(随着需求的减少,消费者数量会回落到设置为 'concurrentConsumers' 的值)。只要 MessageListener 本身是线程安全的,就可以增加此值以控制吞吐量。我个人永远不会使用 MessageListener 来做委托给“服务”以外的事情,因此即使在极不可能的情况下,我希望有一个有状态的对象最终处理消息的内容,那么该目标对象也会配置一个池化目标源。MessageListener 本身将始终是线程安全的,因此 'concurrentConsumers' 和 'maxConcurrentConsumers' 的值可以按预期用于管理吞吐量。
这个话题引出了另一点。全面的比较会在这里揭示 Spring 的另一个优点——即 Spring 的监听器适配器。该适配器提供从 JMS 消息到简单 Java 负载的自动转换,然后委托给任何 Spring 管理的对象来处理该负载。例如,在上面的配置中,“logger”和“tradeService”监听器甚至不必实现 MessageListener 接口。如果它们没有实现,Spring 会自动用一个适配器包装这些 POJO,该适配器会转换消息并确定要调用的方法。它甚至会将返回值(如果存在)转换为 JMS 回复消息,并自动回复到由入站消息的“reply-to”属性指定的目的地。这种行为从头开始实现非常困难,因为 JMS MessageListener 处理方法的返回类型是“void”。
public interface MessageListener {
void onMessage(Message message);
}
EJB 3 仅限于 @AroundInvoke,示例通过拦截展示了一些简单的审计应用。Spring 示例展示了 @Before 通知,因为审计只需要在方法执行之前发生一些事情(而不是环绕)。我很欣赏示例强调了在 EJB 3 端需要调用 context.proceed(),而 AspectJ @Before 通知则简单得多。但我感到失望的是,一些与会者似乎认为 AspectJ 模型仅限于 @Before,因此 EJB 3 的 @AroundInvoke 更强大。为了全面起见,我本应该在 Spring 端包含一个 @Around 通知示例,以澄清它是受支持的,但并不总是必需的。
EJB 3 拦截模型的最大限制是应该被拦截的方法(或类)是直接标注的,而 AOP 的基本目标之一是非侵入性——最终甚至支持对您无法控制的代码提供通知。考虑到这个目标,AspectJ 表达式语言可以说在支持可能应用通知的所有构造的同时,尽可能清晰简洁。虽然初看可能显得神秘,但学习起来相当容易。例如,它在概念上类似于但范围比正则表达式更有限(详细信息请参阅 AspectJ 主页上的表达式语言参考)。
关于这一点,我首先要指出,使用 Spring 有助于缩短开发/测试周期,这样大部分开发时间是在 IDE 中度过,而不是部署到应用服务器,而 IDE 是很好的工具。基于 Eclipse 的 Spring IDE 是一个非常有价值的附加组件,为 Spring 项目提供开发协助,而 IntelliJ 也提供了 IDEA 对 Spring 的支持。至于部署工具,基于 Spring 的应用程序当然可以部署到任何容器中,并且由于 Spring 可以利用底层资源(DataSource、TransactionManager、JMS ConnectionFactory 等),这些资源以与部署到特定容器中的任何应用程序相同的方式进行管理。Spring 能够将任何对象暴露为 JMX MBean(包括对前面提到的 @ManagedResource 的支持)以及其对 JMX 通知/监听器的支持,对于自定义监控和管理需求非常强大。
尽管如此,Spring 显然可以从更多的工具支持中受益。这就是为什么最近建立了“Spring Tool Suite”,旨在将 Spring IDE、AJDT、AspectJ 和 Mylyn 整合在一起,并发展成为更强大的工具。有关更多信息,请参阅此处的文章和链接。
众所周知,可移植性是“说起来容易做起来难”。尽管 EJB 3 在这方面可能比 EJB 2 痛苦少一些(配置的冗余减少),但事实仍然是应用服务器提供不同的功能,因此有不同的配置选项。显然,如果您正在部署到应用服务器,您可能应该利用某些特定功能。原始陈述的问题在于,它暗示在应用程序配置层面存在使 Spring 本身更不具可移植性的东西。恰恰相反,任何从一个应用服务器迁移到另一个应用服务器的 Spring 用户都会同意,Spring 在这方面提供了显著的抽象。在上面的第 4 点中,我提到 Spring 的 JtaTransactionManager 在任何 Java EE 应用服务器中都使用自动检测,同样适用于 MBean 服务器和 RMI 注册表。与此类似,当使用 JPA 时,Spring 会检测 persistence.xml 并相应地创建 EntityManagerFactory。在所有这些情况下,Spring 元数据——无论是注解(@PersistenceContext、@Transactional、@Resource 等)还是 XML('jee:jndi-lookup' 等)——都与任何 EJB 3 应用程序一样具有可移植性。
即使超越典型 EJB 3 应用程序的能力,最小的配置更改也能带来巨大的便利。在这方面,Spring 实际上促进了对更广泛环境的可移植性:Tomcat、Jetty、 standalone、Eclipse、IDEA 等等。我在这里的建议是,下载 Spring 发行版的 PetClinic 示例应用程序,并尝试构建 WAR 文件并将其部署到多个应用服务器中。然后,注意它可以同样轻松地部署到 Tomcat 中,并且一旦您想在应用程序支持的不同数据访问策略(JDBC、Hibernate 和 JPA)之间切换时,其可移植性程度实际上远远超过 EJB 3 应用程序。仔细查看这些不同版本的配置文件(位于 'samples/petclinic/war/WEB-INF' 目录中)。特别是增加了 Spring 2.5 之后,配置极其简洁。请注意,在这些不同版本之间切换所需进行的唯一更改是 web.xml 中的一行,其中 Spring 上下文被引导。如果您想使用容器管理的 DataSource 运行,请使用一行 'jee:jndi-lookup' 元素。否则,有一个 bean 定义用于使用 standalone 的 DataSource,并且实际的数据库属性已外部化到 jdbc.properties 中。
看来我说得比我想象的要多 :)。我的初衷是从 Spring 的角度提供一些客观的澄清,希望读者能够明白这一点。我知道这次展示在 JUG 和会议上非常受欢迎,而且我认为这是一个重要的讨论。许多 Java 开发人员如今面临着大量的选择,获得必要的所有事实来做出明智的决策非常重要。虽然我在这里没有强调(您可能也不想让我继续说下去),但这次展示以及《EJB 3 实战》(EJB 3 in Action)这本书的一个观点是,Spring 和 EJB 3 不必相互排斥。Spring 可以在 EJB 应用程序中使用,可以从 Spring 应用程序访问 EJB,并且 Spring 现在支持大多数相同的注解:@Resource、@PersistenceContext、@PostConstruct、@PreDestroy、@EJB 和 @WebServiceRef。