Green Beans:Spring 在您的服务层入门

工程 | Josh Long | 2011 年 1 月 8 日 | ...

所有应用程序都源于领域模型。 “领域模型”一词描述了系统中对您试图解决的问题很重要的名词,即数据。服务层——业务逻辑所在之处——会操作应用程序数据,并最终必须持久化它(通常是保存在数据库中)。这个解释很简单,但实际上,构建一个优秀的服务层对于任何开发人员来说都可能是一项艰巨的任务。本文将向开发人员介绍 Spring 框架中用于构建更好的服务层的可用选项。假设读者对 SQL 语言有一定的经验,并且——更重要的是——读者熟悉 Spring 基本依赖注入和配置概念。本项目源代码位于 SpringSource 的 Git 存储库中的 Spring Samples 项目下。

名词与动词

服务层描述系统中的动词(操作)。领域模型描述名词(数据)。像 Grails 和 Spring Roo 这样的工具可以通过查看领域模型来自动推断和生成业务对象。这种方法称为模型驱动开发,对于高度交互式的应用程序开发来说,它是一个很大的帮助。理解构建块最终将帮助您更高效地使用 Spring Roo 等工具。在本文中,我们将构建一个服务来处理符合以下规则的客户数据:
  1. 一个人只有在从企业购买了某些东西后才算作客户。
  2. 一个人的购买称为 purchase,其中包含 line items。
  3. line items 是特定订单中已购买产品的记录。

这种类型的数据——具有浅记录计数的链接数据——非常适合关系型数据库管理系统(通常称为 RDBMS)。RDBMS 通过将领域模型映射到表来工作。我们服务的表可视化如下:

ERD diagram fro the CRM system in the Green Beans post on building a better service tier

设置数据库

我们的实现会将所有数据存储在一个名为 H2 的 RDBMS 中。当然,您可以随意按照您喜欢的任何数据库进行操作。本文将使用几个简单的数据库表作为示例。这些表的 H2 数据定义语言 (DDL) 脚本可在源代码(src/main/resources/crm.sql)中找到。DDL 非常简单,通过微小的调整就可以在大多数数据库中正常工作。如果您想使用 H2,请按照这些说明进行设置(如果您尚未设置)。否则,请随时跳到下一节“领域模型”。H2 是一个轻量级的、嵌入式的内存 SQL 数据库,可以快速设置和运行。要开始,请从 H2 主页下载最新分发版。选择您喜欢的任何分发版(Windows 或“所有平台”),尽管出于本文的目的,我们将选择“所有平台”分发版。将分发版解压缩到您喜欢的文件夹中。在命令行中,导航到分发版中的 bin 文件夹,然后运行适合您平台的 shell 脚本(Windows 为 h2.bat,Unix 或 Linux 环境为 h2.sh)来启动数据库进程并启动一个可用于与数据库交互的 shell。在“JDBC URL:”字段中输入:jdbc:h2:tcp://127.0.0.1/~/crm_example(不带引号),其余不变,然后单击“连接”按钮。通过在浏览器中打开 URL https://:8082/login.jsp 来调出数据库控制台。H2 可以嵌入式运行(与我们现在这样作为服务器运行相反),但像我们这样将其作为服务器运行可以提供更丰富的体验。登录后,您将能够在 H2 控制台中尝试查询。

领域模型

描述领域模型的代码应尽可能不包含持久化方面的考虑。理想情况下,您应该能够以清晰、面向对象的术语描述您的领域模型。我们领域模型的代码是:

package org.springsource.examples.crm.model;
…
public class Customer implements java.io.Serializable {
    private Long id;
    private String firstName;
    private String lastName;
    private Set purchases = new HashSet();
    // constructors, and accessor / mutator pairs omitted for brevity
}

Customer 实体包含对 `Purchase` 的引用,Purchase 定义如下:

package org.springsource.examples.crm.model;
…
public class Purchase implements java.io.Serializable {
    private Long id;
    private Customer customer;
    private double total;
    private Set lineItems = new HashSet();
    // constructors, and accessor / mutator pairs omitted for brevity
}

Purchase 又包含对 `LineItems` 集合的引用,LineItems 定义如下:

package org.springsource.examples.crm.model;
…
public class LineItem implements java.io.Serializable {
    private Long id;
    private Purchase purchase;
    private Product product;
    // constructors, and accessor / mutator pairs omitted for brevity
}

最后,`LineItem` 引用一个 `Product`。`Product` 是库存中某个物品的定义,定义如下:

package org.springsource.examples.crm.model;
…
public class Product implements java.io.Serializable {
    private Long id;
    private String description;
    private String name;
    private double price;
    private Set lineItems = new HashSet();
    // constructors, and accessor / mutator pairs omitted for brevity
}

构建 Customer Repository

因此,我们的首要任务是构建一个存储库对象来持久化 `Customer` 记录。暂时忽略领域模型中的其他实体。存储库应使使用者与用于处理持久化的原始 API 隔离。输入和输出应该是领域模型对象,而不是低级持久化原语。让我们看看 `Customer` 存储库的接口:
package org.springsource.examples.crm.services.jdbc.repositories;
import org.springsource.examples.crm.model.Customer;

public interface CustomerRepository {
  Customer saveCustomer(Customer customer) ;
  Customer getCustomerById(long id);
}

我们将使用 JDBC 来构建我们的存储库。JDBC(Java 数据库连接 API)是 Java 平台提供的标准数据库连接框架。它的用法有充分的文档记录,所有主要供应商都提供其数据库的 JDBC 驱动程序连接。这似乎使 JDBC 成为构建存储库的自然起点。但实际上,直接的 JDBC 实现可能会很繁琐。

package org.springsource.examples.crm.services;
import org.springsource.examples.crm.model.Customer;

public interface CustomerService {
    Customer getCustomerById(long id);
    Customer createCustomer(String fn, String ln);
}

由于直接使用 JDBC 可能非常繁琐,我们将不再深入探讨。建议您查看本文的源代码,其中我们构建了一个直接的 JDBC 存储库,它需要令人眼花缭乱的 150 多行代码来处理我们将要介绍的内容,包括线程安全以及资源获取和销毁。

相反,让我们介绍一个 Spring 框架类,称为 `JdbcTemplate`,它极大地简化了基于 JDBC 的开发。

要开始,我们设置了一个标准的 Spring XML 文件,该文件设置了类路径组件扫描,并为 `database.properties` 文件中的属性引入了属性占位符解析。我们在此处不再重复 XML,因为它已包含在源代码中(本文的示例 XML 文件位于 src/main/resources),并且代表了非常基本的 Spring 配置。Spring 类路径组件扫描进而拾取 Java 配置类,这正是我们将在本文中重点关注和扩展的内容。名为 `CrmConfiguration` 的基础通用 Java 配置类如下所示。`CrmConfiguration` 类仅配置一个 `javax.sql.DataSource`。

package org.springsource.examples.crm.services.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import javax.sql.DataSource;

@Configuration
public class CrmConfiguration {
    @Value("${dataSource.driverClassName}")
    private String driverName;

    @Value("${dataSource.url}")
    private String url;

    @Value("${dataSource.user}")
    private String user;

    @Value("${dataSource.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource simpleDriverDataSource = new SimpleDriverDataSource();
        simpleDriverDataSource.setPassword(this.password);
        simpleDriverDataSource.setUrl(this.url);
        simpleDriverDataSource.setUsername(this.user);
        simpleDriverDataSource.setDriverClass(org.h2.Driver.class);
        return simpleDriverDataSource;
    }
}

为了有效地使用 JDBC,我们将使用 Spring 的 `JdbcTemplate` 来最大限度地减少样板代码。`JdbcTemplate` 将使我们免受资源管理的困扰,并极大地简化与 JDBC API 的工作。以下是 `CustomerRepository` 接口的 `JdbcTemplate` 基于实现的配置。在配置中,我们定义了一个 `JdbcTemplate` 实例。

package org.springsource.examples.crm.services.jdbc;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springsource.examples.crm.services.config.CrmConfiguration;

import javax.sql.DataSource;

@Configuration
public class JdbcConfiguration extends CrmConfiguration {
    @Bean
    public JdbcTemplate jdbcTemplate() {
        DataSource ds = dataSource(); // this comes from the parent class
        return new JdbcTemplate(ds);
    }
}

以下是基于 `JdbcTemplate` 的 `CustomerRepository` 实现:

package org.springsource.examples.crm.services.jdbc.repositories;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import org.springsource.examples.crm.model.Customer;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Repository
public class JdbcTemplateCustomerRepository implements CustomerRepository, InitializingBean {

  @Value("${jdbc.sql.customers.queryById}")
  private String customerByIdQuery;

  @Value("${jdbc.sql.customers.insert}")
  private String insertCustomerQuery;

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public Customer getCustomerById(long id) {
    return jdbcTemplate.queryForObject(customerByIdQuery, customerRowMapper, id);
  }

  public Customer saveCustomer(Customer customer) {

    SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    simpleJdbcInsert.setTableName("customer");
    simpleJdbcInsert.setColumnNames(Arrays.asList("first_name", "last_name"));
    simpleJdbcInsert.setGeneratedKeyName("id");

    Map<String, Object> args = new HashMap<String, Object>();
    args.put("first_name", customer.getFirstName());
    args.put("last_name", customer.getLastName());

    Number id = simpleJdbcInsert.execute(args);
    return getCustomerById(id.longValue());
  }

  public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.jdbcTemplate, "the jdbcTemplate can't be null!");
    Assert.notNull(this.customerByIdQuery, "the customerByIdQuery can't be null");
    Assert.notNull(this.insertCustomerQuery, "the insertCustomerQuery can't be null");
  }

  private RowMapper<Customer> customerRowMapper = new RowMapper<Customer>() {

    public Customer mapRow(ResultSet resultSet, int i) throws SQLException {
      long id = resultSet.getInt("id");
      String firstName = resultSet.getString("first_name");
      String lastName = resultSet.getString("last_name");
      return new Customer(id, firstName, lastName);
    }
  };
}

在此示例中,存储库类用 @Repository 注释,这是一个 Spring 框架的*原型*注释,(除了我们在此不需要担心的某些小方面外)等同于 Spring 框架的 `@Component` *原型*注释。在此特定示例中,我们也可以轻松地使用 `@Component`。

第一个方法——`getCustomerById(long)`——使用 `jdbcTemplate` 实例来执行查询。`JdbcTemplate` 的 `query` 方法的第一个参数是 SQL 语句,第二个参数是 `RowMapper` 的实例。RowMapper 是一个 Spring 接口,客户端可以实现它来处理将结果集数据映射到对象(在这种情况下,是 `Customer` 的实例)。对于返回结果集中的每一行,`JdbcTemplate` 将调用 `mapRow(ResultSet,int)`。

RowMapper 实例是无状态的(因此是线程安全的),并且应该被缓存以供其他映射 Customer 记录的查询使用。在 SQL 字符串和 RowMapper 实例之后,`jdbcTemplate.queryForObject` 方法支持 Java 5 的 varargs 语法,用于按数字顺序绑定到查询的参数:第一个可变参数绑定到查询中的第一个“?”,第二个绑定到第二个“?”,依此类推。

第二个方法插入一条记录(简单),然后检索新插入记录的 ID。我们使用 `SimpleJdbcInsert` 对象来描述表、所需的参数,然后以与数据库无关的方式执行插入。

现在,我们有了一个工作的存储库。存储库是一个哑对象。它不知道事务,也不理解业务逻辑。业务对象使用存储库实现并对其进行编排。业务对象拥有“大局”,而存储库只关心持久化您的领域模型。在决定什么放在哪里时,请牢记这一点。

让我们先检查我们的 `CustomerService` 接口。

package org.springsource.examples.crm.services;
import org.springsource.examples.crm.model.Customer;

public interface CustomerService {
    Customer getCustomerById(long id);
    Customer createCustomer(String fn, String ln);
}

这看起来与存储库接口相似,人们可能会想为什么需要使用存储库。应该理解的是,服务关心业务状态,而不是应用程序状态。服务关心业务事件。例如,存储库关心将 `Customer` 记录持久化到数据库的机制,而服务则关心确保 `Customer` 记录处于有效状态,并且 `Customer` (例如)尚未注册公司免费产品试用活动。因此,虽然服务和存储库似乎都有一个“创建客户”的方法,但它们的目的应该非常不同且单一。考虑到这一点,让我们看看我们简单的基于 JDBC 的 CustomerService 实现。

package org.springsource.examples.crm.services.jdbc.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springsource.examples.crm.model.Customer;
import org.springsource.examples.crm.services.CustomerService;
import org.springsource.examples.crm.services.jdbc.repositories.CustomerRepository;

@Service
public class JdbcCustomerService implements CustomerService {

  @Autowired
  private CustomerRepository customerRepository;

  public Customer getCustomerById(long id) {
    return this.customerRepository.getCustomerById(id);
  }

  public Customer createCustomer(String fn, String ln) {
    Customer customer = new Customer(fn, ln);
    return this.customerRepository.saveCustomer(customer);
  }
}

这个服务很直接。与存储库一样,我们将此服务用 Spring 注释原型 `@Service` 进行注释。这个注释比 `@Component` 更能传达类的意图,但没有理由不能使用 `@Component`。一个典型的服务应该比存储库具有更粗粒度的方法,因此通常可以看到服务中使用多个存储库。服务编排多个存储库。这意味着服务需要确保跨多个存储库、跨多个客户端的一致状态。不难想象不一致状态所代表的灾难。假设您有一个服务代表电子商务网站上的用户管理购物车结账。当用户点击“提交订单”时,服务需要从库存中预留购物车中的所有行项目,并从用户账户中扣款。如果此时,另一位用户尝试结账相同的商品(不幸的是,库存中只剩一件),并且结账速度足够快,那么第一位用户将为他无力提供的商品付费!

事务

这种情况很常见,这也是数据库支持事务概念的原因。事务将数据库中的活动块划定界限,并缓冲该活动期间的所有更改。在事务中的所有操作都成功执行并*提交*之前,数据库保持不变。如果另一个客户端读取事务中正在更改的数据,该客户端将“看到”事务开始之前的数据对象和记录。在第一个事务提交之前,这些客户端无法对数据库进行更改。事务确保并发读取的一致状态。

单个操作——也许是查询和更新——会转化为单个 `jdbcTemplate` 调用。通过使用 Spring 的 `TransactionTemplate` 和 Spring 的 PlatformTransactionManager 层次结构实例,这些单个调用可以共享同一个事务。然后 `TransactionTemplate` 使用事务管理器按需启动和提交事务。Spring 框架提供 `TransactionTemplate` 来提供事务同步。我们的服务只使用一个事务资源——一个 `javax.sql.DataSource`——因此 `DataSourceTransactionManager` 的实例就足够了。将以下内容添加到 `JdbcConfiguration` 类中。

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(this.dataSource());
        return dataSourceTransactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate() {
        TransactionTemplate tt = new TransactionTemplate();
        tt.setTransactionManager( this.transactionManager() );
        return tt;
    }

使用事务模板在服务类中执行应包含在事务中的逻辑。下面显示了服务代码的关键部分的变化。


    @Autowired 
    private TransactionTemplate transactionTemplate; 

     public Customer getCustomerById(final long id) {
        return this.transactionTemplate.execute(new TransactionCallback() {
            public Customer doInTransaction(TransactionStatus status) {
               // … all the same business logic as before
            }
        });
    }

    public Customer createCustomer( final String firstName, final String lastName) {
        return this.transactionTemplate.execute(new TransactionCallback() {
            public Customer doInTransaction(TransactionStatus status) {
               // … all the same business logic as before
            }
        });
    }

瞧!`JdbcTemplate` 使处理 JDBC 变得异常简单,而 `TransactionTemplate` 使事务管理变得轻而易举。这种实现比我们手工完成的任何事情都要简单得多。我认为这是一个成功。

然而,我们可以做得更好。代码的许多演变都是为了从代码中移除横切关注点。在可能的情况下,Spring 提供了比 API(如 JDBC)更简单的抽象。Spring 还支持在有益的行为可以通过面向方面编程 (AOP) 系统地应用的地方引入功能。在前面的示例中,如果您仔细观察,就会清楚 `transactionTemplate` 只是将方法执行本身——而不是方法执行的任何单个部分——包装在事务上下文中。当方法执行开始时,会创建或重用一个事务。当方法执行结束时,如果不是嵌套事务,则会提交事务。任何可以根据方法执行边界来描述的问题都可以很好地通过 AOP 方法来解决。不出所料,Spring 提供了开箱即用的基于 AOP 的事务支持,可以在方法执行边界启动和提交事务。

要启用 Spring 的事务支持,请将此添加到您的 Spring XML 配置文件中:

	<tx:annotation-driven transaction-manager = "transactionManager" />

这个 `` 引用了在 Java 配置中配置的 `transactionManager` bean。该声明启用了 Spring 框架的功能,该功能会检测到服务 bean 的方法上存在 Spring 的 `@Transactional` 注释。`transactionTemplate` 的引用变得无关紧要,并且可以从配置类和实现中删除。剩下的就是将 `@Transactional` 添加到服务方法的定义中。

`@Transactional` 注释可以参数化以自定义事务行为。`getCustomerById` 方法被注释为 `@Transaction(readOnly = true)`,因为该方法不会在此方法中修改数据库中的任何内容。将其设置为 `readOnly` 只是告诉 Spring 框架不必创建事务。创建事务并不总是廉价的,应该谨慎使用。有关 Spring 框架中事务支持的更多信息,鼓励读者查阅 Juergen Hoeller 的(非常酷的)事务管理策略的录制演示

修改后的实现如下所示:

    @Transactional(readOnly = true)
    public Customer getCustomerById(final long id) {
	// … same as before, with transactionTemplate removed
    }

    @Transactional
    public Customer createCustomer(final String firstName, final String lastName) {
	// … same as before, with transactionTemplate removed
    }

承诺关系(使用 Java Persistence API)

我们有了一个完整的、工作的 `CustomerService` 实现,使用了我们基于 JDBC 的存储库。所有能够让我们以领域模型的术语干净地与数据存储进行通信的事情,使用 JDBC,都已经完成了。这些示例之所以简单,是因为到目前为止,我们试图做的事情很简单。我们正在处理一种类型的对象——Customer——还没有开始编写处理关系的代码,例如 Customer 的 purchases。回想一下,purchases 包含 line items,line items 引用 products。即使在我们简单的领域中,这个对象图也可能非常深。虽然当然可以将数据库表作为对象来处理,但这并不自然。数据库强制执行的模型——行、列和外键——与我们的领域模型之间的这种分裂被称为*对象-关系阻抗不匹配*,并且是所有面向对象语言的共同问题,不仅仅是 Java。Java Persistence API (JPA) 标准化了对象-关系映射技术(ORM)。ORM 通常会接受对象类型和数据库表之间的映射,并提供一种基于此映射来持久化、操作和查询对象的干净方式。在 JPA 中,这种映射主要由领域类本身的元数据注释驱动。JPA 实现通常支持多个数据库供应商。

要开始使用 JPA,您应该已经选择了一个数据库(前面的示例已经建立了 H2 数据库,所以我们将使用它),以及一个 JPA 实现。有许多不同的 JPA 提供商。许多 JPA 实现都打包在其他早于该标准的 ORM 解决方案中。出于本文的解决方案目的,我们正在使用 Hibernate JPA 实现。

让我们修改我们的第一个示例以使用 JPA。首先,修改 Customer 类以包含 JPA 引擎正确的注释驱动元数据。元数据是使用默认值派生的,而在需要显式配置时,则使用 Java 语言注释。下面的代码中省略了 mutators。

package org.springsource.examples.crm.model;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "customer")
public class Customer implements java.io.Serializable {
    // private variables and mutators omitted
    @Id
    @GeneratedValue
    @Column(name = "id", unique = true, nullable = false)
    public Long getId() {
        return this.id;
    }

    @Column(name = "first_name", nullable = false)
    public String getFirstName() {
        return this.firstName;
    }

    @Column(name = "last_name", nullable = false)
    public String getLastName() {
        return this.lastName;
    }

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "customer")
    public Set getPurchases() {
        return this.purchases;
    }
}

用 `@Entity` 注释的类将被注册到 JPA 实现。由于我们的 `CustomerService` 实现使用预先存在的数据库模式,因此我们添加了 `@Table` 注释并指定了要将此类映射到的特定表。接下来,使用 `@Id` 注释指定哪个字段映射到表的主键。`@GeneratedValue` 注释告诉 JPA 期望该列将由数据库系统自动递增(或生成)。`@Column` 注释在此类中是多余的,因为 JPA 引擎将自动从类的 JavaBean 风格的属性推断列名,但如果存在不匹配,则可用于控制 JavaBean 属性如何映射到表中的列名。

对 purchases 集合的 mutator 的注释是最重要的,因为它描述了一个关系。该注释——`@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "customer")`——有点密集,但功能强大!此注释告诉 JPA 引擎,该 `Customer` 对象属于零个或多个 Purchase 对象。JPA 引擎知道所有具有与当前客户 ID 相同的 `Customer` 属性(类型为 `Customer`)的 `Purchase` 对象都属于该客户。这些 purchase 对象——用数据库的术语来说——具有引用该客户记录的外键,这由 `mappedBy = "customer."` 表达。因此,Customer 类有一个 purchase 的 JavaBean 属性(集合),并且 `Purchase` 有一个 `Customer` 的 JavaBean 属性。下面摘录了 Purchase 类中的双向映射:

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    public Customer getCustomer() {
        return this.customer;
    }

JPA 引擎知道 `Purchase` 对象上的 `Customer` 对象是通过查看 `PURCHASE` 表的 `CUSTOMER_ID` 列,然后加载 `Customer` 实例来获得的。让我们回顾一下 `CustomerService` 实现,看看它如何比基于 `JdbcTemplate` 的实现有所改进。首先是新配置:

package org.springsource.examples.crm.services.jpa;

import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.*;
import org.springframework.transaction.PlatformTransactionManager;
import org.springsource.examples.crm.services.config.CrmConfiguration;
import javax.persistence.EntityManagerFactory;

@Configuration
public class JpaConfiguration extends CrmConfiguration {

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    localContainerEntityManagerFactoryBean.setDataSource(this.dataSource());
    return localContainerEntityManagerFactoryBean;
  }

  // this is required to replace JpaTemplate's exception translation
  @Bean
  public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
    PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor =  new PersistenceExceptionTranslationPostProcessor();
    persistenceExceptionTranslationPostProcessor.setRepositoryAnnotationType( Service.class);
    // do this to make the persistence bean post processor pick up our @Service class. Normally it only picks up @Repository
    return persistenceExceptionTranslationPostProcessor;
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
    EntityManagerFactory entityManagerFactory = entityManagerFactory().getObject();
    return new JpaTransactionManager(entityManagerFactory);
  }

}

事务管理器实现是 `JpaTransactionManager`,这是一个 `PlatformTransactionManager` 实现,它知道如何管理 JPA 本地事务。`LocalContainerEntityManagerFactoryBean` 创建 `javax.persistence.EntityManagerFactory` 的实现,这是一个 JPA 类,可用于创建 `javax.persistence.EntityManager` 的实例,它是用 JPA API 的术语与数据源交互的核心 API。这个 API 是您可能与 JPA 进行的所有操作的关键。JPA 提供的简洁性使得一个合适的存储库对象看起来有点*多余*。毕竟,存储库的全部价值在于它允许客户端以领域模型的术语处理持久化方面的考虑,而 JPA 已经这样做了。如果您确实有非常棘手的持久化需求,您仍然可以保留单独的层,尤其是这样。然而,出于本文的目的,我们将利用 JPA 的简洁性,将存储库层折叠到服务层中。

您会注意到缺少一个模板类。Spring 确实提供了 `JpaTemplate`,但最好让 Spring 直接为您注入一个 EntityManager。当您使用组件扫描(就像我们正在做的那样)时,Spring 会自动查找 `@javax.persistence.PersistenceContext`(一个标准注释)并为您注入一个配置好的 `EntityManager` 实例*代理*。为什么是代理?因为 `EntityManager` 不是线程安全的,所以 Spring 会做繁重的工作来确保不同的客户端请求可以使用线程本地的 `EntityManager` 实例。如果我们使用了 `JpaTemplate` 类,我们将受益于 Spring 的异常转换。对于 Spring 框架提供的所有 ORM 模板类,Spring 会自动将技术特定的(检查型)异常转换为 Spring ORM 包中的标准运行时异常层次结构(根为 `org.springframework.dao.DataAccessException`)。这样,您就可以以标准方式处理代码中的不同异常。我们选择在此处简单地注入 entity manager,因此我们需要重新启用异常转换。为此,请注册一个 `PersistenceExceptionTranslationPostProcessor`,它将在没有 `JpaTemplate` 的情况下为我们处理异常转换。

下面的代码代表了基于 JPA 的服务。它比我们的 `JdbcTemplateCustomerService` 稍长,但实现了与存储库*和*我们的服务相同的目标!

以下是基于 JPA 的 CustomerService 实现的代码:

package org.springsource.examples.crm.services.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springsource.examples.crm.model.Customer;
import org.springsource.examples.crm.services.CustomerService;

@Service
public class JpaDatabaseCustomerService implements CustomerService {

  @PersistenceContext
  private EntityManager entityManager;

  @Transactional(readOnly = true)
  public Customer getCustomerById(long id) {
    return this.entityManager.find(Customer.class, id);
  }

  @Transactional
  public Customer createCustomer(String fn, String ln) {
    Customer newCustomer = new Customer();
    newCustomer.setFirstName(fn);
    newCustomer.setLastName(ln);
    this.entityManager.persist(newCustomer);
    return newCustomer;
  }
}

不错!我们的实现变成了您面前的短暂的缩影。导入语句和实际的方法体行数一样多!一旦外围的关注点消失,JPA 本身就能在很大程度上让您以领域中的对象来解决问题。注入的 `EntityManager` 代理已将 JDBC 中本应是许多冗长操作简化为单行代码,并且它是线程安全的!最后,Spring 的基于 AOP 的事务支持使得管理业务对象中的应用程序状态的通常复杂、充满并发挑战的任务变得轻松(所有这些都只需一个注释!)。

总结

在本文中,我们探讨了一些更常见的、可能会让迷茫的开发人员面临的选择。我们使用 Spring 来尽可能提高生产力,并——凭借所有 Spring 可以为我们简化的知识——我们得出了这个*非常*简单、最终的实现。

Spring 框架为许多其他数据持久化选项提供了类似的 YS 支持。这些选项都遵循与本文建立的支持相同的通用模板,因此如果您决定改用它们,它们将很容易上手。本文不涵盖 Spring 对许多其他 ORM 解决方案(如 Hibernate、JDO、TopLink 等)的支持。例如,有关 Hibernate 支持的更多信息,您可以查阅 Alef 关于该主题的精彩博文。该博文也没有涉及 Spring Data 项目提供的广泛的 NoSQL 支持。本文中的示例逐步简化,并且越来越依赖于约定优于配置。随着示例向上移动抽象栈,总有一种方法可以利用底层 API 的全部功能。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有