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 社群所有即將舉行的活動。

檢視所有