领先一步
VMware 提供培训和认证,助您加速进步。
了解更多昨晚我参加了一个新英格兰 Java 用户组(NEJUG)的会议,Reza Rahman 在会上发表了关于 EJB 3 和 Spring 的“比较分析”。Reza 是《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 注解(支持与 @Stateless EJB 相同的语义,默认传播级别为 REQUIRED)。我特别感到失望的是,比较中没有在双方都包含 JPA(见下文第 3 点)。Spring 2.0 还引入了完整的基于注解的 AspectJ 支持(@Aspect、@Before、@After、@Around)以及“构造型”注解的概念。例如,@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 支持任何类型的对象的 DI)和 EJB 3 的主要优势(使用注解而不是 XML)。Spring 2.5 还引入了一个更细粒度的基于注解的依赖注入模型,基于 @Autowired 和(可扩展的)@Qualifier 注解。Spring 2.5 还将“构造型”注解扩展到包括 @Service 和 @Controller。每个构造型注解通过将其作为元注解来扩展通用的 @Component 注解。通过应用相同的技术,@Component 注解为用户定义的构造型提供了扩展点。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 版的文章和博客。
这实际上被 presenters 作为一个 Spring 的优势来介绍,但强调了配置的开销。事实是,任何认真对待测试和敏捷开发的项目的都将需要支持“多种部署环境”。换句话说,这个特定主题经常被曲解,好像它只适用于多种*生产*环境。实际上,在每个开发和测试周期中都部署到应用服务器会是敏捷性的一个重大障碍。通常,Spring 用户会模块化其配置,以便“基础设施”配置(例如 DataSource、TransactionManager、JMS ConnectionFactory)是独立的,并且动态属性被外部化。由于 Spring 支持根据外部化属性替换 '${placeholders}',因此包含不同的属性文件通常会变成一个透明的问题。
我不得不承认,这一点最让我感到困扰。在比较幻灯片中,EJB 3 的示例显示了 JPA 通过 *entityManager* 进行数据访问,并且 *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。同样,如果您想添加一些直接的 JDBC 进行数据访问,而 ORM 是过度的话,Spring 也支持这一点——即使是在与 Hibernate 或 JPA 数据访问相同的事务中调用。
一个具体的例子是事务管理器的定义。有人说您必须了解容器供应商级别的知识才能配置 Spring 集成。这是不正确的。例如,以下 bean 定义不包含任何特定于容器的信息,但 Spring 将自动检测所有 Java EE 应用服务器中的事务管理器。
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
如果您确实想利用特定于容器的功能,例如每个事务的隔离级别,那么 Spring 也提供了一些专门的实现:*WebLogicJtaTransactionManager*、*WebSphereUowTransactionManager* 和 *OC4JJtaTransactionManager*。切换这些实现之间只需更改这一个定义。
除此之外,Spring 的配置幻灯片不必要地冗长。我担心这也可能是出于强调 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 Session 交互来管理 Spring 应用程序状态的说法相反。此外,存储库配置是可插拔的,因此可以使用各种策略来物理存储状态(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' 元素(上面显示)上可用,在那里它将只设置一次,并且由每个内部为每个侦听器定义创建的容器实例共享。
好吧,这确实是当晚最奇怪的时刻。代码幻灯片描绘了一个完全无状态的消息侦听器实现(正如它应该的那样!),然后配置幻灯片显示 'maxConcurrentConsumers' 的值为 1。此时,有人说将该值设置为除一以外的任何值都会导致线程安全问题。抱歉,这是彻头彻尾的错误信息。并发消费者设置决定了用于接收消息的线程数,而 'maxConcurrentConsumers' 决定了在负载过重时消费者池可以增长到什么程度(当需求减少时,消费者数量会下降到设置为 'concurrentConsumers' 的值)。只要消息侦听器本身是线程安全的,就可以增加此值来控制吞吐量。我个人永远不会将消息侦听器用于任何除了委托给“服务”之外的目的,因此即使在(非常不可能)的情况下,我也想让一个有状态对象最终处理消息的内容,那么该目标对象将配置一个池目标源。消息侦听器本身将始终是线程安全的,因此 '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、独立环境、Eclipse、IDEA 等。我的建议是,获取 Spring 发行版的 PetClinic 示例应用程序,并尝试将 WAR 文件构建并部署到多个应用服务器中。然后,您会注意到它可以轻松地部署到 Tomcat 中,并且一旦您想在应用程序支持的不同数据访问策略(JDBC、Hibernate 和 JPA)之间切换,其可移植性程度实际上远远超过了 EJB 3 应用程序。仔细查看这些不同版本(位于 'samples/petclinic/war/WEB-INF' 目录中)的不同配置文件。特别是随着 Spring 2.5 的加入,配置非常简洁。请注意,在这些不同版本之间切换所需的唯一更改是在引导 Spring 上下文的 web.xml 中的一行。如果您想使用容器管理的 DataSource,可以使用单行 'jee:jndi-lookup' 元素。否则,有一个 bean 定义用于使用独立的 DataSource,实际的数据库属性被外部化到 jdbc.properties 中。
嗯,看来我的话比我想的要多 :)。我的意图是提供一些来自 Spring 视角的客观澄清,我希望读者能够清楚地看到这一点。我知道这次演讲在 JUG 和会议上非常受欢迎,我认为这是一个重要的讨论。今天,许多 Java 开发人员被海量选项所压倒,重要的是他们拥有做出明智决策所需的所有事实。虽然我在这里没有强调这一点(而且您可能也不希望我再说下去),但本次演讲和书籍(《EJB 3 in Action》)的一个观点是,Spring 和 EJB 3 无需互斥。Spring 可以在 EJB 应用程序中使用,EJBs 可以从 Spring 应用程序访问,并且 Spring 现在支持大多数相同的注解:@Resource、@PersistenceContext、@PostConstruct、@PreDestroy、@EJB 和 @WebServiceRef。