Spring5 高级教程(六)
原文:Pro Spring 5
九、事务管理
事务是构建可靠的企业应用的最关键部分之一。最常见的事务类型是数据库操作。在典型的数据库更新操作中,数据库事务开始,数据被更新,然后事务被提交或回滚,这取决于数据库操作的结果。然而,在许多情况下,根据应用的需求和应用需要与之交互的后端资源(如 RDBMS、面向消息的中间件、ERP 系统等等),事务管理可能要复杂得多。
在 Java 应用开发的早期(在 JDBC 创建之后,但在 JEE 标准或像 Spring 这样的应用框架可用之前),开发人员以编程方式控制和管理应用代码中的事务。当 JEE,更具体地说,EJB 标准可用时,开发人员能够使用容器管理的事务(CMTs)以声明的方式管理事务。但是 EJB 部署描述符中复杂的事务声明很难维护,并且给事务处理带来了不必要的复杂性。一些开发人员喜欢对事务有更多的控制,并选择 bean 管理的事务(BMTs)来以编程的方式管理事务。然而,用 Java 事务 API (JTA)编程的复杂性也阻碍了开发人员的生产力。
正如在第五章中所讨论的,事务管理是一个横切关注点,不应该编码在业务逻辑中。实现事务管理的最合适的方式是允许开发人员以声明的方式定义事务需求,并让 Spring、JEE 或 AOP 等框架代表我们编织事务处理逻辑。在本章中,我们将讨论 Spring 如何帮助简化事务处理逻辑的实现。Spring 同时支持声明式和编程式事务管理。
Spring 为声明性事务提供了出色的支持,这意味着您不需要用事务管理代码来混淆您的业务逻辑。您所要做的就是声明那些必须参与事务的方法(在类或层中),以及事务配置的细节,Spring 将负责处理事务管理。更具体地说,本章包括以下内容:
- Spring 事务抽象层:我们讨论 Spring 事务抽象类的基本组件,并解释如何使用这些类来控制事务的属性。
- 声明式事务管理:我们向您展示了如何使用 Spring 和普通 Java 对象来实现声明式事务管理。我们提供了使用 XML 配置文件和 Java 注释进行声明式事务管理的例子。
- 编程式事务管理:尽管编程式事务管理并不经常使用,但我们解释了如何使用 Spring 提供的
TransactionTemplate类,它让您可以完全控制事务管理代码。 - 使用 JTA 的全局事务:对于需要跨越多个后端资源的全局事务,我们展示了如何使用 JTA 在 Spring 中配置和实现全局事务。
探索 Spring 事务抽象层
在开发应用时,无论您是否选择使用 Spring,您都必须在使用事务时做出一个基本的选择,即是使用全局事务还是本地事务。本地事务特定于单个事务性资源(例如,JDBC 连接),而全局事务由容器管理,可以跨多个事务性资源。
交易类型
本地事务易于管理,如果应用中的所有操作只需要与一个事务性资源交互(如 JDBC 事务),使用本地事务就足够了。但是,如果您没有使用 Spring 之类的应用框架,那么您需要编写大量的事务管理代码,并且如果将来需要跨多个事务资源扩展事务的范围,那么您必须放弃本地事务管理代码,重新编写它以使用全局事务。
在 Java 世界中,全局事务是通过 JTA 实现的。在这个场景中,一个 JTA 兼容的事务管理器通过各自的资源管理器连接到多个事务资源,这些资源管理器能够通过 XA 协议(一个定义分布式事务的开放标准)与事务管理器通信,两阶段提交(2PC)机制用于确保所有后端数据源都被更新或一起回滚。如果任何一个后端资源失败,整个事务将回滚,因此对其他资源的更新也将回滚。
图 9-1 显示了与 JTA 的全球交易的高级视图。如图 9-1 所示,一个全局事务(通常也称为分布式事务)有四个主要参与方。第一方是后端资源,比如 RDBMS、消息中间件、企业资源规划(ERP)系统等等。
图 9-1。
Overview of global transactions with JTA
第二方是资源管理器,一般由后端资源厂商提供,负责与后端资源交互。例如,当连接到 MySQL 数据库时,我们需要与 MySQL 的 Java 连接器提供的MysqlXADataSource类进行交互。其他后端资源(例如 MQ、ERP 等等)也提供了它们的资源管理器。
第三方是 JTA 事务管理器,它负责管理、协调事务状态,并与参与事务的所有资源管理器同步事务状态。使用 XA 协议,这是一种广泛用于分布式事务处理的开放标准。JTA 事务管理器还支持 2PC,因此所有的更改将一起提交,如果任何资源更新失败,整个事务将回滚,导致没有任何资源被更新。整个机制由 Java 事务服务(JTS)规范指定。
最后一个组件是应用。应用本身或者运行应用的底层容器或 Spring 框架管理事务(开始、提交、回滚事务,等等)。同时,应用通过 JEE 定义的各种标准与底层后端资源进行交互。如图 9-1 所示,应用通过 JDBC 连接到 RDBMS,通过 JMS 连接到 MQ,通过 Java EE 连接器架构(JCA)连接到 ERP 系统。
所有成熟的符合 JEE 标准的应用服务器(例如,JBoss、WebSphere、WebLogic 和 GlassFish)都支持 JTA,在这些服务器中,可以通过 JNDI 查找来处理事务。至于独立的应用或 web 容器(例如,Tomcat 和 Jetty),也存在开放源代码和商业解决方案,在这些环境中提供对 JTA/XA 的支持(例如,Atomikos、Java Open Transaction Manager[JOTM]和 Bitronix)。
PlatformTransactionManager 的实现
在 Spring 中,PlatformTransactionManager接口使用TransactionDefinition和TransactionStatus接口来创建和管理事务。这些接口的实际实现必须详细了解事务管理器。
图 9-2 显示了PlatformTransactionManager在 Spring 中的实现。
图 9-2。
PlatformTransactionManager implementations as of Spring
Spring 为PlatformTransactionManager接口提供了丰富的实现。CciLocalTransactionManager类支持 JEE、JCA 和公共客户端接口(CCI)。DataSourceTransactionManager类用于一般的 JDBC 连接。对于 ORM 端,有很多实现,包括 JPA(JpaTransactionManager类) 1 和 Hibernate 5 ( HibernateTransactionManager)。 2 对于 JMS,实现通过JmsTransactionManager类支持 JMS 2.0。 3 对于 JTA,通用的实现类是JtaTransactionManager。Spring 还提供了几个特定于特定应用服务器的 JTA 事务管理器类。这些类为 WebSphere(WebSphereUowTransactionManager类)、WebLogic(WebLogicJtaTransactionManager类)和 Oracle OC4J(OC4JJtaTransactionManager类)提供了本地支持。
分析事务属性
在这一节中,我们将讨论 Spring 支持的事务属性,重点是作为后端资源与 RDBMS 进行交互。
事务有四个众所周知的 ACID 属性(原子性、一致性、隔离性和持久性),事务资源负责维护事务的这些方面。您无法控制事务的原子性、一致性和持久性。但是,您可以控制事务传播和超时,以及配置事务是否应该是只读的和指定隔离级别。
Spring 将所有这些设置封装在一个TransactionDefinition接口中。该接口用于 Spring 中事务支持的核心接口,即PlatformTransactionManager接口,其实现在特定平台上执行事务管理,如 JDBC 或 JTA。核心方法PlatformTransactionManager.getTransaction()将一个TransactionDefinition接口作为参数,并返回一个TransactionStatus接口。TransactionStatus接口用于控制交易执行,更具体地说是设置交易结果,检查交易是否完成或是否是新交易。
事务定义接口
正如我们前面提到的,TransactionDefinition接口控制事务的属性。让我们更详细地看看这里显示的TransactionDefinition接口,并描述它的方法:
package org.springframework.transaction;
import java.sql.Connection;
public interface TransactionDefinition {
// Variable declaration statements omitted
...
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
String getName();
}
这个接口的简单而明显的方法是getTimeout(),它返回事务必须完成的时间(以秒为单位)和isReadOnly(),它指示事务是否是只读的。事务管理器实现可以使用这个值来优化执行,并进行检查以确保事务只执行读操作。getName()方法返回事务的名称。
另外两种方法getPropagationBehavior()和getIsolationLevel(),需要更详细的讨论。我们从getIsolationLevel()开始,它控制其他事务看到的数据变化。表 9-1 列出了您可以使用的事务隔离级别,并解释了在当前事务中其他事务可以访问的更改。隔离级别表示为在TransactionDefinition接口中定义的静态值。
表 9-1。
Transaction Isolation Levels
| 隔离级别 | 描述 | | --- | --- | | `ISOLATION_DEFAULT` | 基础数据存储区的默认隔离级别。 | | `ISOLATION_READ_UNCOMMITTED` | 最低水平的隔离;它几乎不是一个事务,因为它允许这个事务看到被其他未提交的事务修改的数据。 | | `ISOLATION_READ_COMMITTED` | 大多数数据库中的默认级别;它确保其他事务不能读取其他事务未提交的数据。但是,由一个事务读取的数据可以由其他事务更新。 | | `ISOLATION_REPEATABLE_READ` | 比`ISOLATION_READ_COMMITTED`更严格;它确保一旦选择了数据,您至少可以再次选择相同的集合。但是,如果其他事务插入了新数据,您仍然可以选择新插入的数据。 | | `ISOLATION_SERIALIZABLE` | 最昂贵、最可靠的隔离级别;所有事务都被视为是一个接一个执行的。 |选择适当的隔离级别对于数据的一致性很重要,但是做出这些选择会对性能产生很大的影响。最高隔离级别ISOLATION_SERIALIZABLE的维护成本特别高。
getPropagationBehavior()方法根据是否有活动的事务来指定事务调用会发生什么。表 9-2 描述了该方法的数值。传播类型被表示为在TransactionDefinition接口中定义的静态值。
表 9-2。
Transaction Isolation Levels
| 传播类型 | 描述 | | --- | --- | | `PROPAGATION_REQUIRED` | 支持已经存在的事务。如果没有事务,它将开始一个新的事务。 | | `PROPAGATION_SUPPORTS` | 支持已经存在的事务。如果没有事务,它将以非事务方式执行。 | | `PROPAGATION_MANDATORY` | 支持已经存在的事务。如果没有活动事务,将引发异常。 | | `PROPAGATION_REQUIRES_NEW` | 总是开始一个新的事务。如果活动事务已经存在,它将被挂起。 | | `PROPAGATION_NOT_SUPPORTED` | 不支持活动事务的执行。总是以非事务方式执行,并挂起任何现有的事务。 | | `PROPAGATION_NEVER` | 即使存在活动事务,也总是以非事务方式执行。如果存在活动事务,将引发异常。 | | `PROPAGATION_NESTED` | 如果活动事务存在,则在嵌套事务中运行。如果没有活动的事务,则如同设置了`PROPAGATION_REQUIRED`一样执行。 |TransactionStatus 接口
接下来显示的TransactionStatus接口允许事务管理器控制事务的执行。该代码可以检查事务是新事务还是只读事务,并且可以启动回滚。
package org.springframework.transaction;
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
TransactionStatus接口的方法是不言自明的;最值得注意的是setRollbackOnly(),它会导致回滚并结束活动事务。
hasSavePoint()方法返回事务内部是否带有保存点(也就是说,事务是作为基于保存点的嵌套事务创建的)。如果适用的话,flush()方法是到数据存储的底层会话(例如,当使用 Hibernate 时)。isCompleted()方法返回事务是否已经结束(即提交或回滚)。
示例代码的示例数据模型和基础结构
本节概述了我们的事务管理示例中使用的数据模型和基础设施。我们使用 JPA 和 Hibernate 作为实现数据访问逻辑的持久层。此外,Spring Data JPA 及其存储库抽象用于简化基本数据库操作的开发。
创建一个带有依赖项的简单 Spring JPA 项目
让我们从创建项目开始。因为我们使用的是 JPA,所以我们还需要为本章中的例子添加所需的依赖项。
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M3'
//logging libs
slf4jVersion = '1.7.25'
logbackVersion = '1.2.3'
guavaVersion = '21.0'
junitVersion = '4.12'
aspectjVersion = '1.9.0.BETA-5'
//database library
h2Version = '1.4.194'
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
atomikosVersion = '4.0.0M4'
spring = [
context : "org.springframework:spring-context:$springVersion",
aop : "org.springframework:spring-aop:$springVersion",
aspects : "org.springframework:spring-aspects:$springVersion",
tx : "org.springframework:spring-tx:$springVersion",
jdbc : "org.springframework:spring-jdbc:$springVersion",
contextSupport: "org.springframework:spring-context-support:$springVersion",
orm : "org.springframework:spring-orm:$springVersion",
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
test : "org.springframework:spring-test:$springVersion"
]
hibernate = [
...
em : "org.hibernate:hibernate-entitymanager:$hibernateVersion",
tx : "com.atomikos:transactions-hibernate4:$atomikosVersion"
]
boot = [
...
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
starterJpa :
"org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
misc = [
...
slf4jJcl : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
logback : "ch.qos.logback:logback-classic:$logbackVersion",
aspectjweaver: "org.aspectj:aspectjweaver:$aspectjVersion",
lang3 : "org.apache.commons:commons-lang3:3.5",
guava : "com.google.guava:guava:$guavaVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter09/build.gradle
dependencies {
//we specify these dependencies for all submodules, except
// the boot module, that defines its own
if !project.name.contains"boot" {
//we exclude transitive dependencies, because spring-data
//will take care of these
compile spring.contextSupport {
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
//we exclude the 'hibernate' transitive dependency
//to have control over the version used
compile hibernate.tx {
exclude group: 'org.hibernate', module: 'hibernate'
}
compile spring.orm, spring.context, misc.slf4jJcl,
misc.logback, db.h2, misc.lang3,
hibernate.em
}
testCompile testing.junit
}
为了在我们修改事务属性时观察示例代码的详细行为,让我们也在logback中打开DEBUG级别的日志记录。下面的代码片段显示了logback.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %thread %-5level %logger{5} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.apress.prospring5.ch8" level="debug"/>
<logger name="org.springframework.transaction" level="info"/>
<logger name="org.hibernate.SQL" level="debug"/>
<root level="info">
<appender-ref ref="console" />
</root>
</configuration>
样本数据模型和公共类
为了简单起见,我们将只使用两个表,即我们在关于数据访问的章节中使用的SINGER和ALBUM表。不需要 SQL 脚本来创建表格,因为您可以使用 Hibernate 属性hibernate.hbm2ddl.auto并将其设置为create-drop,这样每次测试时我们都会有一个干净的运行。表格将基于Singer和Album实体生成。下面描述了带有注释字段的片段:
//Singer.java
package com.apress.prospring5.ch9.entities;
...
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_ALL, query="select s from Singer s"),
@NamedQuery(name=Singer.COUNT_ALL, query="select count(s) from Singer s")
})
public class Singer implements Serializable {
public static final String FIND_ALL = "Singer.findAll";
public static final String COUNT_ALL = "Singer.countAll";
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL, orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
...
}
/Album.java
package com.apress.prospring5.ch9.entities;
...
@Entity
@Table(name = "album")
public class Album implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column
private String title;
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
private Date releaseDate;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
private Singer singer;
...
}
这两个类将被隔离在一个名为base-dao的项目中,该项目将成为所有事务项目的依赖项。除了实体之外,这个项目中还定义了存储库接口。我们将在稍后的课程中对它们进行描述。还需要一个配置类来定义DataSource bean。这里显示了配置类,出于实用和教育目的,将直接使用数据库凭证、驱动程序和 URL,而不是从外部文件读取。(不过,在生产中你永远不会遇到这种情况。我们希望。)
package com.apress.prospring5.ch9.config;
...
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch9.repos"})
public class DataJpaConfig {
private static Logger logger =
LoggerFactory.getLogger(DataJpaConfig.class);
@SuppressWarnings("unchecked")
@Bean
public DataSource dataSource() {
try {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
Class<? extends Driver> driver =
(Class<? extends Driver>) Class.forName("org.h2.Driver");
dataSource.setDriverClass(driver);
dataSource.setUrl("jdbc:h2:musicdb");
dataSource.setUsername("prospring5");
dataSource.setPassword("prospring5");
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
//hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
定义了嵌入式 H2 数据库。凭证直接在代码中设置,DataSource实现是SimpleDriverDataSource,它被设计成只用于简单的、测试的或教育的应用。
目前,使用注释是 Spring 中定义事务需求最常见的方式。主要的好处是,事务需求和详细的事务属性(超时、隔离级别、传播行为等等)都是在代码本身中定义的,这使得应用更容易跟踪和维护。配置也是使用注释和 Java 配置类来完成的。为了使用 XML 配置在 Spring 中启用对事务管理的注释支持,我们需要在 XML 配置文件中添加<tx:annotation-driven>标记。在下面的配置片段中,您可以看到事务性配置和事务性匹配名称空间的片段。如果您感兴趣,可以在项目中找到完整的配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd ...">
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven />
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
<context:component-scan
base-package="com.apress.prospring5.ch9" />
<jpa:repositories base-package="com.apress.prospring5.ch9.repos"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"/>
</beans>
因为我们使用 JPA,所以您定义了JpaTransactionManager bean。<tx:annotation-driven>标签指定我们使用注释进行事务管理。这个简单的定义指示 Spring 寻找一个名为transactionManager的类型为PlatformTransactionManager的 bean。如果事务 bean 的名称不同,比如说customTransactionManager,元素定义必须用属性transaction-manager声明,该属性必须接收事务管理 bean 的名称作为值。
<tx:annotation-driven transaction-manager="customTransactionManager"/>
然后定义了EntityManagerFactory bean,后面跟着<context:component-scan>标记来扫描服务层类。最后,<jpa:repositories>标签用于启用 Spring Data JPA 的存储库抽象。这个元素在DataJpaConfiguration类中被替换为@EnableJpaRepositories注释。
在专业环境中,将持久性配置(DAO)与事务性配置(服务)分开是一种常见的做法。这就是为什么前面介绍的 XML 内容在 Java 配置中被分成两个配置类。前面介绍的DataJpaConfig只包含数据访问 bean,接下来描述的ServicesConfig只包含与事务管理相关的 bean:
package com.apress.prospring5.ch9.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.apress.prospring5.ch9")
public class ServicesConfig {
@Autowired EntityManagerFactory entityManagerFactory;
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
}
对于SingerService接口的实现,我们首先用SingerService接口中所有方法的空实现来创建这个类。让我们首先实现SingerService.findAll()方法。下面的代码片段显示了实现了findAll()方法的SingerServiceImpl类:
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
}
当使用基于注释的事务管理时,我们需要处理的唯一注释是@Transactional。在前面的代码片段中,@Transactional注释应用于类级别,这意味着默认情况下,Spring 将确保在执行类中的每个方法之前存在一个事务。@Transactional注释支持许多属性,您可以提供这些属性来覆盖默认行为。表 9-3 显示了可用的属性,以及可能值和默认值。
表 9-3。
Attributes for the @Transactional Annotation
| 属性名 | 缺省值 | 可能的值 | | --- | --- | --- | | `propagation` | `Propagation.REQUIRED` | `Propagation.REQUIRED``Propagation.SUPPORTS``Propagation.MANDATORY``Propagation.REQUIRES_NEW``Propagation.NOT_SUPPORTED``Propagation.NEVER` | | `isolation` | `Isolation.DEFAULT`(底层资源的默认隔离级别) | `Isolation.DEFAULT``Isolation.READ_UNCOMMITTED``Isolation.READ_COMMITTED``Isolation.REPEATABLE_READ` | | `timeout` | `TransactionDefinition.TIMEOUT_DEFAULT`(底层资源的默认事务超时,以秒为单位) | 大于零的整数值;指示超时的秒数 | | `readOnly` | 错误的 | { `true`,`false` } | | `rollbackFor` | 将回滚事务的异常类 | 不适用的 | | `rollbackForClassName` | 将回滚事务的异常类名 | 不适用的 | | `noRollbackFor` | 不会回滚事务的异常类 | 不适用的 | | `noRollbackForClassName` | 不会回滚事务的异常类名 | 不适用的 | | `value` | `""`(指定交易的限定符值) | 不适用的 |因此,基于表 9-3 ,没有任何属性的@Transactional注释意味着需要事务传播,隔离是默认的,超时是默认的,模式是读写。对于之前介绍的findAll()方法,该方法用@Transactional(readOnly=true)标注。这将覆盖在类级别应用的默认注释,所有其他属性不变,但是事务被设置为只读。下面的代码片段显示了findAll()方法的测试程序:
package com.apress.prospring5.ch9;
import java.util.List;
import com.apress.prospring5.ch9.config.DataJpaConfig;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class TxAnnotationDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
ctx.close();
}
}
启用适当的日志记录,换句话说,<logger name="org.springframework.orm.jpa" level="debug"/>,您将能够在日志中看到与事务处理相关的消息。运行该程序会产生以下缩减的输出(有关完整的详细信息,请参见控制台中的调试日志):
DEBUG o.s.o.j.JpaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.findAll]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''
DEBUG o.s.o.j.JpaTransactionManager - Participating in existing transaction
Hibernate: select singer0_.ID as ID1_1_, singer0_.BIRTH_DATE as BIRTH_DA2_1_,
singer0_.FIRST_NAME as FIRST_NA3_1_, singer0_.LAST_NAME as LAST_NAM4_1_,
singer0_.VERSION as VERSION5_1_ from singer singer0_
DEBUG o.s.o.j.JpaTransactionManager - Closing JPA EntityManager
[...] after transaction
DEBUG o.s.o.j.JpaTransactionManager - Initiating transaction commit
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
如前面的输出所示,为了清楚起见,删除了不相关的输出语句。首先,在运行findAll()方法之前,Spring 的JpaTransactionManager创建了一个具有默认属性的新事务(名称等于带有方法名称的完全限定类名),但是事务被设置为只读,正如在方法级@Transactional注释中定义的那样。然后,提交查询,在完成且没有任何错误的情况下,提交事务。JpaTransactionManager处理事务的创建和提交操作。
让我们继续执行更新操作。我们需要在SingerServiceImpl接口中实现findById()和save()方法。以下代码片段显示了实现:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Override
@Transactional(readOnly = true)
public Singer findById(Long id) {
return singerRepository.findById(id).get();
}
@Override
public Singer save(Singer singer) {
return singerRepository.save(singer);
}
}
findById()方法也用@Transactional(readOnly=true)进行了注释。一般来说,readOnly=true属性应该应用于所有的查找器方法。主要原因是大多数持久性提供者会对只读事务执行一定程度的优化。例如,Hibernate 不会维护从打开只读的数据库中检索的托管实例的快照。
对于save()方法,我们简单地调用CrudRepository.save()方法,并且不提供任何注释。这意味着将使用类级别的注释,这是一个读写事务。让我们修改用于测试save()方法的TxAnnotationDemo类,如下面的代码所示:
package com.apress.prospring5.ch9;
...
public class TxAnnotationDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
Singer singer = singerService.findById(1L);
singer.setFirstName("John Clayton");
singer.setLastName("Mayer");
singerService.save(singer);
System.out.println("Singer saved successfully: " + singer);
ctx.close();
}
}
检索 ID 为 1 的Singer对象,然后更新名字并保存到数据库中。运行代码会产生以下相关输出:
Singer saved successfully: Singer - Id: 1, First name: John Clayton,
Last name: Mayer, Birthday: 1977-10-16
save()方法获取从类级@Transactional注释继承的默认属性。更新操作完成后,Spring 的JpaTransactionManager会触发一个事务提交,这会导致 Hibernate 刷新持久性上下文,并将底层的 JDBC 连接提交给数据库。最后,我们来看看countAll()的方法。我们将研究这种方法的两种事务配置。虽然CrudRepository.count()方法可以达到目的,但我们不会使用那种方法。相反,出于演示的目的,我们将实现另一个方法,主要是因为 Spring Data 中由CrudRepository接口定义的方法已经用适当的事务属性进行了标记。
下面的代码片段显示了在SingerRepository接口中定义的新方法countAllSingers():
package com.apress.prospring5.ch9.repos;
import com.apress.prospring5.ch9.entities.Singer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface SingerRepository extends
CrudRepository<Singer, Long> {
@Query("select count(s) from Singer s")
Long countAllSingers();
}
对于新的countAllSingers()方法,应用了@Query注释,其值等于计算联系人数量的 JPQL 语句。下面的代码片段显示了SingerServiceImpl类中countAll()方法的实现:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly=true)
public long countAll() {
return singerRepository.countAllSingers();
}
}
注释与其他 finder 方法相同。要测试这个方法,只需在TxAnnotationDemo类的main()方法中添加System.out.println("Singer count: " + contactService.countAll());,并观察控制台。如果您看到类似Singer count: 3的消息,则该方法执行正确。
在这个输出中,您可以看到countAll()的事务是用只读等于 true 创建的,正如所预期的那样。但是对于countAll()函数,我们根本不想让它加入到事务中。我们不需要由底层 JPA EntityManager来管理结果。相反,我们只想得到计数并忘记它。在这种情况下,我们可以将事务传播行为覆盖到Propagation.NEVER。下面的方法显示了修改后的countAll()方法:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
...
@Override
@Transactional(propagation = Propagation.NEVER)
public long countAll() {
return singerRepository.countAllSingers();
}
}
再次运行测试代码,您会发现调试输出中不会为countAll()方法创建事务。
本节介绍了您在日常事务处理中需要处理的一些主要配置。对于特殊情况,您可能需要为特定异常定义超时、隔离级别、回滚(或不回滚)等等。
Spring 的
JpaTransactionManager不支持自定义隔离级别。相反,它总是使用基础数据存储区的默认隔离级别。如果您使用 Hibernate 作为 JPA 服务提供者,您可以使用一种变通方法:扩展HibernateJpaDialect类以支持定制的隔离级别。
使用 AOP 配置进行事务管理
另一种常见的声明式事务管理方法是使用 Spring 的 AOP 支持。在 Spring 版本 2 之前,我们需要使用TransactionProxyFactoryBean类来定义 Spring beans 的事务需求。然而,从版本 2 开始,Spring 通过引入aop名称空间和使用通用 AOP 配置技术来定义事务需求,提供了一种更简单的方法。当然,在引入注释之后,这种配置事务管理的方式也受到了反对。但是知道它的存在是有用的,以防万一你可能需要包装一个不属于你的项目的事务代码,并且你不能编辑它来添加@Transaction注释。
在下面的配置片段中,上一节中的示例是使用 XML 配置的,并使用了aop名称空间:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean name="dataJpaConfig"
class="com.apress.prospring5.ch9.config.DataJpaConfig" />
<aop:config>
<aop:pointcut id="serviceOperation" expression=
"execution(* com.apress.prospring5.ch9.*ServiceImpl.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
<tx:method name="count*" propagation="NEVER"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<context:component-scan
base-package="com.apress.prospring5.ch9.services" />
</beans>
该配置与本节开始时介绍的 XML 配置非常相似。基本上,<tx:annotation-driven>标记被移除,而<context:component-scan>标记被修改为我们用于声明性事务管理的包名。最重要的标签是<aop:config>和<tx:advice>。
在<aop:config>标签下,为服务层内的所有操作定义了一个切入点(即com.apress.prospring5.ch9.services包下的所有实现类)。该通知引用了 ID 为txAdvice的 bean,它是由<tx:advice>标记定义的。在<tx:advice>标签中,我们为想要参与事务的各种方法配置了事务属性。如标签所示,您指定所有的 finder 方法(带有前缀find的方法)将是只读的,我们指定 count 方法(带有前缀count的方法)将不参与事务。对于其余的方法,将应用默认的事务行为。该配置与注释示例中的配置相同。
因为事务管理是通过aop显式完成的,所以SingerServiceImpl类或其中的方法不再需要@Transactional注释。
要测试前面的配置,可以使用下面的类:
package com.apress.prospring5.ch9;
import java.util.List;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.support.GenericXmlApplicationContext;
public class TxDeclarativeDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/tx-declarative-app-context.xml");
ctx.refresh();
SingerService singerService = ctx.getBean(SingerService.class);
// Testing findAll()
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
// Testing save()
Singer singer = singerService.findById(1L);
singer.setFirstName("John Clayton");
singerService.save(singer);
System.out.println("Singer saved successfully: " + singer);
// Testing countAll()
System.out.println("Singer count: " + singerService.countAll());
ctx.close();
}
}
我们将让您测试程序,并观察 Spring 和 Hibernate 执行的与事务相关的操作的输出。基本上,它们与注释示例相同。
使用程序化事务
第三种选择是以编程方式控制事务行为。在这种情况下,我们有两个选择。第一种是在 bean 中注入一个PlatformTransactionManager实例,直接与事务管理器交互。另一个选择是使用 Spring 提供的TransactionTemplate类,这大大简化了您的工作。在本节中,我们将演示如何使用TransactionTemplate class。为了简单起见,我们集中于实现SingerServiceImpl.countAll()方法。下面的代码片段描述了为使用编程事务而修改的ServiceConfig类:
package com.apress.prospring5.ch9.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManagerFactory;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch9")
public class ServicesConfig {
@Autowired EntityManagerFactory entityManagerFactory;
@Bean
public TransactionTemplate transactionTemplate() {
TransactionTemplate tt = new TransactionTemplate();
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
tt.setTimeout(30);
tt.setTransactionManager(transactionManager());
return tt;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
}
这里删除了 AOP 事务通知。此外,使用org.springframework.transaction.support.TransactionTemplate类定义了一个带有一些事务属性的transactionTemplate bean。此外,@EnableTransactionManagement也被删除了,因为事务管理现在不是显式完成的。让我们看看countAll()方法的实现,如下所示:
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerService")
@Repository
public class SingerServiceImpl implements SingerService {
@Autowired
private SingerRepository singerRepository;
@Autowired
private TransactionTemplate transactionTemplate;
@PersistenceContext
private EntityManager em;
@Override
public long countAll() {
return transactionTemplate.execute(
transactionStatus -> em.createNamedQuery(Singer.COUNT_ALL,
Long.class).getSingleResult());
}
}
这里的TransactionTemplate类是从 Spring 注入的。然后在countAll()方法中,调用TransactionTemplate.execute()方法,传入实现TransactionCallback<T>接口的内部类的声明。然后doInTransaction()被期望的逻辑覆盖。逻辑将在由transactionTemplate bean 定义的属性中运行。您没有清楚地看到前面代码片段中所有内容的原因是因为使用了 Java 8 lambda 表达式。以下代码是前面方法的扩展版本(因为它是在 lambda 表达式引入之前编写的):
public long countAll() {
return transactionTemplate.execute(new TransactionCallback<Long>() {
public Long doInTransaction(TransactionStatus transactionStatus) {
return em.createNamedQuery(Singer.COUNT_ALL,
Long.class).getSingleResult();
}
});
}
以下代码片段显示了测试程序:
package com.apress.prospring5.ch9;
import com.apress.prospring5.ch9.config.DataJpaConfig;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class TxProgrammaticDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
System.out.println("Singer count: " + singerService.countAll());
ctx.close();
}
}
我们将让您来运行程序并观察结果。尝试调整事务属性,看看在countAll()方法的事务处理中会发生什么。
关于交易管理的思考
那么,在讨论了实现事务管理的各种方法之后,您应该使用哪一种呢?在所有情况下都推荐使用声明性方法,并且应该尽可能避免在代码中实现事务管理。大多数情况下,当您发现有必要在应用中编写事务控制逻辑时,这是由于糟糕的设计,在这种情况下,您应该考虑将您的逻辑重构为可管理的部分,并在这些部分上以声明方式定义事务需求。
对于声明性方法,使用 XML 和使用注释各有利弊。一些开发人员不喜欢在代码中声明事务需求,而另一些开发人员则喜欢使用注释以便于维护,因为您可以在代码中看到所有的事务需求声明。同样,让应用需求驱动您的决策,一旦您的团队或公司已经标准化了方法,就要保持与配置风格的一致。
Spring 的全球事务
许多企业 Java 应用需要访问多个后端资源。例如,从外部业务伙伴收到的一条客户信息可能需要更新多个系统(CRM、ERP 等)的数据库。有些人甚至需要为公司内对客户信息感兴趣的所有其他应用生成一条消息,并通过 JMS 将其发送到 MQ 服务器。跨越多个后端资源的事务被称为全局(或分布式)事务。
全局事务的一个主要特征是保证原子性,这意味着所涉及的资源都被更新,或者都不被更新。这包括应该由事务管理器处理的复杂的协调和同步逻辑。在 Java 世界中,JTA 是实现全局事务的事实上的标准。
Spring 支持 JTA 事务和本地事务,并在业务代码中隐藏了这种逻辑。在这一节中,我们将演示如何通过在 Spring 中使用 JTA 来实现全局事务。
实施 JTA 样本的基础设施
我们使用的表格与本章前面的示例中的表格相同。然而,嵌入式 H2 数据库并不完全支持 XA(至少在编写本文时是这样),所以在本例中,我们使用 MySQL 作为后端数据库。
我们还想展示如何在独立应用或 web 容器环境中实现与 JTA 的全局事务。因此,在这个例子中,我们使用 Atomikos ( www.atomikos.com/Main/TransactionsEssentials ),这是一个广泛用于非 JEE 环境的开源 JTA 事务管理器。
为了展示全局事务是如何工作的,我们至少需要两个后端资源。为了简单起见,我们将使用一个 MySQL 数据库和两个 JPA 实体管理器来模拟用例。效果是一样的,因为不同的后端数据库有多个 JPA 持久性单元。
在 MySQL 数据库中,我们创建了两个模式和相应的用户,如以下 DDL 脚本所示:
CREATE USER 'prospring5_a'@'localhost' IDENTIFIED BY 'prospring5_a';
CREATE SCHEMA MUSICDB_A;
GRANT ALL PRIVILEGES ON MUSICDB_A . * TO 'prospring5_a'@'localhost';
PRIVILEGES;
CREATE USER 'prospring5_b'@'localhost' IDENTIFIED BY 'prospring5_b';
CREATE SCHEMA MUSICDB_B;
GRANT ALL PRIVILEGES ON MUSICDB_B . * TO 'prospring5_b'@'localhost';
PRIVILEGES;
设置完成后,我们可以继续进行 Spring 配置和实现。
实施与 JTA 的全球交易
首先我们来看看 Spring 的配置。下面的代码片段描述了声明访问两个数据库所需的 beans 的XAJpaConfig配置类:
package com.apress.prospring5.ch9.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.Driver;
import java.util.Properties;
@Configuration
@EnableJpaRepositories
public class XAJpaConfig {
private static Logger logger = LoggerFactory.getLogger(XAJpaConfig.class);
@SuppressWarnings("unchecked")
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSourceA() {
try {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("XADBMSA");
dataSource.setXaDataSourceClassName(
"com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(xaAProperties());
dataSource.setPoolSize(1);
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties xaAProperties() {
Properties xaProp = new Properties();
xaProp.put("databaseName", "musicdb_a");
xaProp.put("user", "prospring5_a");
xaProp.put("password", "prospring5_a");
return xaProp;
}
@SuppressWarnings("unchecked")
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSourceB() {
try {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("XADBMSB");
dataSource.setXaDataSourceClassName(
"com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(xaBProperties());
dataSource.setPoolSize(1);
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties xaBProperties() {
Properties xaProp = new Properties();
xaProp.put("databaseName", "musicdb_b");
xaProp.put("user", "prospring5_b");
xaProp.put("password", "prospring5_b");
return xaProp;
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.transaction.factory_class",
"org.hibernate.transaction.JTATransactionFactory");
hibernateProp.put("hibernate.transaction.jta.platform",
"com.atomikos.icatch.jta.hibernate4.AtomikosPlatform");
// required by Hibernate 5
hibernateProp.put("hibernate.transaction.coordinator_class", "jta");
hibernateProp.put("hibernate.dialect",
"org.hibernate.dialect.MySQL5Dialect");
// this will work only if users/schemas are created first,
// use ddl.sql script for this
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory emfA() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSourceA());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setPersistenceUnitName("emfA");
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
@Bean
public EntityManagerFactory emfB() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSourceB());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setPersistenceUnitName("emfB");
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
}
配置长但不太复杂。首先,定义两个DataSourcebean 来表示两个数据库资源。bean 名称是dataSourceA和dataSourceB,它们分别连接到模式musicdb_a和musicdb_a。两个DataSourcebean 都使用类com.atomikos.jdbc.AtomikosDataSourceBean,该类支持 XA 兼容的DataSource,在两个 bean 的定义中,定义了 MySQL 的 XA DataSource实现类:com.mysql.cj.jdbc.MysqlXADataSource,它是 MySQL 的资源管理器。然后,提供数据库连接信息。注意,poolSize属性定义了 Atomikos 需要维护的连接池中的连接数。这不是强制性的。但是,如果没有提供该属性,Atomikos 将使用默认值 1。
然后,定义两个EntityManagerFactorybean,命名为emfA和emfB。常见的 JPA 属性一起包装在hibernateProperties bean 中。两个 beans 唯一的区别就是被注入了相应的数据源(即dataSourceA注入了emfA,而dataSourceB注入了emfB)。因此,emfA将通过dataSourceA bean 连接到 MySQL 的prospring5_a模式,而emfB将通过dataSourceB bean 连接到prospring5_b模式。看看emfBase bean 中的属性hibernate.transaction.factory_class和hibernate.transaction.jta.platform。这两个属性非常重要,因为 Hibernate 使用它们来查找底层的UserTransaction和TransactionManagerbean,以参与它管理的全局事务的持久性上下文。同样重要的是让 Hibernate 4 的 Atomikos 类与 Hibernate 5 一起工作所需的hibernate.transaction.coordinator_class。 4
下面的代码片段描述了ServicesConfig,它声明了用于实现全局事务管理的 beans:
package com.apress.prospring5.ch9.config;
import com.atomikos.icatch.config.UserTransactionService;
import com.atomikos.icatch.config.UserTransactionServiceImp;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.apress.prospring5.ch9.services")
public class ServicesConfig {
private Logger logger = LoggerFactory.getLogger(ServicesConfig.class);
@Bean(initMethod = "init", destroyMethod = "shutdownForce")
public UserTransactionService userTransactionService(){
Properties atProps = new Properties();
atProps.put("com.atomikos.icatch.service",
"com.atomikos.icatch.standalone.UserTransactionServiceFactory");
return new UserTransactionServiceImp(atProps);
}
@Bean (initMethod = "init", destroyMethod = "close")
@DependsOn("userTransactionService")
public UserTransactionManager atomikosTransactionManager(){
UserTransactionManager utm = new UserTransactionManager();
utm.setStartupTransactionService(false);
utm.setForceShutdown(true);
return utm;
}
@Bean
@DependsOn("userTransactionService")
public UserTransaction userTransaction(){
UserTransactionImp ut = new UserTransactionImp();
try {
ut.setTransactionTimeout(300);
} catch (SystemException se) {
logger.error("Configuration exception.", se);
return null;
}
return ut;
}
@Bean
public PlatformTransactionManager transactionManager(){
JtaTransactionManager ptm = new JtaTransactionManager();
ptm.setTransactionManager(atomikosTransactionManager());
ptm.setUserTransaction(userTransaction());
return ptm;
}
}
对于 Atomikos 部分,定义了两个 bean,即atomikosTransactionManager和atomikosUserTransactionbean。实现类由 Atomikos 提供,它分别实现了标准的 Spring org.springframework.transaction.PlatformTransactionManager和javax.transaction.UserTransaction接口。这些 beans 提供 JTA 所需的事务协调和同步服务,并通过支持 2PC 的 XA 协议与资源管理器通信。然后,定义 Spring 的transactionManager bean(用org.springframework.transaction.jta.JtaTransactionManager作为实现类),注入 Atomikos 提供的两个事务 bean。这指示 Spring 使用 Atomikos JTA 进行事务管理。另外,请注意用于配置 Atomikos 事务服务来管理未决事务的UserTransactionService bean。 5
下面的代码片段显示了 JTA 的SingerServiceImpl类。注意,为了简单起见,只实现了save()方法。
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import java.util.List;
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
throw new NotImplementedException("findAll");
}
@Override
@Transactional(readOnly = true)
public Singer findById(Long id) {
throw new NotImplementedException("findById");
}
@Override
public Singer save(Singer singer) {
Singer singerB = new Singer();
singerB.setFirstName(singer.getFirstName());
singerB.setLastName(singer.getLastName());
if (singer.getId() == null) {
emA.persist(singer);
emB.persist(singerB);
//throw new JpaSystemException(new PersistenceException());
} else {
emA.merge(singer);
emB.merge(singer);
}
return singer;
}
@Override
public long countAll() {
return 0;
}
}
定义的两个实体管理器被注入到SingerServiceImpl类中。在save()方法中,我们将联系对象分别持久化到两个模式中。此刻忽略抛出异常语句;稍后我们将使用它来验证当保存到模式prospring5_b失败时,事务被回滚。以下代码片段显示了测试程序:
package com.apress.prospring5.ch9;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.config.XAJpaConfig;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.Date;
import java.util.GregorianCalendar;
public class TxJtaDemo {
private static Logger logger = LoggerFactory.getLogger(TxJtaDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
XAJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerService.save(singer);
if (singer.getId() != null) {
logger.info("--> Singer saved successfully");
} else {
logger.info("--> Singer was not saved, check the configuration!!");
}
ctx.close();
}
}
程序创建一个新的 contact 对象并调用SingerService.save()方法。该实现将尝试将同一个对象保存到两个数据库中。假设一切顺利,运行程序会产生以下输出(另一个输出被省略):
--> Singer saved successfully
Atomikos 创建一个复合事务,与 XA DataSource(这里是 MySQL)通信,执行同步,提交事务,等等。从数据库中,您将看到新的联系人被分别保存到数据库的两个模式中。但是如果您想检查代码中的保存,您可以为findAll()方法提供一个实现来完成这项工作。
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
private static final String FIND_ALL= "select s from Singer s";
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
@Override
@Transactional(readOnly = true)
public List<Singer> findAll()
{
List<Singer> singersFromA = findAllInA();
List<Singer> singersFromB = findAllInB();
if (singersFromA.size()!= singersFromB.size()){
throw new AsyncXAResourcesException("
XA resources do not contain the same expected data.");
}
Singer sA = singersFromA.get(0);
Singer sB = singersFromB.get(0);
if (!sA.getFirstName().equals(sB.getFirstName())) {
throw new AsyncXAResourcesException("
XA resources do not contain the same expected data.");
}
List<Singer> singersFromBoth = new ArrayList<>();
singersFromBoth.add(sA);
singersFromBoth.add(sB);
return singersFromBoth;
}
private List<Singer> findAllInA(){
return emA.createQuery(FIND_ALL).getResultList();
}
private List<Singer> findAllInB(){
return emB.createQuery(FIND_ALL).getResultList();
}
...
}
因此,测试保存在两个数据库中的歌手的代码可以修改如下:
package com.apress.prospring5.ch9;
...
public class TxJtaDemo {
private static Logger logger = LoggerFactory.getLogger(TxJtaDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
XAJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerService.save(singer);
if (singer.getId() != null) {
logger.info("--> Singer saved successfully");
} else {
logger.error("--> Singer was not saved, check the configuration!!");
}
// check saving in both databases
List<Singer> singers = singerService.findAll();
if (singers.size()!= 2) {
logger.error("--> Something went wrong.");
} else {
logger.info("--> Singers form both DBs: " + singers);
}
ctx.close();
}
}
现在让我们看看回滚是如何工作的。如下面的代码片段所示,我们没有调用emB.persist(),而是抛出一个异常来模拟出了问题,数据无法保存在第二个数据库中。
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
private static final String FIND_ALL= "select s from Singer s";
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
...
@Override
public Singer save(Singer singer) {
Singer singerB = new Singer();
singerB.setFirstName(singer.getFirstName());
singerB.setLastName(singer.getLastName());
if (singer.getId() == null) {
emA.persist(singer);
if(true) {
throw new JpaSystemException(new PersistenceException(
"Simulation of something going wrong."));
}
emB.persist(singerB);
} else {
emA.merge(singer);
emB.merge(singer);
}
return singer;
}
@Override
public long countAll() {
return 0;
}
}
再次运行该程序会产生以下结果:
...
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA
EntityManagerFactory for persistence unit 'emfA'
INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA
EntityManagerFactory for persistence unit 'emfB'
INFO o.s.t.j.JtaTransactionManager - Using JTA UserTransaction:
com.atomikos.icatch.jta.UserTransactionImp@6da9dc6
INFO o.s.t.j.JtaTransactionManager - Using JTA TransactionManager:
com.atomikos.icatch.jta.UserTransactionManager@2216effc
DEBUG o.s.t.j.JtaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.save]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
DEBUG o.s.o.j.EntityManagerFactoryUtils - Opening JPA EntityManager
DEBUG o.s.o.j.EntityManagerFactoryUtils - Registering transaction synchronization
for JPA EntityManager
Hibernate: insert into singer (BIRTH_DATE, FIRST_NAME, LAST_NAME, VERSION)
values (?, ?, ?, ?)
DEBUG o.s.o.j.EntityManagerFactoryUtils - Closing JPA EntityManager
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction rollback
WARN c.a.j.AbstractConnectionProxy - Forcing close of pending statement:
com.mysql.cj.jdbc.PreparedStatementWrapper@3f685162
Exception in thread "main" org.springframework.orm.jpa.JpaSystemException:
Simulation of something going wrong.;
...
Caused by: javax.persistence.PersistenceException:
Simulation of something going wrong.
如前面的输出所示,第一个歌手被持久化(注意insert语句)。但是,当保存到第二个DataSource时,因为抛出了异常,Atomikos 将回滚整个事务。你可以看一下模式musicdb_a来检查新歌手没有被保存。
春船 JTA
JTA Spring Boot 入门版开箱即用,带有一组预配置的 beans,旨在帮助您专注于代码的业务功能,而不是环境设置。再说一遍,这是所有 Spring Boot 入门库都做的事情,不管是什么组件,所以前面的句子可能看起来有点多余。JTA 版的 Spring Boot 包含一个使用 Atomikos 的库,它会提取适当的库并为您配置 Atomikos 组件。将前面的例子迁移到 Spring Boot 意味着将DataSource和事务管理器配置导入到 Spring Boot 应用中。但是由于本节的目的是展示 Spring Boot 如何通过所提供的预配置 beans 来帮助加速涉及全局事务管理的应用的开发,因此有必要提供一个不同的示例。我们将假设我们想要向消息队列传输一条消息,表明创建了一个新的Singer实例。显然,如果将Singer记录保存到数据库失败,我们希望回滚事务并阻止消息被发送。为了总结这个例子,我们需要做以下事情:
-
Configure the Spring Boot Gradle project for JTA and JMS usage. The configuration is as follows:
//build.gradle ext { ... bootVersion = '2.0.0.M1' atomikosVersion = '4.0.4' boot = [ ... starterJpa : "org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion", starterJta : "org.springframework.boot:spring-boot-starter-jta-atomikos:$bootVersion", starterJms : "org.springframework.boot:spring-boot-starter-artemis:$bootVersion" ] misc = [ ... artemis : "org.apache.activemq:artemis-jms-server:2.1.0" ] db = [ ... h2 : "com.h2database:h2:$h2Version" ] } //chapter09/boot-jta/build.gradle buildscript { repositories { ... } dependencies { classpath boot.springBootPlugin } } apply plugin: 'org.springframework.boot' dependencies { compile boot.starterJpa, boot.starterJta, boot.starterJms, db.h2 compilemisc.artemis { exclude group: 'org.apache.geronimo.specs', module: 'geronimo-jms_2.0_spec' } }In Figure 9-3 you can see the Spring Boot starter libraries declared earlier as dependencies for the project, and you can see the dependencies they add to the project.
图 9-3。
Spring Boot Starter libraries and their dependencies
-
定义
Singer实体类和处理它的存储库。Singer实体类的结构与前面提到的相同,但是没有任何相关的实体。并且SingerRepository将被留空,因为只有CrudRepository已经提供的方法将在本例中使用:save(..)和count()。 -
定义一个将保存
Singer记录并发送确认消息的服务类。package com.apress.prospring5.ch9.services; import com.apress.prospring5.ch9.entities.Singer; import com.apress.prospring5.ch9.ex.AsyncXAResourcesException; import com.apress.prospring5.ch9.repos.SingerRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Service("singerService") @Transactional public class SingerServiceImpl implements SingerService { private SingerRepository singerRepository; private JmsTemplate jmsTemplate; public SingerServiceImpl(SingerRepository singerRepository, JmsTemplate jmsTemplate) { this.singerRepository = singerRepository; this.jmsTemplate = jmsTemplate; } @Override public Singer save(Singer singer) { jmsTemplate.convertAndSend("singers", "Just saved:" + singer); if(singer == null) { throw new AsyncXAResourcesException( "Simulation of something going wrong."); } singerRepository.save(singer); return singer; } @Override public long count() { return singerRepository.count(); } }不需要
@Autowired注释来注入存储库 bean,对于JmsTemplate也是如此。Spring Boot 就是这么神奇,它注入所需的豆子,如果只有它们被声明的话。 -
一个 bean,它将监听传递到
singers队列的消息并打印它们。package com.apress.prospring5.ch9; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; @Component public class Messages { private static Logger logger = LoggerFactory.getLogger(Messages.class); @JmsListener(destination="singers") public void onMessage(String content){ logger.info("--> Received content: " + content); } } -
配置 Artemis JMS 服务器,创建一个名为
singers的嵌入式队列。这是通过在application.properties文件中用值singers设置spring.artemis.embedded.queues属性来完成的,该文件可用于配置 Spring Boot 应用。spring.artemis.embedded.queues=singers spring.jta.log-dir=out前面的配置片段描述了
application.properties文件的内容。除了spring.artemis.embedded.queues属性之外,spring.jta.log-dir用于设置 Atomikos 应该在哪里写入 JTA 日志,在本例中设置了out目录。 -
下面是一个应用类,它将所有这些打包在一起并进行测试:
package com.apress.prospring5.ch9; import com.apress.prospring5.ch9.entities.Singer; import com.apress.prospring5.ch9.services.SingerService; import com.atomikos.jdbc.AtomikosDataSourceBean; import org.h2.jdbcx.JdbcDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import java.util.Date; import java.util.GregorianCalendar; import java.util.Properties; import static org.hibernate.cfg.AvailableSettings.*; import static org.hibernate.cfg.AvailableSettings.STATEMENT_FETCH_SIZE; @SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch9.services") public class Application implements CommandLineRunner { private static Logger logger = LoggerFactory.getLogger(Application.class); public static void main(String... args) throws Exception { ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args); System.in.read(); ctx.close(); } @Autowired SingerService singerService; @Override public void run(String... args) throws Exception { Singer singer = new Singer(); singer.setFirstName("John"); singer.setLastName("Mayer"); singer.setBirthDate(new Date( (new GregorianCalendar(1977, 9, 16)).getTime().getTime())); singerService.save(singer); long count = singerService.count(); if (count == 1) { logger.info("--> Singer saved successfully"); } else { logger.error("--> Singer was not saved, check the configuration!!"); } try { singerService.save(null); } catch (Exception ex) { logger.error(ex.getMessage() + "Final count:" + singerService.count()); } } }
如果您运行Application,您将看到类似如下的输出:
...
INFO c.a.j.AtomikosConnectionFactoryBean - AtomikosConnectionFactoryBean
'jmsConnectionFactory': init...
INFO o.s.t.j.JtaTransactionManager - Using JTA UserTransaction:
com.atomikos.icatch.jta.UserTransactionManager@408a247c
INFO c.a.j.AtomikosJmsXaSessionProxy - atomikos xa session proxy for resource
jmsConnectionFactory: calling createQueue on JMS driver session...
INFO c.a.j.AtomikosJmsXaSessionProxy - atomikos xa session proxy for resource
jmsConnectionFactory: calling getTransacted on JMS driver session...
DEBUG o.s.t.j.JtaTransactionManager - Participating in existing transaction
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction commit
INFO c.a.d.x.XAResourceTransaction - XAResource.start ...
INFO c.a.d.x.XAResourceTransaction - XAResource.end ...
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction commit
DEBUG o.s.t.j.JtaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.save]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
INFO c.a.i.i.BaseTransactionManager - createCompositeTransaction ( 10000 ):
created new ROOT transaction with id 127.0.0.1.tm0000200001
DEBUG o.s.t.j.JtaTransactionManager - Participating in existing transaction
INFO c.a.p.c.Application - --> Singer saved successfully
...//etc
从日志中,您可以清楚地看到为每个操作创建和重用的全局事务。如果您通过按任意键正常退出应用,然后回车,请耐心等待,因为应用需要一段时间才能正常关闭。
以下是关于使用 Spring Boot 创建 JTA 应用的一些结论:虽然看起来很容易,但是当处理多个数据源时,配置环境是您无法回避的事情。此外,如果 JTA 提供者是由 JEE 服务器提供的,事情会变得相当复杂。但是对于用于教育目的和测试的示例应用,它非常实用。
关于使用 JTA 事务管理器的思考
是否使用 JTA 进行全球交易管理正在激烈辩论中。例如,Spring 开发团队通常不推荐使用 JTA 进行全局事务。
作为一般原则,当您的应用被部署到一个成熟的 JEE 应用服务器时,不使用 JTA 是没有意义的,因为流行的 JEE 应用服务器的所有供应商都已经为他们的平台优化了他们的 JTA 实现。这是你花钱购买的一个主要功能。
对于独立或 web 容器部署,让应用需求驱动您的决策。尽可能早地执行负载测试,以验证使用 JTA 不会影响性能。
一个好消息是,Spring 可以与大多数主流 web 和 JEE 容器中的本地和全局事务无缝协作,所以当您从一种事务管理策略切换到另一种时,通常不需要修改代码。如果您决定在应用中使用 JTA,请确保使用 Spring 的JtaTransactionManager。
摘要
在几乎任何类型的应用中,事务管理都是确保数据完整性的关键部分。在这一章中,我们讨论了如何使用 Spring 来管理事务,而几乎不影响您的源代码。您还学习了如何使用本地和全局事务。
我们提供了各种事务实现的例子,包括使用 XML 配置和注释的声明性方法,以及编程方法。
本地事务在 JEE 应用服务器内部/外部都得到支持,只需要简单的配置就可以在 Spring 中启用本地事务支持。然而,设置一个全局事务环境需要做更多的工作,并且很大程度上取决于您的应用需要与哪个 JTA 提供者和相应的后端资源进行交互。
Footnotes 1
JDO 的支持在 Spring 5 月被放弃;因此,JdoTransactionManager从类图中消失了。
2
Spring 5 只能和 Hibernate 5 一起用;Hibernate 3 和 Hibernate 4 的实现已经删除。
3
对 JMS 1.1 的支持在 Spring 5 中被删除。
4
这个配置是在 Atomikos 官方文档的帮助下创建的,用于在 https://www.atomikos.com/Documentation/SpringIntegration 和 https://stackoverflow.com/questions/33127854/hibernate-5-with-spring-jta 的堆栈溢出社区的帮助下与 Spring Integration。
5
该配置是 XML 配置的注释配置改编,在 https://www.atomikos.com/Documentation/SpringIntegration#The_Advanced_Case_40As_of_3.3_41 的 Atomikos 文档中作为示例给出。
十、带有类型转换和格式的验证
在企业应用中,验证是至关重要的。验证的目的是验证正在处理的数据满足所有预定义的业务需求,并确保数据完整性和在应用的其他层的有用性。
在应用开发中,数据验证总是与转换和格式化一起被提及。原因是数据源的格式很可能与应用中使用的格式不同。例如,在 web 应用中,用户在 web 浏览器前端输入信息。当用户保存该数据时,它被发送到服务器(在本地验证完成之后)。在服务器端,执行数据绑定过程,在该过程中,来自 HTTP 请求的数据被提取、转换并绑定到相应的域对象(例如,用户在 HTML 表单中输入歌手信息,然后绑定到服务器中的Singer对象),这是基于为每个属性定义的格式规则(例如,日期格式模式是yyyy-MM-dd)。当数据绑定完成时,验证规则被应用到域对象,以检查任何约束违反。如果一切运行正常,数据将被持久化,并向用户显示一条成功消息。否则,验证错误消息将被填充并显示给用户。
在本章的第一部分,您将了解 Spring 如何为类型转换、字段格式化和验证提供复杂的支持。具体来说,本章涵盖以下主题:
- Spring 类型转换系统和格式化程序服务提供者接口(SPI):我们介绍泛型类型转换系统和格式化程序 SPI。我们将介绍如何使用新服务来取代以前的
PropertyEditor支持,以及它们如何在任何 Java 类型之间转换。 - Spring 中的验证:我们讨论 Spring 如何支持域对象验证。首先,我们简单介绍一下 Spring 自己的
Validator接口。然后,我们关注 JSR-349 (Bean 验证)支持。
属国
与前几章一样,本章中的示例需要一些依赖项,这些依赖项在下面的配置片段中有所描述。您可能会注意到的一个依赖项是joda-time。如果你运行的是 Java 8,Spring 5 也支持 JSR-310,也就是javax.time API。
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
jodaVersion = '2.9.9'
javaxValidationVersion = '2.0.0.Beta2' //1.1.0.Final
javaElVersion = '3.0.1-b04' // 3.0.0
glasshfishELVersion = '2.2.1-b05' // 2.2
hibernateValidatorVersion = '6.0.0.Beta2' //5.4.1.Final
spring = [...]
hibernate = [
validator :
"org.hibernate:hibernate-validator:$hibernateValidatorVersion",
...
]
misc = [
validation :
"javax.validation:validation-api:$javaxValidationVersion",
joda : "joda-time:joda-time:$jodaVersion",
...
]
...
}
//chapter10/build.gradle
dependencies {
compile spring.contextSupport, misc.slf4jJcl, misc.logback,
db.h2, misc.lang3, hibernate.em, hibernate.validator,
misc.joda, misc.validation
testCompile testing.junit
}
Spring 式转换系统
在 Spring 3 中,引入了一个新的类型转换系统,提供了一种在 Spring 支持的应用中在任何 Java 类型之间进行转换的强大方法。本节展示了这个新服务如何执行先前的PropertyEditor支持所提供的相同功能,以及它如何支持任何 Java 类型之间的转换。我们还演示了如何使用转换器 SPI 实现自定义类型的转换器。
使用 PropertyEditors 从字符串转换
第三章讲述了 Spring 如何通过支持PropertyEditor s 来处理从属性文件中的String到 POJOs 属性的转换。让我们在这里做一个快速回顾,然后讲述 Spring 的转换器 SPI(从 3.0 开始可用)如何提供一个更强大的选择。
考虑这个简单版本的Singer类:
package com.apress.prospring5.ch10;
import java.net.URL;
import java.text.SimpleDateFormat;
import org.joda.time.DateTime;
public class Singer {
private String firstName;
private String lastName;
private DateTime birthDate;
private URL personalSite;
//getters and setters
...
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return String.format("{First name: %s, Last name: %s,
Birthday: %s, Site: %s}",
firstName, lastName, sdf.format(birthDate.toDate()), personalSite);
}
}
对于birthDate属性,我们使用 JodaTime 的DateTime类。此外,如果适用的话,还有一个URL类型的字段,指示歌手的个人网站。现在假设我们想在 Spring 的ApplicationContext中构造Singer对象,其值存储在 Spring 的配置文件或属性文件中。下面的配置片段显示了 Spring XML 配置文件(prop-editor-app-context.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<context:annotation-config/>
<context:property-placeholder location="classpath:application.properties"/>
<bean id="customEditorConfigurer"
class="org.springframework.beans.factory.config.CustomEditorConfigurer"
p:propertyEditorRegistrars-ref="propertyEditorRegistrarsList"/>
<util:list id="propertyEditorRegistrarsList">
<bean class="com.apress.prospring5.ch10.DateTimeEditorRegistrar">
<constructor-arg value="${date.format.pattern}"/>
</bean>
</util:list>
<bean id="eric" class="com.apress.prospring5.ch10.Singer"
p:firstName="Eric"
p:lastName="Clapton"
p:birthDate="1945-03-30"
p:personalSite="http://www.ericclapton.com"/>
<bean id="countrySinger" class="com.apress.prospring5.ch10.Singer"
p:firstName="${countrySinger.firstName}"
p:lastName="${countrySinger.lastName}"
p:birthDate="${countrySinger.birthDate}"
p:personalSite="${countrySinger.personalSite}"/>
</beans>
这里我们构造了两个不同的Singer类的 beans。eric bean 是用配置文件中提供的值构造的,而对于countrysinger bean,属性被外化到一个属性文件中。此外,还定义了一个自定义编辑器,用于从String到 JodaTime 的DateTime类型的转换,日期时间格式模式也在属性文件中具体化了。下面的代码片段显示了属性文件(application.properties):
date.format.pattern=yyyy-MM-dd
countrySinger.firstName=John
countrySinger.lastName=Mayer
countrySinger.birthDate=1977-10-16
countrySinger.personalSite=http://johnmayer.com/
以下代码片段显示了用于将String值转换为 JodaTime DateTime类型的自定义编辑器:
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import java.beans.PropertyEditorSupport;
public class DateTimeEditorRegistrar implements PropertyEditorRegistrar {
private DateTimeFormatter dateTimeFormatter;
public DateTimeEditorRegistrar(String dateFormatPattern) {
dateTimeFormatter = DateTimeFormat.forPattern(dateFormatPattern);
}
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(DateTime.class,
new DateTimeEditor(dateTimeFormatter));
}
private static class DateTimeEditor extends PropertyEditorSupport {
private DateTimeFormatter dateTimeFormatter;
public DateTimeEditor(DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(DateTime.parse(text, dateTimeFormatter));
}
}
}
DateTimeEditorRegistrar实现PropertyEditorRegister接口来注册我们的自定义PropertyEditor。然后我们创建一个名为DateTimeEditor的内部类,处理从String到DateTime的转换。我们在这个例子中使用了一个内部类,因为它只被PropertyEditorRegistrar实现访问。现在我们来测试一下。下一个代码片段显示了测试程序:
package com.apress.prospring5.ch10;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class PropEditorDemo {
private static Logger logger =
LoggerFactory.getLogger(PropEditorDemo.class);
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/prop-editor-app-context.xml");
ctx.refresh();
Singer eric = ctx.getBean("eric", Singer.class);
logger.info("Eric info: " + eric);
Singer countrySinger = ctx.getBean("countrySinger", Singer.class);
logger.info("John info: " + countrySinger);
ctx.close();
}
}
如您所见,这两个Singerbean 是从ApplicationContext中检索并打印出来的。运行该程序会产生以下输出:
[main] INFO c.a.p.c.PropEditorDemo - Eric info: {First name: Eric,
Last name: Clapton, Birthday: 1945-03-30, Site: http://www.ericclapton.com}
[main] INFO c.a.p.c.PropEditorDemo - John info: {First name: John,
Last name: Mayer, Birthday: 1977-10-16, Site: http://johnmayer.com/}
如输出所示,属性被转换并应用于Singerbean。这里使用 XML 而不是 Java 配置类的原因是,要注入的值被声明为文本值,Spring 在后台透明地进行转换。
引入 Spring 类型转换
在 Spring 3.0 中,引入了一个通用类型转换系统,它位于包org.springframework.core.convert下。除了提供对PropertyEditor支持的替代,类型转换系统可以被配置成在任何 Java 类型和 POJOs 之间进行转换(而PropertyEditor则专注于将属性文件中的String表示转换成 Java 类型)。
实现自定义转换器
要查看类型转换系统的运行情况,让我们重新看看前面的例子,使用同一个Singer类。假设这次我们想使用类型转换系统将String格式的日期转换成Singer的birthDate属性,该属性属于 JodaTime 的DateTime类型。为了支持转换,我们通过实现org.springframework.core.convert.converter.Converter<S,T>接口来创建一个定制的转换器,而不是创建一个定制的PropertyEditor。以下代码片段显示了自定义转换器:
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.core.convert.converter.Converter;
import javax.annotation.PostConstruct;
public class StringToDateTimeConverter implements Converter<String, DateTime> {
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
private DateTimeFormatter dateFormat;
private String datePattern = DEFAULT_DATE_PATTERN;
public String getDatePattern() {
return datePattern;
}
public void setDatePattern(String datePattern) {
this.datePattern = datePattern;
}
@PostConstruct
public void init() {
dateFormat = DateTimeFormat.forPattern(datePattern);
}
@Override
public DateTime convert(String dateString) {
return dateFormat.parseDateTime(dateString);
}
}
我们实现了接口Converter<String, DateTime>,这意味着转换器负责将一个String(源类型S)转换成一个DateTime(目标类型T)。日期时间模式的注入是可选的,可以通过调用 setter setDatePattern来完成。如果没有注入,则使用默认模式yyyy-MM-dd。然后,在初始化方法(用@PostConstruct注释的init()方法)中,构造一个 JodaTime 的DateTimeFormat类的实例,它将根据指定的模式执行转换。最后,实现convert()方法来提供转换逻辑。
配置 ConversionService
为了使用转换服务而不是PropertyEditor,我们需要在 Spring 的ApplicationContext中配置一个org.springframework.core.convert.ConversionService接口的实例。以下代码片段显示了 Java 配置类:
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.Singer;
import com.apress.prospring5.ch10.StringToDateTimeConverter;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.converter.Converter;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@PropertySource("classpath:application.properties")
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Value("${date.format.pattern}")
private String dateFormatPattern;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Singer john(@Value("${countrySinger.firstName}") String firstName,
@Value("${countrySinger.lastName}") String lastName,
@Value("${countrySinger.personalSite}") URL personalSite,
@Value("${countrySinger.birthDate}") DateTime birthDate)
throws Exception {
Singer singer = new Singer();
singer.setFirstName(firstName);
singer.setLastName(lastName);
singer.setPersonalSite(personalSite);
singer.setBirthDate(birthDate);
return singer;
}
@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean conversionServiceFactoryBean =
new ConversionServiceFactoryBean();
Set<Converter> convs = new HashSet<>();
convs.add(converter());
conversionServiceFactoryBean.setConverters(convs);
conversionServiceFactoryBean.afterPropertiesSet();
return conversionServiceFactoryBean;
}
@Bean
StringToDateTimeConverter converter(){
StringToDateTimeConverter conv = new StringToDateTimeConverter();
conv.setDatePattern(dateFormatPattern);
conv.init();
return conv;
}
}
这些值从一个属性文件中读取,该文件的内容与上一节中介绍的文件相同,并使用@Value注释注入到创建的 bean 中。
这里我们通过用类ConversionServiceFactoryBean声明一个conversionService bean 来指示 Spring 使用类型转换系统。如果没有定义转换服务 bean,Spring 将使用基于PropertyEditor的系统。
默认情况下,类型转换服务支持常见类型之间的转换,包括字符串、数字、枚举、集合、映射等。此外,在基于PropertyEditor的系统中,支持从String到 Java 类型的转换。
对于conversionService bean,配置了一个自定义转换器,用于从String到DateTime的转换。测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class ConvServDemo {
private static Logger logger = LoggerFactory.getLogger(ConvServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ctx.close();
}
}
运行测试程序会产生以下输出:
15:41:09.960 main INFO c.a.p.c.ConvServDemo - Singer info: {First name: John,
Last name: Mayer, Birthday: 1977-10-16, Site: http://johnmayer.com/}
如您所见,john bean 的属性转换结果与我们使用PropertyEditor s 时的结果相同。
在任意类型之间转换
类型转换系统的真正优势是能够在任意类型之间进行转换。为了查看它的运行情况,假设我们有另一个名为AnotherSinger的类,它与Singer类相同。代码如下所示:
package com.apress.prospring5.ch10;
import java.net.URL;
import java.text.SimpleDateFormat;
import org.joda.time.DateTime;
public class AnotherSinger {
private String firstName;
private String lastName;
private DateTime birthDate;
private URL personalSite;
//seters and getters
...
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return String.format("{First name: %s, Last name: %s,
Birthday: %s, Site: %s}", firstName, lastName,
sdf.format(birthDate.toDate()), personalSite);
}
}
我们希望能够将Singer类的任何实例转换成AnotherSinger类。转换后,Singer的firstName和lastName值将分别变成AnotherSinger的lastName和firstName。让我们实现另一个自定义转换器来执行转换。以下代码片段显示了自定义转换器:
package com.apress.prospring5.ch10;
import org.springframework.core.convert.converter.Converter;
public class SingerToAnotherSingerConverter
implements Converter<Singer, AnotherSinger> {
@Override
public AnotherSinger convert(Singer singer) {
AnotherSinger anotherSinger = new AnotherSinger();
anotherSinger.setFirstName(singer.getLastName());
anotherSinger.setLastName(singer.getFirstName());
anotherSinger.setBirthDate(singer.getBirthDate());
anotherSinger.setPersonalSite(singer.getPersonalSite());
return anotherSinger;
}
}
类是简单的;只需在Singer和AnotherSinger类之间交换firstName和lastName属性值。要将自定义转换器注册到ApplicationContext中,请用以下代码片段中的代码片段替换AppConfig类中的conversionService bean 的定义:
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.Singer;
import com.apress.prospring5.ch10.SingerToAnotherSingerConverter;
import com.apress.prospring5.ch10.StringToDateTimeConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.core.convert.converter.Converter;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Bean
public Singer john() throws Exception {
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setPersonalSite(new URL("http://johnmayer.com/"));
singer.setBirthDate(converter().convert("1977-10-16"));
return singer;
}
@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean conversionServiceFactoryBean =
new ConversionServiceFactoryBean();
Set<Converter> convs = new HashSet<>();
convs.add(converter());
convs.add(singerConverter());
conversionServiceFactoryBean.setConverters(convs);
conversionServiceFactoryBean.afterPropertiesSet();
return conversionServiceFactoryBean;
}
@Bean
StringToDateTimeConverter converter() {
return new StringToDateTimeConverter();
}
@Bean
SingerToAnotherSingerConverter singerConverter() {
return new SingerToAnotherSingerConverter();
}
}
converter 属性中 beans 的顺序并不重要。为了测试转换,我们使用下面的测试程序,它是这里显示的MultipleConvServDemo类:
package com.apress.prospring5.ch10;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionService;
import com.apress.prospring5.ch10.config.AppConfig;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MultipleConvServDemo {
private static Logger logger =
LoggerFactory.getLogger(MultipleConvServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ConversionService conversionService =
ctx.getBean(ConversionService.class);
AnotherSinger anotherSinger =
conversionService.convert(john, AnotherSinger.class);
logger.info("Another singer info: " + anotherSinger);
String[] stringArray = conversionService.convert("a,b,c",
String[].class);
logger.info("String array: " + stringArray[0]
+ stringArray[1] + stringArray[2]);
List<String> listString = new ArrayList<>();
listString.add("a");
listString.add("b");
listString.add("c");
Set<String> setString =
conversionService.convert(listString, HashSet.class);
for (String string: setString)
System.out.println("Set: " + string);
}
}
从ApplicationContext获得ConversionService接口的句柄。因为我们已经用自定义转换器在ApplicationContext中注册了ConversionService,我们可以用它来转换Singer对象,以及在转换服务已经支持的其他类型之间进行转换。如清单所示,出于演示目的,还添加了从String(由逗号分隔)转换为Array以及从List转换为Set的示例。运行该程序会产生以下输出:
[main] INFO c.a.p.c.MultipleConvServDemo - Singer info:
{First name: John, Last name: Mayer, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
[main] INFO c.a.p.c.MultipleConvServDemo - Another singer info:
{First name: Mayer, Last name: John, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
[main] INFO c.a.p.c.MultipleConvServDemo - String array: abc
Set: a
Set: b
Set: c
在输出中,您将看到Singer和AnotherSinger被正确转换,以及String到Array和List到Set。使用 Spring 的类型转换服务,您可以轻松地创建自定义转换器,并在应用中的任何层执行转换。一个可能的用例是,您有两个系统,需要更新相同的歌手信息。但是,数据库结构不同(例如,系统 A 中的姓表示系统 B 中的名,以此类推)。在保存到每个单独的系统之前,您可以使用类型转换系统来转换对象。
从 Spring 3.0 开始,Spring MVC 大量使用转换服务(以及下一节讨论的格式化程序 SPI)。在 web 应用上下文配置中,标记<mvc:annotation-driven/>的声明,或者在 Java 配置类中使用 Spring 3.1 中引入的@EnableWebMvc,将自动注册所有默认转换器(例如,StringToArrayConverter、StringToBooleanConverter和StringToLocaleConverter,它们都位于org.springframework.core.convert.support包下)和格式化程序(例如,CurrencyFormatter、DateFormatter和NumberFormatter,它们都位于org.springframework.format包内的各个子包下)。当我们在 Spring 中讨论 web 应用开发时,会在第十六章中涉及更多内容。
Spring 中的字段格式
除了类型转换系统,Spring 带给开发者的另一个很棒的特性是格式化程序 SPI。如您所料,这个 SPI 可以帮助配置字段格式。
在格式化程序 SPI 中,实现格式化程序的主要接口是org.springframework.format.Formatter<T>接口。Spring 提供了一些常用类型的实现,包括CurrencyFormatter、DateFormatter、NumberFormatter和PercentFormatter。
实现自定义格式化程序
实现自定义格式化程序也很容易。我们将使用相同的Singer类,并实现一个自定义格式化程序,用于将birthDate属性的DateTime类型与String类型相互转换。
然而,这一次我们将采取不同的方法;我们将扩展 Spring 的org.springframework.format.support.FormattingConversionServiceFactoryBean类,并提供我们的自定义格式化程序。FormattingConversionServiceFactoryBean类是一个工厂类,它提供了对底层FormattingConversionService类的方便访问,后者支持类型转换系统,以及根据为每个字段类型定义的格式化规则进行字段格式化。
下面的代码片段显示了一个扩展了FormattingConversionServiceFactoryBean类的自定义类,其中定义了一个自定义格式化程序来格式化 JodaTime 的DateTime类型。
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.Formatter;
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@Component("conversionService")
public class ApplicationConversionServiceFactoryBean extends
FormattingConversionServiceFactoryBean {
private static Logger logger =
LoggerFactory.getLogger(ApplicationConversionServiceFactoryBean.class);
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
private DateTimeFormatter dateFormat;
private String datePattern = DEFAULT_DATE_PATTERN;
private Set<Formatter<?>> formatters = new HashSet<>();
public String getDatePattern() {
return datePattern;
}
@Autowired(required = false)
public void setDatePattern(String datePattern) {
this.datePattern = datePattern;
}
@PostConstruct
public void init() {
dateFormat = DateTimeFormat.forPattern(datePattern);
formatters.add(getDateTimeFormatter());
setFormatters(formatters);
}
public Formatter<DateTime> getDateTimeFormatter() {
return new Formatter<DateTime>() {
@Override
public DateTime parse(String dateTimeString, Locale locale)
throws ParseException {
logger.info("Parsing date string: " + dateTimeString);
return dateFormat.parseDateTime(dateTimeString);
}
@Override
public String print(DateTime dateTime, Locale locale) {
logger.info("Formatting datetime: " + dateTime);
return dateFormat.print(dateTime);
}
};
}
}
在前面的清单中,自定义格式化程序带有下划线。它实现了Formatter<DateTime>接口,并实现了该接口定义的两个方法。parse()方法将String格式解析为DateTime类型(为了支持本地化,还传递了区域设置),而logger.info()方法将DateTime实例格式化为String。日期模式可以注入到 bean 中(或者默认为yyyy-MM-dd)。同样,在init()方法中,自定义格式化程序是通过调用setFormatters()方法注册的。您可以根据需要添加任意数量的格式化程序。
配置 ConversionServiceFactoryBean
声明一个类型为FormattingConversionServiceFactoryBean的 bean 大大减小了AppConfig配置类的大小。
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.ApplicationConversionServiceFactoryBean;
import com.apress.prospring5.ch10.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.net.URL;
import java.util.Locale;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Autowired
ApplicationConversionServiceFactoryBean conversionService;
@Bean
public Singer john() throws Exception {
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setPersonalSite(new URL("http://johnmayer.com/"));
singer.setBirthDate(conversionService.
getDateTimeFormatter().parse("1977-10-16", Locale.ENGLISH));
return singer;
}
}
测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionService;
public class ConvFormatServDemo {
private static Logger logger =
LoggerFactory.getLogger(ConvFormatServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ConversionService conversionService =
ctx.getBean("conversionService", ConversionService.class);
logger.info("Birthdate of singer is : " +
conversionService.convert(john.getBirthDate(), String.class));
ctx.close();
}
}
运行该程序会产生以下输出:
Parsing date string: 1977-10-16
[main] INFO c.a.p.c.ConvFormatServDemo - Singer info: {
First name: John, Last name: Mayer, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
Formatting datetime: 1977-10-16T00:00:00.000+03:00
[main] INFO c.a.p.c.ConvFormatServDemo -
Birthdate of singer is : 1977-10-16
在输出中,您可以看到 Spring 使用我们的自定义格式化程序的parse()方法将属性从String转换为birthDate属性的DateTime类型。当我们调用ConversionService.convert()方法并传入birthDate属性时,Spring 将调用logger的info方法来格式化输出。
Spring 验证
验证是任何应用的关键部分。应用于域对象的验证规则确保所有的业务数据都是结构良好的,并且满足所有的业务定义。理想的情况是,所有的验证规则都在一个集中的位置维护,相同的规则集应用于相同类型的数据,而不管数据来自哪个源(例如,通过 web 应用的用户输入、通过 web 服务的远程应用、JMS 消息或文件)。
当谈到验证时,转换和格式化也很重要,因为在验证一段数据之前,应该根据为每种类型定义的格式化规则将它转换成所需的 POJO。例如,用户通过浏览器中的 web 应用输入一些歌手信息,然后将数据提交给服务器。在服务器端,如果 web 应用是在 Spring MVC 中开发的,Spring 将从 HTTP 请求中提取数据,并根据格式规则执行从String到所需类型的转换(例如,表示日期的String将被转换为Date字段,格式规则为yyyy-MM-dd)。这个过程称为数据绑定。当数据绑定完成并且构造了域对象后,将对该对象进行验证,并将所有错误返回并显示给用户。如果验证成功,对象将被保存到数据库中。
Spring 支持两种主要类型的验证。第一个是由 Spring 提供的,在其中可以通过实现org.springframework.validation.Validator接口来创建自定义验证器。另一个是通过 Spring 对 JSR-349 的支持(Bean 验证)。我们将在接下来的章节中介绍这两种方法。
使用 Spring 验证器接口
使用 Spring 的Validator接口,我们可以通过创建一个实现接口的类来开发一些验证逻辑。让我们看看它是如何工作的。对于我们到目前为止使用的Singer类,假设名字不能为空。为了根据这个规则验证Singer对象,我们可以创建一个定制的验证器。以下代码片段显示了验证器类:
package com.apress.prospring5.ch10;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component("singerValidator")
public class SingerValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Singer.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "firstName",
"firstName.empty");
}
}
validator 类实现了Validator接口并实现了两个方法。supports()方法指示验证器是否支持对传入的类类型的验证。validate()方法对传入的对象进行验证。结果将存储在org.springframework.validation.Errors接口的一个实例中。在validate()方法中,我们只对firstName属性执行检查,并使用方便的ValidationUtils.rejectIfEmpty()方法来确保歌手的名字不为空。最后一个参数是错误代码,可用于从资源包中查找验证消息,以显示本地化的错误消息。
以下代码片段描述了配置类:
package com.apress.prospring5.ch10.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
}
以下代码片段包含测试程序:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import java.util.List;
public class SpringValidatorDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringValidatorDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer singer = new Singer();
singer.setFirstName(null);
singer.setLastName("Mayer");
Validator singerValidator = ctx.getBean("singerValidator",
Validator.class);
BeanPropertyBindingResult result =
new BeanPropertyBindingResult(singer, "John");
ValidationUtils.invokeValidator(singerValidator, singer, result);
List<ObjectError> errors = result.getAllErrors();
logger.info("No of validation errors: " + errors.size());
errors.forEach(e -> logger.info(e.getCode()));
ctx.close();
}
}
用设置为null的名字来构造一个Singer对象。然后,从ApplicationContext中检索验证器。为了存储验证结果,构建了一个BeanPropertyBindingResult类的实例。为了执行验证,调用了ValidationUtils.invokeValidator()方法。然后我们检查验证错误。运行该程序会产生以下输出:
[main] INFO c.a.p.c.SpringValidatorDemo - No of validation errors: 1
[main] INFO c.a.p.c.SpringValidatorDemo - firstName.empty
验证产生一个错误,错误代码显示正确。
使用 JSR-349 Bean 验证
从 Spring 4 开始,已经实现了对 JSR-349 (Bean 验证)的完全支持。Bean Validation API 在包javax.validation.constraints下定义了一组 Java 注释形式的约束(例如,@NotNull),这些约束可以应用于域对象。此外,可以通过使用注释来开发和应用定制验证器(例如,类级别的验证器)。
使用 Bean 验证 API 使您不必耦合到特定的验证服务提供者。通过使用 Bean 验证 API,您可以使用标准注释和 API 来实现域对象的验证逻辑,而无需了解底层的验证服务提供者。例如,Hibernate Validator 版本 5 ( http://hibernate.org/subprojects/validator )就是 JSR-349 参考实现。
Spring 为 Bean 验证 API 提供了无缝支持。主要特性包括支持定义验证约束的 JSR-349 标准注释、定制验证器,以及在 Spring 的ApplicationContext中配置 JSR-349 验证。让我们在接下来的章节中一个接一个地讨论它们。当在类路径中使用 Hibernate Validator 版本 4 和 1.0 版本的验证 API 时,Spring 仍然无缝地提供了对 JSR-303 的支持。
定义对象属性的验证约束
让我们从对域对象属性应用验证约束开始。下面的代码片段显示了一个更高级的Singer类,其中验证约束应用于firstName和genre属性:
package com.apress.prospring5.ch10.obj;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Singer {
@NotNull
@Size(min=2, max=60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
//setters and getters
...
}
这里应用的验证约束显示为下划线。对于firstName属性,应用了两个约束。第一个由@NotNull注释控制,这表明该值不应该是null。此外,@Size注释决定了firstName属性的长度。@NotNull约束也适用于genre属性。
以下代码示例分别显示了Genre和Gender枚举类:
//Genre.java
package com.apress.prospring5.ch10.obj;
public enum Genre {
POP("P"),
JAZZ("J"),
BLUES("B"),
COUNTRY("C");
private String code;
private Genre(String code) {
this.code = code;
}
public String toString() {
return this.code;
}
}
//Genfer.java
package com.apress.prospring5.ch10.obj;
public enum Gender {
MALE("M"), FEMALE("F");
private String code;
Gender(String code) {
this.code = code;
}
@Override
public String toString() {
return this.code;
}
}
流派表示歌手所属的音乐流派,而性别与音乐生涯并没有太大关系,所以可能是null。
在 Spring 中配置 Bean 验证支持
为了在 Spring 的ApplicationContext中配置 bean 验证 API 的支持,我们在 Spring 的配置中定义了一个类型为org.springframework.validation.beanvalidation.LocalValidatorFactoryBean的 Bean。以下代码片段描述了配置类:
package com.apress.prospring5.ch10.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Bean LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
只需要声明一个类型为LocalValidatorFactoryBean的 bean。默认情况下,Spring 会搜索类路径中是否存在 Hibernate 验证器库。现在,让我们创建一个为Singer类提供验证服务的服务类。验证器类如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.obj.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
@Service("singerValidationService")
public class SingerValidationService {
@Autowired
private Validator validator;
public Set<ConstraintViolation<Singer>>
validateSinger(Singer singer) {
return validator.validate(singer);
}
}
注入了javax.validation.Validator的一个实例(注意与 Spring 提供的Validator接口的不同,后者是org.springframework.validation.Validator)。一旦定义了LocalValidatorFactoryBean,您就可以在应用的任何地方创建一个Validator接口的句柄。为了对 POJO 执行验证,调用了Validator.validate()方法。验证结果将作为ConstraintViolation<T>接口的List返回。
测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import com.apress.prospring5.ch10.obj.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import javax.validation.ConstraintViolation;
import java.util.Set;
public class Jsr349Demo {
private static Logger logger =
LoggerFactory.getLogger(Jsr349Demo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerValidationService singerBeanValidationService =
ctx.getBean( SingerValidationService.class);
Singer singer = new Singer();
singer.setFirstName("J");
singer.setLastName("Mayer");
singer.setGenre(null);
singer.setGender(null);
validateSinger(singer, singerBeanValidationService);
ctx.close();
}
private static void validateSinger(Singer singer,
SingerValidationService singerValidationService) {
Set<ConstraintViolation<Singer>> violations =
singerValidationService.validateSinger(singer);
listViolations(violations);
}
private static void listViolations(
Set<ConstraintViolation<Singer>> violations) {
logger.info("No. of violations: " + violations.size());
for (ConstraintViolation<Singer> violation : violations) {
logger.info("Validation error for property: " +
violation.getPropertyPath()
+ " with value: " + violation.getInvalidValue()
+ " with error message: " + violation.getMessage());
}
}
}
如清单所示,Singer对象由违反约束的firstName和genre构成。在validateSinger()方法中,调用了SingerValidationService.validateSinger()方法,这又将调用 JSR-349 (Bean 验证)。运行该程序会产生以下输出:
[main] INFO o.h.v.i.u.Version - HV000001:
Hibernate Validator 6.0.0.Beta2
[main] INFO c.a.p.c.Jsr349Demo - No. of violations: 2
[main] INFO c.a.p.c.Jsr349Demo - Validation error for property:
firstName with value: J with error message: size must be between 2 and 60
[main] INFO c.a.p.c.Jsr349Demo - Validation error for property:
genre with value: null with error message: may not be null
如您所见,有两个违规,并且显示了消息。在输出中,您还会看到 Hibernate Validator 已经基于注释构造了默认的验证错误消息。您还可以提供自己的验证错误消息,我们将在下一节中演示。
创建自定义验证程序
除了属性级验证,我们还可以应用类级验证。例如,对于Singer类,对于乡村歌手,我们希望确保lastName和gender属性不是null,并不是说这真的很重要,只是出于教育目的。在这种情况下,我们可以开发一个定制的验证器来执行检查。在 Bean 验证 API 中,开发自定义验证器是一个两步过程。首先为验证器创建一个Annotation类型,如下面的代码片段所示。第二步是开发实现验证逻辑的类。
package com.apress.prospring5.ch10;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy=CountrySingerValidator.class)
@Documented
public @interface CheckCountrySinger {
String message() default "Country Singer should
have gender and last name defined";
Class<?> groups() default {};
Class<? extends Payload> payload() default {};
}
@Target(ElementType.TYPE)注释意味着注释应该只应用于类级别。@Constraint注释表明它是一个验证器,而validatedBy属性指定了提供验证逻辑的类。在主体中,定义了三个属性(以方法的形式),如下所示:
message属性定义了违反约束时返回的消息(或错误代码)。还可以在注释中提供默认消息。- 如果适用的话,
groups属性指定验证组。可以将验证器分配给不同的组,并在特定的组上执行验证。 - 属性指定了额外的有效负载对象(实现接口的类的)。它允许您向约束附加附加信息(例如,有效负载对象可以指示违反约束的严重性)。
下面的代码片段显示了提供验证逻辑的CountrySingerValidator类:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.obj.Singer;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CountrySingerValidator implements
ConstraintValidator<CheckCountrySinger, Singer> {
@Override
public void initialize(CheckCountrySinger constraintAnnotation) {
}
@Override
public boolean isValid(Singer singer,
ConstraintValidatorContext context) {
boolean result = true;
if (singer.getGenre() != null && (singer.isCountrySinger() &&
(singer.getLastName() == null || singer.getGender() == null))) {
result = false;
}
return result;
}
}
CountrySingerValidator实现了ConstraintValidator<CheckCountrySinger, Singer>接口,这意味着验证器检查Singer类上的CheckCountrySinger注释。实现了isValid()方法,底层的验证服务提供者(例如,Hibernate Validator)将验证下的实例传递给方法。在方法中,我们验证如果歌手是乡村音乐歌手,那么lastName和gender属性不应该是null。结果是一个指示验证结果的Boolean值。
要启用验证,请将@CheckCountrySinger注释应用到Singer类,如下所示:
package com.apress.prospring5.ch10.obj;
import com.apress.prospring5.ch10.CheckCountrySinger;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@CheckCountrySinger
public class Singer {
@NotNull
@Size(min = 2, max = 60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
//getters and setter
...
public boolean isCountrySinger() {
return genre == Genre.COUNTRY;
}
}
为了测试定制验证,将在测试类Jsr349CustomDemo中创建下面的Singer实例:
public class Jsr349CustomDemo {
...
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setGenre(Genre.COUNTRY);
singer.setGender(null);
validateSinger(singer, singerValidationService);
...
}
运行该程序会产生以下输出(另一个输出被省略):
[main] INFO o.h.v.i.u.Version - HV000001: Hibernate Validator 6.0.0.Beta2
[main] INFO c.a.p.c.Jsr349CustomDemo - No. of violations: 1
[main] INFO c.a.p.c.Jsr349CustomDemo - Validation error for property:
with value: com.apress.prospring5.ch10.obj.Singer@3116c353
with error message: Country Singer should have gender and last name defined
在输出中,您可以看到被检查的值(即Singer实例)违反了乡村歌手的验证规则,因为gender属性是null。还要注意,在输出中,属性路径是空的,因为这是一个类级别的验证错误。
使用 AssertTrue 进行自定义验证
除了实现自定义验证器,在 Bean 验证 API 中应用自定义验证的另一种方式是使用@AssertTrue注释。让我们看看它是如何工作的。对于Singer类,移除了@CheckCountrySinge r 注释,并将isCountrySinger()方法修改如下:
public class Singer {
@NotNull
@Size(min = 2, max = 60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
...
@AssertTrue(message="ERROR! Individual customer should have
gender and last name defined")
public boolean isCountrySinger() {
boolean result = true;
if (genre!= null &&
(genre.equals(Genre.COUNTRY) &&
(gender == null || lastName == null))) {
result = false;
}
return result;
}
}
正如您可能推断的那样,@CheckCountrySinger注释和CountrySingerValidator类不再是必需的。
将isCountrySinger()方法添加到Singer类中,并用@AssertTrue进行注释(在包javax.validation.constraints下)。当调用验证时,提供者将调用检查并确保结果为真。JSR-349 还提供了@AssertFalse注释来检查一些应该为假的条件。现在运行测试程序Jsr349AssertTrueDemo,您将获得与定制验证程序相同的输出。
自定义验证的注意事项
那么,对于 JSR-349 中的自定义验证,您应该使用哪种方法:自定义验证器还是@AssertTrue注释?一般来说,@AssertTrue方法实现起来更简单,您可以在域对象的代码中看到验证规则。然而,对于具有更复杂逻辑的验证器(例如,假设您需要注入一个服务类,访问一个数据库,并检查有效值),那么实现一个定制的验证器是可行的,因为您永远不希望将服务层对象注入到您的域对象中。此外,自定义验证器可以在相似的域对象中重用。
决定使用哪个验证 API
讨论了 Spring 自己的Validator接口和 Bean 验证 API 之后,您应该在您的应用中使用哪一个?JSR-349 是绝对的出路。以下是主要原因:
- JSR-349 是一个 JEE 标准,被许多前端/后端框架广泛支持(例如,Spring、JPA 2、Spring MVC 和 GWT)。
- JSR-349 提供了一个标准的验证 API,它隐藏了底层的提供者,所以你不会被绑定到一个特定的提供者。
- 从版本 4 开始,Spring 与 JSR-349 紧密集成。例如,在 Spring MVC web 控制器中,您可以用
@Valid注释对方法中的参数进行注释(在包javax.validation下),Spring 将在数据绑定过程中自动调用 JSR-349 验证。而且,在一个 Spring MVC web 应用上下文配置中,一个简单的名为<mvc:annotation-driven/>的标签将配置 Spring 自动启用 Spring 类型转换系统和字段格式化,以及对 JSR-349 (Bean Validation)的支持。 - 如果您使用 JPA 2,提供者将在持久化之前自动对实体执行 JSR-349 验证,提供另一层保护。
关于使用 JSR-349 (Bean Validation)配合 Hibernate Validator 作为实现提供者的详细信息,请参考 Hibernate Validator 的文档页: http://docs.jboss.org/hibernate/validator/ 5.1/reference/en-US/html。
摘要
在本章中,我们介绍了 Spring 类型转换系统以及现场格式化程序 SPI。除了对PropertyEditors的支持之外,您还看到了新的类型转换系统是如何用于任意类型转换的。
我们还介绍了 Spring 中的验证支持,Spring 的Validator接口,以及 Spring 中推荐的 JSR-349 (Bean 验证)支持。