2018/1/1
业务层职能不仅仅是调用DAO这么简单,事务处理是EE开发不能回避的问题;业务层硬编码进行事务管理弊端很大,Spring提供了声明式事务处理机制,它基于AOP实现,无须编写任何事务管理代码,所有工作全部在配置文件完成。
一、什么是事务?
答:
理解事务管理之前,先通过一个例子讲一下什么是事务管理:取钱:比如你去ATM机取1000块钱,大体有两个步骤:(1)输入密码金额,银行卡扣掉1000元钱;(2)ATM出1000元钱。
这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。 事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。 在企业级应用程序开发中,事务管理必不可少的技术,用来确保数据的完整性和一致性。
二、事务有哪些特质呢?
答:
·原子性(Atomicity): 事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
·一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完 成部分失败。在现实中的数据不应该被破坏。
·隔离性(Isolation): 可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
·持久性(Durability): 一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
三、Spring 框架的事务管理
Spring 作为企业级应用程序框架,它在不同的事务管理API之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理!!
(1)编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式事务中,必须在每个业务操作中包含额外的事务管理代码
(2)声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过Spring AOP框架支持声明式事务管理。
四、Spring事务的传播属性
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。
Spring定义了7种传播行为:
传播行为 | 含义 |
PROPAGATION_ MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_ NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
PROPAGATION_ NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_ NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION_ REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
其中PROPAGATION_REQUIRED为默认的传播属性。
并发事务所导致的问题
在同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能会出现许多意外的问题。并发事务所导致的问题可以分为以下三类:
① 脏读:脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
② 不可重复读:不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间更新了数据
③ 幻读:幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录
五、Spring 事务管理实例
下面是一个事务管理的小Demo:
【1】新建一个User.java 实体类:
package com.smbms.entities;
import java.util.Date;
import java.util.List;
public class User {
private Integer id;
private String userCode;
private String userName;
private String userPassword;
private Integer gender;
private Date birthday;
private String phone;
private String address ;
private Integer userRole;
private Integer createdBy;
private Date creationDate;
private Integer modifyBy;
private Date modifyDate;
private String userRoleName;
private Role role;
private List<Address> addressList;
public List<Address> getAddressList() {
return addressList;
}
public void setAddressList(List<Address> addressList) {
this.addressList = addressList;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public String getUserRoleName() {
return userRoleName;
}
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserCode() {
return userCode;
}
public void setUserCode(String userCode) {
this.userCode = userCode;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getUserRole() {
return userRole;
}
public void setUserRole(Integer userRole) {
this.userRole = userRole;
}
public Integer getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Integer createdBy) {
this.createdBy = createdBy;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public Integer getModifyBy() {
return modifyBy;
}
public void setModifyBy(Integer modifyBy) {
this.modifyBy = modifyBy;
}
public Date getModifyDate() {
return modifyDate;
}
public void setModifyDate(Date modifyDate) {
this.modifyDate = modifyDate;
}
public User(Integer id, String userCode, String userName, String userPassword, Integer gender, Date birthday,
String phone, String address, Integer userRole, Integer createdBy, Date creationDate, Integer modifyBy,
Date modifyDate) {
super();
this.id = id;
this.userCode = userCode;
this.userName = userName;
this.userPassword = userPassword;
this.gender = gender;
this.birthday = birthday;
this.phone = phone;
this.address = address;
this.userRole = userRole;
this.createdBy = createdBy;
this.creationDate = creationDate;
this.modifyBy = modifyBy;
this.modifyDate = modifyDate;
}
public User() {
super();
// TODO 自动生成的构造函数存根
}
@Override
public String toString() {
return "User [id=" + id + ", userCode=" + userCode + ", userName=" + userName + ", userPassword=" + userPassword
+ ", gender=" + gender + ", birthday=" + birthday + ", phone=" + phone + ", address=" + address
+ ", userRole=" + userRole + ", createdBy=" + createdBy + ", creationDate=" + creationDate
+ ", modifyBy=" + modifyBy + ", modifyDate=" + modifyDate + "]";
}
}
【2】新建UserMaper.xml,添加以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.smbms.dao.UserMapper">
<insert id="add" parameterType="com.smbms.entities.User">
insert into smbms_user (userCode,userName,userPassword,gender,
birthday,phone,address,userRole,createBy , createDate)
values (#{userCode},#{userName},#{userPassword},#{gender},#{birthday},#{phone}
,#{address},#{userRole},#{createdBy},#{creationDate})
</insert>
</mapper>
【3】新建UserMapper.java接口,添加以下内容:
package com.smbms.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.smbms.entities.User;
public interface UserMapper {
/**
* 下面定义的方法是用于--Spring声明式事务练习。
* */
public int add(User user);
}
【4】新建UserService.java接口和实现类UserServiceImpl.java:
UserService.java:
package com.smbms.service;
import com.smbms.entities.User;
public interface UserService {
public boolean addNewUser(User user);
}
UserServiceImpl.java:
package com.smbms.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.smbms.dao.UserMapper;
import com.smbms.entities.User;
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public boolean addNewUser(User user) {
boolean result = false;
try{
if(userMapper.add(user) == 1){
result = true;
}
}catch (RuntimeException e){
e.printStackTrace();
throw e;
}
return result;
}
public UserServiceImpl(UserMapper userMapper) {
super();
this.userMapper = userMapper;
}
public UserServiceImpl() {
super();
// TODO 自动生成的构造函数存根
}
public UserMapper getUserMapper() {
return userMapper;
}
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
【5】新建配置文件applicationContext-mybatis.xml,添加以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 数据源配置 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url">
<value><![CDATA[jdbc:mysql://127.0.0.1:3306/test?
useUnicode=true&characterEncoding=utf-8]]></value>
</property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
<!-- SqlSessionFactoryBean 配置 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 引用数据源组件 -->
<property name="dataSource" ref="dataSource"></property>
<!-- 引用Mybatis配置文件的配置 -->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>
<!-- 配置MapperScannerConfigurer,扫描生成mapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.smbms.dao"></property>
</bean>
<!-- 扫描service注解配置 -->
<context:component-scan base-package="com.smbms.service"></context:component-scan>
<!-- 事务管理器组件,以提供对事务处理的全面支持和统一管理,在切面中相当于增强处理的角色 ,需要注入数据源组件-->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 指定的事务管理器 设置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- 定义属性,声明事务规则 -->
<tx:attributes>
<tx:method name="find*" propagation="SUPPORTS"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED" />
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 定义切面,这里不知道能不能也使用注解? -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="serviceMethod" expression="execution(* com.smbms.service.*.*(..))" />
<!-- 将事务增强和切入点组合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod"/>
</aop:config>
</beans>
解释:
(1)为业务方法配置事务切面,需要用到tx 和 aop命名空间,首先需要导入它们;
(2)配置一个事务管理器组件,使用的是spring 的 DataSourceTransactionManager ,其配置方式如下:
<!-- 事务管理器组件,以提供对事务处理的全面支持和统一管理,在切面中相当于增强处理的角色 ,需要注入数据源组件-->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
它提供了对事务处理的全面支持和统一管理,在切面中相当于增强处理的角色。记住要导入事先定义好的数据源组件DataSource。
(3)与之前增强处理不一样,事务管理器组件 还可以进一步配置,以便适应不同业务方法对于事务的不同要求。使用<tx:advice>标签配置事务增强,设定事务的属性,为不同的业务方法指定具体的事务规则,其代码片段如下:
<!-- 指定的事务管理器 设置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- 定义属性,声明事务规则 -->
<tx:attributes>
<tx:method name="find*" propagation="SUPPORTS"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED" />
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
而<tx:attributes>子标签,就是定制事务属性,通过<tx:method >标签进行设置。
(4)设置完了事务规则,最后还要定义切面,将事务规则应用在指定的方法上,代码片段如下:
<!-- 定义切面,这里不知道能不能也使用注解? -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="serviceMethod" expression="execution(* com.smbms.service.*.*(..))" />
<!-- 将事务增强和切入点组合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod"/>
</aop:config>
(5)小结:加入组件,定义事务规则,AOP应用。
【6】写单元测试UserServiceImplTest.java:
package com.smbms.service;
import org.apache.log4j.Logger;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.smbms.entities.User;
public class UserServiceImplTest {
static Logger log = Logger.getLogger(UserServiceImplTest.class.getName());
@Test
public void test() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-mybatis.xml");
UserService userservice = (UserService) ctx.getBean("userService");
User user = new User();
user.setUserName("yuanshikai");
boolean result = userservice.addNewUser(user);
log.debug("testAdd result :"+result);
}
}
【7】工程说明:
(1)这是Mybatis和Spring整合练习的工程;
(2)实际使用有3个包:dao、entities、service;
(3)Mybatis配置文件内容很简洁,Spring完成大部分配置管理;
(4)通过SQLSessionTemplate的实现类对数据库进行操作;
(5)配置DAO组件并进入SqlSessionTemplate实例;
(6)配置业务Bean并注入DAO实例 ;
(7)完成的功能是:查询provider的列表,模糊查询供应商信息;
(8)与smbms05MybatisSpring的区别:去掉了实现类ProviderMapperImpl;仅仅保留ProviderMapper
接口和相关SQL映射文件,通过MapperFactoryBean注入给业务组件。
(9)此外,也新增了一个User查询,照壶画瓢;
(10)smbms06是使用MapperFactoryBean注入映射器,
而smbms07是更加方便的使用MapperScannerConfigurer注入映射器。
(11)smbms08增加新功能,实现按条件查询订单表Bill;使用resultMap做显示列表字段的自定义映射,
使用Map传参,
采用MapperFactoryBean注册映射器实现。
[bill 和 provider 表]
(12) smbms09 在 smbms08 的基础上,进行拓展,用MapperScannerConfigurer注入映射器。
(13) smbms10,声明式事务,提高在XML配置文件注册org.springframework.jdbc.datasource.DataSourceTransactionManager,
然后利用AOP切面对事务规则进行注入时。
综上,这里使用了xml对aop进行运用,从之前我们就知道,可以利用注解代替xml的配置,所以,不妨参考下一篇博文:《Spring(15):为业务层Service 配置声明式事务(下)-- 注解配置》。