绿豆:在服务层中开始使用 Spring

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

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

名词和动词

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

这种数据——具有浅记录数的关联数据——非常适合关系数据库管理系统(通常称为 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://127.0.0.1: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,其定义如下:

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的集合,其定义如下:

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引用ProductProduct是库存中某物的定义,定义如下:

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记录。现在,我们将忽略领域模型中的其他实体。存储库应将用户与用于处理持久性的原始 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 多行代码来处理我们将要介绍的内容,包括线程安全以及资源获取和销毁。

相反,让我们介绍一个名为JdbcTemplate的 Spring 框架类,它大大简化了基于 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 的工作。以下是基于JdbcTemplateCustomerRepository接口的配置。在配置中,我们定义了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);
    }
}

以下是基于JdbcTemplateCustomerRepository实现。

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实例发出查询。JdbcTemplatequery方法将其第一个参数作为 SQL 语句,并将第二个参数作为RowMapper<T>的实例。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" />

这个<tx:annotation-driven/>引用了在 Java 配置中配置的transactionManager bean。该声明启动 Spring 框架中的功能,该功能检测 Spring 的@Transactional注解在服务 bean 中方法中的存在。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 持久性 API)

我们有一个完整的、可工作的CustomerService实现,使用基于 JDBC 的存储库。使用 JDBC,为了让我们能够根据我们的领域模型清晰地与数据存储进行交互,已经完成了一切可以做的事情。这些示例非常简单,因为到目前为止,我们正在尝试做的事情很简单。我们正在处理一种类型的对象——客户——并且尚未开始编写处理关系(例如客户的购买)的代码。回想一下,购买有项目,项目引用产品。即使在我们的简单领域中,这个对象图也可能非常深。虽然根据对象处理数据库表当然是可以的,但这并不自然。数据库强加的模型(行、列和外键)与我们的领域模型之间的这种差异称为对象关系阻抗不匹配,这在所有面向对象的语言中都是常见的,而不仅仅是 Java。Java 持久性 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注解指定哪个字段映射到表的 primary key。@GeneratedValue注解告诉 JPA 预期此列将由数据库系统自动递增(或生成)。@Column注解在此类中是冗余的,因为 JPA 引擎会自动从类的 JavaBean 样式属性推断列名,但如果存在不匹配,则可以使用它来控制 JavaBean 属性如何映射到表中的列名。

purchases 集合的 mutator 上的注解最重要,因为它描述了一种关系。注解 - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "customer") - 有点密集,但却包含了很多内容!此注解告诉 JPA 引擎,有零个或多个 Purchase 对象属于此Customer对象。JPA 引擎知道所有具有“customer”属性(类型为Customer)且 ID 与当前属于此客户的 ID 相同的Purchase对象。这些购买对象——用数据库术语来说——具有引用此客户记录的外键,这由mappedBy = "customer."表示。因此,Customer 类有一个 JavaBean 属性(一个集合)用于购买,而Purchase有一个 JavaBean 属性用于Customer。下面的 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 的简洁性并将存储库层折叠到服务层中。

您会注意到缺少一个 Template 类。Spring 确实附带了一个JpaTemplate,但是您最好让 Spring 直接为您注入一个 EntityManager。当您使用组件扫描(就像我们一样)时,Spring 将自动查找@javax.persistence.PersistenceContext(一个标准注解)并为您注入已配置的EntityManager实例代理。为什么是代理?因为EntityManager不是线程安全的,因此 Spring 会完成繁重的工作以确保不同的客户端请求可以使用线程本地EntityManager实例。如果我们使用了JpaTemplate类,我们将受益于 Spring 的异常转换。对于 Spring 框架附带的所有 ORM 模板类,Spring 会自动将特定于技术的(已检查)异常转换为 Spring ORM 包中的标准运行时异常层次结构(根为org.springframework.dao.DataAccessException)。这样,您可以以标准方式处理代码中的不同异常。我们选择在这里只注入实体管理器,因此我们需要重新启用异常转换。通过注册一个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框架还支持许多其他的数据持久化选项。这些选项都遵循与本文中建立的支持相同的通用模板,因此如果您决定使用它们,将会很容易上手。本文没有涵盖Spring对许多其他ORM解决方案(如Hibernate、JDO、TopLink等)提供的支持。例如,关于Hibernate支持的更多信息,您可以查看Alef关于这个主题的精彩文章。本文也没有讨论Spring Data项目提供的广泛的NoSQL支持。本文中的示例逐步简化,越来越依赖约定而不是配置。随着示例向上抽象堆栈移动,始终有一种方法可以利用底层API的全部功能。

获取Spring新闻通讯

关注Spring新闻通讯

订阅

领先一步

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

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部