Spring 中的事务管理
Spring支持两种事务管理方式:编程式事务和声明式事务。官方大力推荐使用声明式事务。
- 编程式事务:将业务代码和事务代码放在一起书写,它的耦合性太高,开发中不使用
声明式事务:
- 声明式事务是建立在 AOP 的基础上的。
其本质是在方法前后进行拦截,在方法开始之前创建事务,在方法结束后根据执行情况提交或回滚事务。
- 声明式事务的优点:可以将事务代码和业务代码完全分离开发,然后通过配置的方式实现运行时组装运行。
- 声明式事务的不足:只能作用到方法级别,无法像编程式事务那样可以作用到代码块级别
- 声明式事务是建立在 AOP 的基础上的。
Spring 中事务管理相关 API
Spring中的事务控制主要就是通过这三个API实现的:
- PlatformTransactionManager 接口:事务管理器,负责事务的管理,其子类负责具体工作
- TransactionDefinition 接口:定义了事务的一些相关参数
- TransactionStatus 接口:代表事务运行的一个实时状态
三者的关系:事务管理器 通过读取 事务定义参数 进行事务管理,然后会产生一系列的 事务状态
PlatformTransactionManager 接口:
- Spring进行事务管理的一个根接口,使用其实现类做事务管理器(增强的事务处理的功能)
接口方法:
// 获取事务的状态信息 TransactionStatus getTransaction(TransactionDefinition def) // 提交事务 void commit(TransactionStatus status) // 回滚事务 void rollback(TransactionStatus status)
常用实现类:
- DataSourceTransactionManager :使用 JDBC 和 MyBatis 进行持久化数据时使用
- JpaTransactionManager :使用 JPA 进行持久化时使用(jpa,hibernate)
- HibernateTransactionManager :使用Hibernate进行持久化数据时使用
- JtaTransactionManager :事务跨越多个事务管理源【分布式事务】时使用
TransactionStatus 接口 :
- 事务运行的一个实时状态
接口方法
// 是否是新事物 boolean isNewTransaction() // 是否有回滚点 boolean hasSavepoint() // 设置为只回滚事务 void setRollbackOnly() // 是否是只回滚事务 boolean isRollbackOnly() // 刷新事务状态 void flush() // 事务是否完成 boolean isCompleted()
TransactionDefinition 接口:
- 事务的定义信息(事事务隔离级别,传播行为,是否只读事务,超时时间等)
事务隔离级别
- Spring中配置事务,支持所有的4中隔离级别
- 默认值:自动选择当前数据库合适的配置项
// 事务隔离级别相关【不设置事务隔离级别,可能引发脏读、不可重复读、幻读】 ISOLATION_READ_UNCOMMITTED 读未提交 mysq1支持四种,默认可重复度 ISOLATION_READ COMMITTED 读已提交 oracle支持两种(读己提交和串行化),默认是读已提交 ISOLATION_REPEATABLE READ 可重复度 ISOLATION SERIALIZABLE 串行化
事务传播行为:描述多个方法嵌套调用时,被调用方法对事务的支持
- PROPAGATION_REQUIRED = 0(必须有事务,这是默认值)
如果存在一个事务,则加入到当前事务。如果没有事务则开启一个新的事务。
- PROPAGATION_SUPPORTS = 1(支持有事务)
如果存在一个事务,则加入到当前事务。如果没有事务则非事务运行。
- PROPAGATION_MANDATORY = 2(强制有事务,自己还不负责创建)
如果存在一个事务,则加入到当前事务。如果没有事务,则抛出异常。
- PROPAGATION_REQUIRES_NEW = 3(必须有新的)
总是开启一个新的事务。如果存在一个事务,则将这个存在的事务挂起,重新开启一个新的事务。
- PROPAGATION_NOT_SUPPORTED = 4(不支持有事务)
总是非事务地执行,并挂起任何存在的事务。
- PROPAGATION_NEVER = 5(强制不要事务,自己还不负责挂起)
总是非事务地执行,如果存在一个活动事务,则抛出异常
- PROPAGATION_NESTED = 6(嵌套事务)
如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务, 则开启一个新的事务。
内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。
而内层事务操作失败并不会引起外层事务的回滚
- PROPAGATION_REQUIRED = 0(必须有事务,这是默认值)
- 是否只读事务
isReadOnly : true | false(默认)
只读事务: 只能查询,不能增 删 改。只读事务只能用于查询方法
- 事务超时时长
TIMEOUT_DEFAULT = -1 :事务的超时时间,需要底层数据库支持才能使用此配置,默认值是 -1,代表无限制。
超时时间:使用默认值
声明式事务(xml 配置事务)
基于XML的方式完成声明式事务配置:
- 配置事务管理器交给Spring容器管理(切面类)
- 配置事务通知
- 配置事务的AOP
<?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"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--包扫描-->
<context:component-scan base-package="cn.test"></context:component-scan>
<!--自定义的java对象:注解-->
<!--第三方jar包中的对象:xml-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///heima23"></property>
</bean>
<!--配置Spring中的事务-->
<!--1、事务管理器交给容器管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2、配置事务通知。配置service层中所有类中所有方法,对事务的要求(支持)
id="advice" :表示IOC容器中真正的通知对象的id
transaction-manager="transactionManager" :表示指定当前要对哪个事务管理器进行配置
如果事务管理器在IOC容器中的id为transactionManager,此配置可以省略。
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
<tx:method :指定目标对象中切入点的方法名指定方法对事务的要求
name :方法名称。支持通配符 *
isolation :事务的隔离级别
timeout :超时时间
propagation :传播行为(REQUIRED)
read-only :是否只读事务(false)
-->
<tx:method name="save*" propagation="REQUIRED" read-only="false"></tx:method>
<tx:method name="update*"></tx:method>
<tx:method name="delete*"></tx:method>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
<tx:method name="*"></tx:method>
</tx:attributes>
</tx:advice>
<!--3、事务的AOP配置-->
<aop:config>
<!--切入点表达式-->
<aop:pointcut id="pt" expression="execution(* cn.test.service.impl.*.*(..))"/>
<!--配置切面。<aop:advisor只有在spring的声明式事务配置时才能使用-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
</beans>
声明式事务(注解)
开启事务注解支持(xml 方式)
- 在XML配置文件中,开启事务注解的支持:事务注解驱动
- 在XML配置文件中,创建事务管理器交给容器管理
- 在需要事务的类或者方法上,使用 @Transactional 注解配置事务
<?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"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--包扫描-->
<context:component-scan base-package="cn.test"></context:component-scan>
<!--开启事务注解的支持-->
<tx:annotation-driven></tx:annotation-driven>
<!--事务管理器交给容器管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--自定义的java对象:注解-->
<!--第三方jar包中的对象:xml-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///test"></property>
</bean>
</beans>
开启事务注解支持(配置类方式)
- @EnableTransactionManagement:标注在配置类上,开启事务注解支持
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages = "cn.test")
@EnableTransactionManagement
public class SpringConfig {
/**
* 创建datasource
*/
@Bean
public DataSource getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setUrl("jdbc:mysql:///test");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
return dataSource;
}
/**
* 创建jdbctemplate
* 1、从容器中获取datasource
* 2、调用方法
*/
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
/**
* 创建事务管理器
*/
@Bean
public PlatformTransactionManager getManager(DataSource dataSource) {
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource);
return manager;
}
}
声明式事务注解的使用
@Transactional:配置事务
常用属性:
rollbackFor 属性:设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚
-
- 指定单一异常类:@Transactional(rollbackFor=Exception.class)
- 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
- readOnly 属性:是否只读事务 ( true | false(默认值) )
- propagation 属性:事务传播行为 ( SUPPORTS | REQUIRED(默认值) )
- transactionManager 属性:多个事务管理器托管在 Spring 容器中时,指定事务管理器的 bean 名称
- isolation 属性:设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况
通常使用数据库的默认隔离级别即可,基本不需要进行设置
- noRollbackFor 属性:设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚
标注位置说明:
- 标注在类上:该类中所有方法都具有相同的事务配置
- 标注在方法上:该方法具有事务配置
- 同时标注在类上和方法上:就近原则(方法上的事务配置生效)
import cn.test.dao.AccountDao;
import cn.test.domain.Account;
import cn.test.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(propagation = Propagation.SUPPORTS)
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
//@Transactional(propagation = Propagation.REQUIRED,readOnly = false)
public void transfer(String sourceName, String targetName, float money) throws Exception {
//1、根据账户名称查询两个账户
Account sourceAccount = accountDao.findByName(sourceName); //转出账户
Account targetAccount = accountDao.findByName(targetName); //转入账户
//2、操作金额转换(转出账户扣除金额,转入账户添加金额)
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);
//3、更新账户
accountDao.update(sourceAccount);
int i=1/0;
accountDao.update(targetAccount);
}
}
拓展
Spring 整合单元测试
当在单元测试中,点击 run 的时候,底层工作的其实是一个运行器,默认是 junit 提供的 ParentRunner 运行器,它是不认识Spring的环境,这也就意味着,它无法从 Spring 的容器中获取bean。
如果想要从 Spring 的容器中获取对象,需要使用 Spring 提供的运行器。
引入依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
<!--spring-junit 整合单元测试--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.6.RELEASE</version> </dependency>
@RunWith 注解:设置单元测试的运行器,通过 value 属性指定单元测试运行环境
- JUnit4.class :指定使用 JUnit4 来运行
- SpringJUnit4ClassRunner.class :Spring 测试环境
- SpringRunner.class:Spring 测试环境
注:
- SpringRunner extends SpringJUnit4ClassRunner.class
- 使用上 JUnit4.12 或更高版本以上 SpringRunner,SpringJUnit4ClassRunner 都可以使用
但是推荐使用 SpringRunner,final类型,安全
- JUnit4.12 以下版本就只能使用 SpringJUnit4ClassRunner
@ContextConfiguration 注解:指定容器的配置信息
- localtions 属性:配置文件路径
- classes 属性:配置类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class AccountJunitTest {
@Autowired
private AccountService accountService;
//测试保存
@Test
public void testInsert() {
//3、调用方法保存
Account account = new Account();
account.setMoney(100f);
account.setName("小李1");
accountService.saveAccount(account);
}
}
配置文件的模块化
若配置都集中配在了一个applicationContext.xml文件中,当开发人员过多时, 如果所有bean都配置到同一个配置文件中,会使这个文件巨大,而且也不方便维护。针对这个问题,Spring提供了多配置文件的方式,也就是所谓的配置文件模块化。
并列的多个配置文件
直接编写多个配置文件,比如说beans1.xml,beans2.xml......, 然后在创建ApplicationContext的时候,直接
传入多个配置文件。ApplicationContext act = new ClassPathXmlApplicationContext("beans1.xml","beans2.xml","...");
主从配置文件
先陪一个主配置文件,然后在里面导入其它的配置文件。<import resource="beans1.xml" /> <import resource="beans2.xml" /> <!--拓展:引入本地properties配置文件--> <context:property-placeholder location="classpath:db.properties"/>
注意事项:
- 同一个xml文件中不能出现相同名称的bean,如果出现会报错
- 多个xml文件如果出现相同名称的bean,不会报错,但是后加载的会覆盖前加载的bean,所以企业开发中尽
量保证bean的名称是唯一的。
Spring Bean 单例的高并发安全问题
Spring的bean默认都是单例的,某些情况下,单例是并发不安全的,以Controller举例,问题根源在于,若在Controller中定义成员变量,多个请求来临,进入的都是同一个单例的Controller对象,若对此成员变量的值进行修改操作,则会互相影响,无法达到并发安全(不同于线程隔离的概念)的效果。
抛出问题
多次访问此url,可以看到每次的结果都是自增的,所以这样的代码显然是并发不安全的。
@Controller
public class HomeController {
private int i;
@GetMapping("testsingleton1")
@ResponseBody
public int test1() {
return ++i;
}
}
解决方案
方案1:尽量避免使用成员变量
在业务允许的条件下,尽量避免使用成员变量,使用方法中的局部变量
方案2:使用并发安全的类
Java作为功能性超强的编程语言,API丰富,如果非要在单例bean中使用成员变量,可以考虑使用并发安全的容器,如ConcurrentHashMap、ConcurrentHashSet等,将成员变量(一般可以是当前运行中的任务列表等这类变量)包装到这些并发安全的容器中进行管理即可。
方案3:分布式或微服务的并发安全
如果还要进一步考虑到微服务或分布式服务的影响,方案2便不足以处理了,所以可以借助于可以共享某些信息的分布式缓存中间件如Redis等,这样即可保证同一种服务的不同服务实例都拥有同一份共享信息(如当前运行中的任务列表等这类变量)。
方案4:单例变原型
对web项目,可以Controller类上加注解@Scope("prototype")或@Scope("request"),对非web项目,在Component类上添加注解@Scope("prototype")。
优点:实现简单
缺点:很大程度上增大了bean创建实例化销毁的服务器资源开销
不可用方案:线程隔离类ThreadLocal
web服务器默认的请求线程池大小为10,这10个核心线程可以被之后不同的Http请求复用。
ThreadLocal的方式可以达到线程隔离,但还是无法达到并发安全。
使用 @Autowired 注解给静态变量赋值
描述:
在一些工具类中可能会用到Ioc容器中的对象,而工具类中的成员变量往往是静态的,此时使用@Autowired
注解就会出现NullpointerException
(空指针异常)。
原理剖析:
静态变量、类变量不是对象的属性,而是一个类的属性,所以静态方法是属于类(class)的,普通方法才是属于实体对象(也就是New出来的对象)的,spring注入是在容器中实例化对象,所以不能使用静态方法。
而使用静态变量、类变量扩大了静态方法的使用范围。静态方法在spring是不推荐使用的,依赖注入的主要目的,是让容器去产生一个对象的实例,然后在整个生命周期中使用他们,同时也让testing工作更加容易。
一旦使用静态方法,就不再需要去产生这个类的实例,这会让testing变得更加困难,同时也不能为一个给定的类,依靠注入方式去产生多个具有不同的依赖环境的实例,这种static field是隐含共享的,并且是一种global全局状态,spring同样不推荐这样去做。
解决方案1:将@Autowire注解加到set方法上
@Component
public class Test {
private static SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
Test.sessionFactory = sessionFactory;
}
}
解决方案2:用@PostConstruct注解
@Component
public class Test {
private static SessionFactory sessionFactory;
@Autowired
private SessionFactory sessionFactory2;
@PostConstruct
public void beforeInit() {
SessionFactory = sessionFactory2;
}
}
解决方案3:将@Autowire注解加到构造方法上
@Component
public class Test {
private static SessionFactory sessionFactory;
@Autowired
public Test(SessionFactory sessionFactory) {
Test.SessionFactory = SessionFactory;
}
}
非容器中的类调用容器中的类
描述:
使用@Autowired注入对象时,一般被注入的类都带有@Coponent、@Controller、@Service 、@repository等注解才可以。注入类和被注入类都被spring所管理,可以完成调用。但是当非容器类(没加以上注解时)使用@Autowired调用容器中的类时,注入对象为空,报空指针异常。
解决方案:
创建工具类BeanUtils,在这个工具类中的getBean可以得到容器中的类,在非容器类中使用
@Component
public class BeanUtils implements ApplicationContextAware {
/**
* 以静态变量保存ApplicationContext,可在任意代码中取出ApplicaitonContext.
*/
private static ApplicationContext context;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
@Override
public void setApplicationContext(ApplicationContext context) {
BeanUtils.context = context;
}
public static ApplicationContext getApplicationContext() {
return context;
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. 方法返回值的类型由调用者决定
*/
public static <T> T getBean(String name) {
return (T) context.getBean(name);
}
/// 获取当前环境
public String getActiveProfile() {
return context.getEnvironment().getActiveProfiles()[0];
}
}
非容器类中使用容器中的类
public class StationFactory {
Map<String, StationOperation> map = new HashMap<>();
{
map.put("定损中心主管指标表", BeanUtils.getBean("leadDSZXOperation"));
map.put("定损中心员工指标表", BeanUtils.getBean("empDSZXOperation"));
map.put("视频查勘中心主管指标表", BeanUtils.getBean("leadVideoSurveyCenterOperation"));
map.put("视频查勘中心员工指标表", BeanUtils.getBean("empVideoSurveyCenterOperation"));
map.put("视频定损中心主管指标表", BeanUtils.getBean("leadVideoDSCenterOperation"));
}
}