Spring事务管理是Spring框架中应用最广泛和最重要的特性之一。事务管理在任何企业应用程序中都是一项琐碎的任务。我们已经学会了如何使用用于事务管理的JDBC API. Spring为事务管理提供了广泛的支持,并帮助开发人员将更多的精力放在业务逻辑上,而不是担心出现任何系统故障时数据的完整性。
目录
Spring事务管理
使用Spring事务管理的一些好处是:
- 支持声明性事务管理。在这个模型中,Spring在事务方法上使用AOP来提供数据完整性。这是首选方法,在大多数情况下都有效。
- 支持大多数事务API,如JDBC、Hibernate、JPA、JDO、JTA等。我们只需要使用适当的事务管理器实现类。例如
org.springframework.jdbc.datasource.DriverManagerDataSource
for JDBC transaction management andorg.springframework.orm.hibernate3.HibernateTransactionManager
if we are using Hibernate as ORM tool. - 通过使用支持编程事务管理
TransactionTemplate
orPlatformTransactionManager
implementation.
我们希望在事务管理器中使用的大多数特性都是由声明性事务管理支持的,因此我们将在我们的示例项目中使用这种方法。
Spring事务管理JDBC示例
我们将创建一个简单的项目,在这个项目中,我们将在一个事务中更新多个表。只有当所有JDBC语句都成功执行时,事务才应该提交,否则应该回滚以避免数据不一致。
如果您了解JDBC事务管理,您可能会争辩说,通过将连接的auto commit设置为false,并根据所有语句的结果,提交或回滚事务,我们可以轻松地做到这一点。显然我们可以做到这一点,但这将导致大量用于事务管理的锅炉板代码。同样的代码也会出现在我们寻找事务管理的所有地方,这会导致紧密耦合和不可维护的代码。
Spring声明性事务管理通过使用面向切面的编程来解决这些问题,以实现松耦合并避免在我们的应用程序中使用锅炉板代码。让我们用一个简单的例子来看看Spring是如何做到的。
在进入Spring项目之前,让我们做一些数据库设置以供使用。
Spring事务管理和数据库设置
我们将创建两个表供我们使用,并在一个事务中更新这两个表。
CREATE TABLE `Customer` (
`id` int(11) unsigned NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Address` (
`id` int(11) unsigned NOT NULL,
`address` varchar(20) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我们可以在这里定义从Address id列到Customer id列的外键关系,但为了简单起见,这里没有定义任何约束。
我们的数据库设置已经为spring事务管理项目做好了准备,让我们在spring工具套件中创建一个简单的springmaven项目。我们的最终项目结构将如下图所示。
让我们逐一研究每一个部分,它们将一起提供一个简单的带有JDBC的spring事务管理示例。
Spring事务管理和Maven依赖项
因为我们使用的是jdbcapi,所以我们必须包括Springjdbc应用程序中的依赖关系。我们还需要MySQL数据库驱动程序来连接到MySQL数据库,所以我们也将包括MySQL连接器java依赖关系。
德克萨斯州Spring如果您不需要自动包含事务,那么它通常也提供了事务管理。您可能会看到日志记录和单元测试的其他依赖项,但是我们不会使用它们中的任何一个。我们的决赛pom.xml文件文件看起来像下面的代码。
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>SpringJDBCTransactionManagement</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.7</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring -->
<spring-framework.version>4.0.2.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
<!-- Test -->
<junit.version>4.11</junit.version>
</properties>
<dependencies>
<!-- Spring and Transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Spring JDBC and MySQL Driver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.5</version>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Test Artifacts -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-framework.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
从今天起,我已经更新了Spring版的最新版本。确保MySQL数据库驱动程序与MySQL安装兼容。
Spring事务管理和模型类
我们将创建两个javabean,Customer和Address,它们将映射到我们的表中。
package com.journaldev.spring.jdbc.model;
public class Address {
private int id;
private String address;
private String country;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
package com.journaldev.spring.jdbc.model;
public class Customer {
private int id;
private String name;
private Address address;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
请注意,Customer bean将Address作为其中一个变量。当我们为Customer实现DAO时,我们将同时获取Customer和address表的数据,并对这些表执行两个单独的insert查询,这就是为什么我们需要事务管理来避免数据不一致。
Spring事务管理和DAO实现
让我们为Customer bean实现DAO,为了简单起见,我们只有一个方法在Customer和address表中插入记录。
package com.journaldev.spring.jdbc.dao;
import com.journaldev.spring.jdbc.model.Customer;
public interface CustomerDAO {
public void create(Customer customer);
}
package com.journaldev.spring.jdbc.dao;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import com.journaldev.spring.jdbc.model.Customer;
public class CustomerDAOImpl implements CustomerDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void create(Customer customer) {
String queryCustomer = "insert into Customer (id, name) values (?,?)";
String queryAddress = "insert into Address (id, address,country) values (?,?,?)";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update(queryCustomer, new Object[] { customer.getId(),
customer.getName() });
System.out.println("Inserted into Customer Table Successfully");
jdbcTemplate.update(queryAddress, new Object[] { customer.getId(),
customer.getAddress().getAddress(),
customer.getAddress().getCountry() });
System.out.println("Inserted into Address Table Successfully");
}
}
请注意,CustomerDAO实现不负责事务管理。这样我们就实现了关注点的分离,因为有时我们从第三方获得DAO实现,而我们对这些类没有控制权。
Spring声明性事务管理和服务
让我们创建一个客户服务,该服务将使用CustomerDAO实现,并在以单个方法在Customer和address表中插入记录时提供事务管理。
package com.journaldev.spring.jdbc.service;
import com.journaldev.spring.jdbc.model.Customer;
public interface CustomerManager {
public void createCustomer(Customer cust);
}
package com.journaldev.spring.jdbc.service;
import org.springframework.transaction.annotation.Transactional;
import com.journaldev.spring.jdbc.dao.CustomerDAO;
import com.journaldev.spring.jdbc.model.Customer;
public class CustomerManagerImpl implements CustomerManager {
private CustomerDAO customerDAO;
public void setCustomerDAO(CustomerDAO customerDAO) {
this.customerDAO = customerDAO;
}
@Override
@Transactional
public void createCustomer(Customer cust) {
customerDAO.create(cust);
}
}
如果您注意到CustomerManager实现,那么它只是使用CustomerDAO实现来创建客户,但是通过注解createCustomer()方法和@Transactional
annotation. That’s all we need to do in our code to get the benefits of Spring transaction management.
@事务性注解可以应用于方法以及整个类。如果你想用你的事务管理方法来注解你的事务,你应该用你的管理方法。有关批注的详细信息,请访问Java注解教程.剩下的唯一部分是连接Spring Bean以使spring事务管理示例正常工作。
Spring事务管理和Bean配置
创建一个名为&8220;的Spring Bean配置文件;spring.xml和#8221;. 我们将在测试程序中使用它来连接Spring Bean,并执行JDBC程序来测试事务管理。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
xmlns:tx="https://www.springframework.org/schema/tx"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.0.xsd
https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- Enable Annotation based Declarative Transaction Management -->
<tx:annotation-driven proxy-target-
transaction-manager="transactionManager" />
<!-- Creating TransactionManager Bean, since JDBC we are creating of type
DataSourceTransactionManager -->
<bean id="transactionManager"
>
<property name="dataSource" ref="dataSource" />
</bean>
<!-- MySQL DB DataSource -->
<bean id="dataSource"
>
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
<property name="username" value="pankaj" />
<property name="password" value="pankaj123" />
</bean>
<bean id="customerDAO">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="customerManager">
<property name="customerDAO" ref="customerDAO"></property>
</bean>
</beans>
在Spring Bean配置文件中需要注意的要点有:
- tx:注解驱动元素用于告诉Spring上下文我们正在使用基于注解的事务管理配置。事务管理器属性用于提供事务管理器bean名称。事务管理器默认值为事务管理器但为了避免混淆,我仍然保留着它。代理目标类属性用于告诉Spring上下文使用基于类的代理,没有它,您将得到运行时异常,并显示如下消息线程&8220;main&8221;中出现异常;org.springframework.beans.factory.BeanNotOfRequiredTypeException:名为‘;customerManager’;的Bean的类型必须为[com.journaldev.spring网站.jdbc.service.CustomerManagerImpl],但实际上是类型[com.sun.proxy网站.$Proxy6]
- 因为我们使用的是JDBC,所以我们正在创建类型为的transactionManager bean
org.springframework.jdbc.datasource.DataSourceTransactionManager
. This is very important and we should use proper transaction manager implementation class based on our transaction API use. - 数据源bean用于创建DataSource对象,我们需要提供数据库配置属性,比如driverClassName、url、username和password。根据本地设置更改这些值。
- 我们正在注射数据源进入之内顾客道比恩。同样,我们正在注射顾客道豆子客户经理bean定义。
我们的设置已经就绪,让我们创建一个简单的测试类来测试事务管理实现。
package com.journaldev.spring.jdbc.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.journaldev.spring.jdbc.model.Address;
import com.journaldev.spring.jdbc.model.Customer;
import com.journaldev.spring.jdbc.service.CustomerManager;
import com.journaldev.spring.jdbc.service.CustomerManagerImpl;
public class TransactionManagerMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
"spring.xml");
CustomerManager customerManager = ctx.getBean("customerManager",
CustomerManagerImpl.class);
Customer cust = createDummyCustomer();
customerManager.createCustomer(cust);
ctx.close();
}
private static Customer createDummyCustomer() {
Customer customer = new Customer();
customer.setId(2);
customer.setName("Pankaj");
Address address = new Address();
address.setId(2);
address.setCountry("India");
// setting value more than 20 chars, so that SQLException occurs
address.setAddress("Albany Dr, San Jose, CA 95129");
customer.setAddress(address);
return customer;
}
}
请注意,我显式地将地址列值设置得太长,以便在将数据插入地址表时出现异常。
现在,当我们运行测试程序时,我们得到以下输出。
Mar 29, 2014 7:59:32 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3fa99295: startup date [Sat Mar 29 19:59:32 PDT 2014]; root of context hierarchy
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Inserted into Customer Table Successfully
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.support.SQLErrorCodesFactory <init>
INFO: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into Address (id, address,country) values (?,?,?)]; Data truncation: Data too long for column "address" at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column "address" at row 1
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:100)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:907)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:968)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:978)
at com.journaldev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$$84f71441.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
at com.journaldev.spring.jdbc.main.TransactionManagerMain.main(TransactionManagerMain.java:20)
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column "address" at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:914)
at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:907)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642)
... 16 more
请注意,日志消息显示数据已成功插入customer表中,但MySQL数据库驱动程序引发的异常清楚地表明,对于address列,该值太长。现在,如果您检查Customer表,则不会在其中找到任何表示事务已完全回滚的行。
如果您想知道事务管理的魔力在哪里发生,请仔细查看日志并注意Spring框架创建的AOP和代理类。Spring框架使用Around advice为CustomerManagerImpl生成一个代理类,并且只有在方法成功返回时才提交事务。如果有任何异常,它只是回滚整个事务。我建议你读书