回应:EJB 3 和 Spring 对比分析
昨晚我参加了新英格兰 Java 用户组(NEJUG)的一次会议,Reza Rahman 在会上展示了关于EJB 3 和 Spring 的“对比分析”。Reza 是《EJB 3 in Action》的作者之一。我很高兴见到 Reza,也很敬佩他能就一个可能引起争议的话题进行演示。我也很赞赏他尝试分析 EJB 3 和 Spring 的优缺点。然而,我感到有必要澄清一些他对 Spring 的介绍中并非完全准确的观点,这些观点让我(以及其他与会者)认为本次演示带有偏向 EJB 3 的倾向。公平地说,与固定规范版本不同,Spring 正在不断发展,我在这里指出的一些内容是新特性。另一方面,有些是 Spring 2.0 的特性,已经存在一年多了。我个人认为,“对比分析”必须考虑所比较产品最新稳定版本的最新特性集。我想不言而喻的是,我也可能有一点偏见,但我在这里的动机是提供一个完全客观的回应,以便演示文稿可以修改,以反映更“同类相比”的比较。我将对演示文稿的 10 个“主题”提供简要回应。
1. EJB 使用注解表示元数据。Spring 使用 XML。
演示中提到,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 注解为用户定义的 stereotypes 提供了一个扩展点。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 版本的文章和博客。
2. Spring 允许您支持多种部署环境,但需要更多配置。
这一点实际上被作为 Spring 的一个优点提出,但强调了配置开销。事实是,任何认真对待测试和敏捷开发的项目都需要支持“多种部署环境”。换句话说,这个话题常常被扭曲,仿佛只适用于多个生产环境。实际上,在每个开发和测试周期都必须部署到应用服务器,这是敏捷性的主要障碍。通常,Spring 用户会将配置模块化,以便“基础设施”配置(例如 DataSource、TransactionManager、JMS ConnectionFactory)是分开的,并且动态属性是外部化的。由于 Spring 支持根据外部化属性替换 '${placeholders}',因此包含不同的属性文件通常会成为一个透明的问题。
3. EJB 使用 JPA,Spring 使用 Hibernate
我必须承认这一点最让我困扰。在对比幻灯片中,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 数据访问相同的事务中调用。
4. Spring 不做任何假设,您必须提供配置。
一个具体的例子是事务管理器的定义。演示中提到,您必须了解容器供应商级别的细节才能配置 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 轻松支持独立的 JTA 提供商,如 Atomikos 和 JOTM。诚然,Spring 的事务管理器设置需要配置一个单一的 bean 定义,但这确实是一次性的开销——而且非常值得。
5. Spring 没有有状态的应用范式。
无状态服务层的好处作为最佳实践已经相当成熟,Spring 也接受这一点。然而,Spring 确实提供了除 singleton 之外的其他作用域。Spring 的“prototype”作用域为每次注入或查找提供一个不同的实例,Spring 2.0 引入了 web 作用域:“request”和“session”。作用域机制本身甚至是可以扩展的;可以将自定义作用域定义并映射到会话概念。Spring 也支持使用 CommonsPoolTargetSource 进行简单的对象池化,但对象池化很少是状态管理的最佳解决方案。
更重要的是,Spring 通过 Spring Web Flow 为 Web 应用提供了非常健壮、高度可配置的状态管理功能。在那里,会话状态是透明管理的,这与本次演示中声称开发者必须直接与 HTTP Session 交互来管理 Spring 应用中的状态的说法是矛盾的。此外,存储库配置是可插拔的,因此可以使用各种策略来物理存储状态(会话、客户端、后端缓存等)。最后,Spring Web Flow 的最新进展包括对扩展持久化上下文的支持以及对 JSF 的完全集成支持。
6. Spring 需要为每个 MessageListener 配置一个容器。
Spring 2.5 提供了一个新的 'jms' 命名空间,极大地简化了消息监听器的配置。请注意,不需要为每个监听器单独配置一个容器。多个监听器共享配置,并且广泛使用了智能默认设置:
<jms:listener-container>
<jms:listener destination="queue.confirm" ref="logger" method="log"/>
<jms:listener destination="queue.order…