Spring的AOP
本篇章中所有的代码都将会放置到git仓库中去,并且会做一个简要的说明。
一、个人理解描述
Spring中所谓的AOP就是在不修改源码的情况下,来进行增强。所谓的增强其实就是在方法执行前后添加一些额外操作。
所谓的增强,就是我们如何来对方法(以类中的方法为基本单位)处理。处理方法有五种:前置增强、后置增强等等
但是最为常用的还是利用环绕通知来进行增强,习惯于手动控制,更加精细化操作。
简单利用画图说明一下上面的描述:
以这里的CourseController和UserController为例,希望在CourseService和UserService类中的每个都开始事务操作,而且还不在修改CourseService和UserService类中源码的情况下来进行操作,不破坏原来的代码的完整性。
那么首先来写个简单的Demo来体验一下SpringAOP的强大之处。
二、案例演示
- 希望只针对UserServiceImpl类中的save无参方法来做日志打印处理。
因为工作中需要,需要利用到注解+xml的方法,所以下面没有提供纯注解的方式来进行操作。
在UserService接口中提供了三个方法来模拟实际开发过程中接口中的方法:
public interface UserService {void save();void save(String name);void save(String name, Integer age);
}
UserServiceImpl是对接口UserService的实现:
@Service("userService")
public class UserServiceImpl implements UserService {@Overridepublic void save() {System.out.println("UserService.save()");try {int randomMs = (int) (Math.random()*1000);Thread.sleep(randomMs);} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void save(String name) {System.out.println("UserService.save(String)");}@Overridepublic void save(String name, Integer age) {System.out.println("UserService.save(String, Integer)");}
}
上面两步操作中是最为简单的!下面就要来为我们的save无参方法来进行增强。
所谓的增强,就是调用save无参方法的时候,打印我们的日志。
那么来写一个增强类即可:
@Component
public class MyAdvice {/*** 环绕通知方法。调用者调用时,Spring会执行这个环绕通知方法* @param pjp 由Spring传递进来的切入点对象(目标方法、目标对象、方法实参等等封装成的对象)* @return*/public Object aroundMethod(ProceedingJoinPoint pjp){Object result = null;try {System.out.println("调用业务方法之前,我希望看下是否已经走了动态代理");long start = System.currentTimeMillis();//自己调用目标对象,得到返回值// 固定写法:调用业务方法-----所以上面的逻辑可以称之为在调用方法之前操作result = pjp.proceed(pjp.getArgs());// 下面的逻辑可以表示成调用业务逻辑方法之后的操作System.out.println("调用业务方法之后,我希望看下业务方法执行之后的结果");long end = System.currentTimeMillis();System.out.println("当前业务方法调用过程中花费的是时间有:" + (end - start) + "毫秒");} catch (Throwable throwable) {System.out.println("当前方法抛出异常,对应的异常是:"+throwable);} finally {System.out.println("无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行");}// 返回最终结果return result;}
}
因为利用到的是注解+xml的方式,所以再写一个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:aop="http://www.springframework.org/schema/aop"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--扫描组件--><context:component-scan base-package="com.guang.service.impl"/><context:component-scan base-package="com.guang.advice"/><!--一切AOP的配置,都要放在 aop:config标签里--><aop:config><aop:aspect ref="myAdvice"><aop:around method="aroundMethod" pointcut="execution(public void com.guang.service..UserServiceImpl.save())"/></aop:aspect></aop:config>
</beans>
那么来写一段代码测试:
public class XmlAopTest {@Testpublic void test(){ApplicationContext app = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");UserService userService = app.getBean("userService", UserService.class);userService.save();System.out.println("---------------------");userService.save("tom");System.out.println("---------------------");userService.save("tom", 20);}
}
打印结果如下所示:
调用业务方法之前,我希望看下是否已经走了动态代理
UserService.save()
调用业务方法之后,我希望看下业务方法执行之后的结果
当前业务方法调用过程中花费的是时间有:838毫秒
无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行
---------------------
UserService.save(String)
---------------------
UserService.save(String, Integer)
从这里可以看到,尽管利用userService调用了三个无参方法,但是只有save无参方法进行了增强。那么到底是如何做到的呢?
下面我们先介绍概念,然后从概念来进行入手,着手分析,然后再进行分析。
三、AOP相关的概念
3.1、AOP相关概念
-
目标对象(Target):要代理的/要增强的目标对象。
-
代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象
-
连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法
目标类里,所有能够进行增强的方法,都是连接点
-
切入点(PointCut):要对哪些连接点进行拦截的定义
已经增强的连接点,叫切入点
-
通知/增强(Advice):拦截到连接点之后要做的事情
对目标对象的方法,进行功能增强的代码
-
切面(Aspect):是切入点和通知的结合
-
织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入
那么画图来描述一下,我觉得更为稳妥,如下所示:
以例子来进行说明:
-
目标对象:就是courseservice所表示的单例对象;
-
代理对象:就是需要对courseservice对应的单例对象来进行代理的对象;
-
所谓的连接点:以上面的courseservice来举例,可以认为是courseservice类中的所有方法;
-
所谓的切入点:就是我们需要筛选courseservice类中的个别方法来作为特殊的点(如何筛选,那么就需要我们手写point表达式来进行选择);
-
增强:所谓的增强就是要对原来的方法来做何种操作;
-
切面:切入点+增强;
3.2、AOP开发前要明确的事项
我们要做的事情:
- 编写核心业务代码(Target目标类的目标方法)
- 编写通知类,通知类中有通知方法(Advice增强功能方法)
- 在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面
Spring的AOP做的事情:
- 生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的
- Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
小结
- AOP相关的概念/术语
- 目标类Target:要对哪个类进行增强
- 代理对象Proxy:对目标类增强后的那个代理对象
- 连接点JoinPoint:目标类里可增强的方法
- 切入点PointCut:要增强的方法
- 通知Advice:功能增强的代码
- 切面Aspect:切入点 + 通知
- 织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程
- 使用AOP,我们要做的事情:
- 编写目标类,自己的业务代码
- 编写通知类
- 配置切面
- 使用AOP,Spring做的事情
- 根据我们配置的切面,进行织入生成代理对象
四、基于XML的AOP
快速入门
1) 需求描述
- 有目标类
UserServiceImpl
,有通知类MyAdvice
- 使用XML方式AOP,对目标类
UserServiceImpl
的方法进行增强
2) 步骤分析
- 创建maven项目,导入AOP相关的依赖坐标
- 创建目标类(要增强的类,内部有切入点),创建通知类(内部有增强的方法代码)
- 修改配置文件:
- 把目标类和通知类都配置成为bean对象
- 配置切入点和通知方法(增强方法)的织入关系:配置切面
- 测试代码
3) 入门实现
1. 创建maven项目,导入坐标
<dependencies><!--Spring上下文核心包--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.0.2.RELEASE</version></dependency><!--AOP的实现包--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.9</version></dependency><!--Spring和单元测试集成--><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.0.2.RELEASE</version></dependency><!--单元测试--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency>
</dependencies>
2. 创建目标类和通知类
- 目标类:
com.guang.aop.UserServiceImpl
public class UserService{void save();
}
public class UserServiceImpl {public void save(){System.out.println("UserServiceImpl.save......");}
}
- 通知类:
com.guang.aop.MyAdvice
public class MyAdvice {public void before(){System.out.println("前置通知...");}
}
3. 修改配置文件
- 把目标类和通知类都配置到Spring配置文件中
- 配置切入和通知方法(增强方法)的织入关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!--配置要增强的目标对象--><bean id="userService" class="com.guang.service.impl.UserServiceImpl"/><!--配置切面对象--><bean id="myAdvice" class="com.guang.aop.MyAdvice"/><!--配置AOP--><aop:config><!--配置切面。切面:通知方法+切入点--><aop:aspect ref="myAdvice"><!--配置通知--><aop:before method="before" pointcut="execution(void com.guang.service.impl.UserServiceImpl.save())"/></aop:aspect></aop:config>
</beans>
注意:在xml中增加了aop的名称空间如下:
4. 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {@Autowiredprivate UserService userService;@Testpublic void testQuickStart(){userService.save();}
}
4) 步骤小结
- 导入jar包:
spring-context, aspectjweaver
- 编写目标类、编写通知类
- 配置切面
<aop:config><aop:aspect ref="通知对象"><aop:before method="通知对象里的通知方法" pointcut="切入点表达式"/></aop:aspect>
</aop:config>
AOP详解
1) 切点表达式的写法
语法:
execution([权限修饰符] 返回值类型 包名.类名.方法名(参数列表))
- 修饰符:可以省略
- 返回值类型:
- 可以指定类型。比如
String
(如果类型有歧义,就写全限定类名,比如:java.util.Date
) *
,表示任意字符。比如Str*
,或者*
- 可以指定类型。比如
- 包名:
- 可以写
.
:表示当前包下的类或者子包。比如com.guang.service
- 可以写
..
:表示当前包里所有后代类、后代包。比如com..service
*
:表示任意字符。比如:com.gua*
,com.*
- 可以写
- 类名:
- 可以指定类名。比如:
UserServiceImpl
*
表示任意字符。比如:*ServiceImpl
,*
- 可以指定类名。比如:
- 方法名:
- 可以指定方法名
*
表示任意字符。比如:save*
,*
- 参数列表:
- 可以指定类型。比如:
String,Integer
表示第一个参数是String,第二个参数是Integer类型 *
表示任意字符。比如:String, *
表示第一个参数是String,第二个参数是任意类型Str*, Integer
表示第一个参数类型Str开头,第二个参数是Integer类型
- 可以使用
..
表示任意个数、任意类型的参数
- 可以指定类型。比如:
示例
execution(public void com.guang.dao.impl.UserDao.save())
execution(void com.guang.dao.impl.UserDao.*(..))
execution(* com.guang.dao.impl.*.*(..))
execution(* com.guang.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
2) 通知的种类
通知的语法
<aop:通知类型 method="通知中的方法" pointcut="切点表达式"></aop:通知类型>
通知的类型
名称 | 标签 | 说明 |
---|---|---|
前置通知 | <aop:before> | 通知方法在切入点方法之前执行 |
后置通知 | <aop:after-returning> | 在切入点方法正常执行之后,执行通知方法 |
异常通知 | <aop:after-throwing> | 在切入点方法抛出异常时,执行通知方法 |
最终通知 | <aop:after> | 无论切入点方法是否有异常,最终都执行通知方法 |
环绕通知 | <aop:around> | 通知方法在切入点方法之前、之后都执行 |
通知示例
注意:通知方法的名称随意,我们这里是为了方便理解,才起名称为:before, after等等
-
前置通知
- 通知方法定义
MyAdvice
的before
方法:
public void before(){System.out.println("前置通知"); }
- xml配置
<aop:before method="before" pointcut="execution(void com.guang.service..*.save())"/>
- 通知方法定义
-
后置通知
- 通知方法定义
public void afterReturning(){System.out.println("后置通知"); }
- xml配置
<aop:after-returning method="afterReturning" pointcut="execution(void com.guang.service..*.save())"/>
-
环绕通知
- 通知方法定义
/*** @param pjp ProceedingJoinPoint:正在执行的切入点方法对象* @return 切入点方法的返回值*/ public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("环绕:前置通知...");Object[] args = pjp.getArgs();//切入点方法执行Object proceed = pjp.proceed(args);System.out.println("环绕:后置通知...");return proceed; }
- xml配置
<aop:around method="around" pointcut="execution(void com.guang.service..*.save())"/>
-
异常抛出通知
- 通知方法定义
public void afterThrowing(){System.out.println("抛出异常通知"); }
- xml配置
<aop:after-throwing method="afterThrowing" pointcut="execution(void com.guang.service..*.save())"/>
-
最终通知
- 通知方法定义
public void after(){System.out.println("最终通知"); }
- xml配置
<aop:after method="after" pointcut="execution(void com.guang.service..*.save())/>
3) 切点表达式的抽取
-
当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;在增强中使用pointcut-ref代替pointcut,来引入切入点表达式。
-
示例:
<!--配置AOP--> <aop:config><!--配置切入点表达式--><aop:pointcut id="myPointCut" expression="execution(void com.guang.service..*.save())"/><!--配置切面--><aop:aspect ref="myAdvice"><!--前置通知--><aop:before method="before" pointcut-ref="myPointCut"/><!--后置通知--><aop:after-returning method="afterReturning" pointcut-ref="myPointCut"/><!--异常通知--><aop:after-throwing method="afterThrowing" pointcut-ref="myPointCut"/><!--最终通知--><aop:after method="after" pointcut-ref="myPointCut"/></aop:aspect> </aop:config>
4) 小结
- 需要我们编写的内容:
- 编写目标类,编写通知类
- 配置切面
<aop:config><aop:pointcut id="xxx" expression="切入点表达式"/><aop:aspect ref="通知对象"><aop:before method="通知对象里的通知方法" pointcut-ref="xxx"/><aop:after-returning method="通知对象里的通知方法" pointcut-ref="xxx"/><aop:after-throwing method="通知对象里的通知方法" pointcut-ref="xxx"/><aop:after method="通知对象里的通知方法" pointcut-ref="xxx"/><aop:around method="通知对象里的通知方法" pointcut-ref="xxx"/></aop:aspect>
</aop:config>
- 注意环绕通知的方法
public Object aroundMethod(ProceedingJoinPoint pjp){Object reuslt = null;try{//写前置通知代码//调用目标对象的方法result = pjp.proceed(pjp.getArgs());//写后置通知代码}catch(Throwable t){//写异常通知代码}finally{//写最终通知代码}
}
五、基于注解的AOP
快速入门
1) 需求描述
- 有目标类
UserServiceImpl
,有通知类MyAdvice
- 使用注解方式的AOP对目标类
UserServiceImpl
的方法进行增强
2) 步骤分析
- 创建maven项目,导入AOP需要的依赖坐标
- 创建目标类,创建通知类
- 使用注解
@Component
标注两个类,配置成为bean对象 - 在通知类中,使用注解配置织入关系
- 使用注解
- 在配置文件中,开启组件扫描和AOP的自动代理(自动装配)
- 测试
3) 入门实现
1. 创建maven项目,导入坐标
- 注意:需要增加AOP的实现包:
aspectjweaver
<dependencies><!--Spring上下文核心包--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.0.2.RELEASE</version></dependency><!--AOP的实现包--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.9</version></dependency><!--Spring和单元测试集成--><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.0.2.RELEASE</version></dependency><!--单元测试--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency>
</dependencies>
2. 创建目标类,创建通知类
-
使用注解标注两个类,配置成为bean对象
- 实际开发中,使用
@Repository
,@Service
,@Controller
注解,按照分层进行配置
- 实际开发中,使用
-
在通知类中,使用注解配置织入关系
- 目标类
com.guang.aop.Target
public class UserService{void save(); }
@Service("userService") public class UserServiceImpl {public void save(){System.out.println("UserServiceImpl.save......");} }
- 通知类
com.guang.aop.MyAdvice
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component;//声明当前类是切面类:把切入点和通知,在这个类里进行织入,当前类就成为了一个切面类 @Aspect @Component("myAdvice") public class MyAdvice {@Before("execution(void com.guang.impl..*.save())")public void before(){System.out.println("前置通知...");}@AfterReturning("execution(void com.guang.impl..*.save()))")public void afterReturning(){System.out.println("后置通知");}@After("execution(void com.guang.impl..*.save())")public void after(){System.out.println("最终通知");}@AfterThrowing("execution(void com.guang.impl..*.save())")public void afterThrowing(){System.out.println("抛出异常通知");}/*** @param pjp ProceedingJoinPoint:正在执行的切入点方法对象* @return 切入点方法的返回值*/@Around("execution(void com.guang.impl..*.save())")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("环绕:前置通知...");//切入点方法执行Object proceed = pjp.proceed();System.out.println("环绕:后置通知...");return proceed;} }
- 目标类
4. 开启组件扫描和AOP自动代理
- 在
applicationContext.xml
中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启组件扫描--><context:component-scan base-package="com.guang"/><!--开启AOP自动代理--><aop:aspectj-autoproxy/>
</beans>
如果要使用纯注解开发,可以使用配置类代替
applicationContext.xml
,配置类如下:@Configuration //标记当前类是:配置类 @ComponentScan(basePackage="com.guang") //配置注解扫描 @EnableAspectJAutoProxy //开启AOP自动代理 public class AppConfig{ }
5. 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {@Autowiredprivate UserService userService;@Testpublic void testQuickStart(){userService.save()}
}
4) 步骤小结
AOP详解
1) 通知的种类
通知的语法
@通知注解("切入点表达式")
通知的类型
名称 | 注解 | 说明 |
---|---|---|
前置通知 | @Before | 通知方法在切入点方法之前执行 |
后置通知 | @AfterRuturning | 通知方法在切入点方法之后执行 |
异常通知 | @AfterThrowing | 通知方法在抛出异常时执行 |
最终通知 | @After | 通知方法无论是否有异常,最终都执行 |
环绕通知 | @Around | 通知方法在切入点方法之前、之后都执行 |
- 注意:
- 注解方式配置的通知,执行顺序是:
前置->最终->后置/异常
- 如果想要指定执行的顺序,就使用环绕通知
- 注解方式配置的通知,执行顺序是:
2) 切点表达式的抽取
- 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
- 抽取方法是:
- 在增强类(切面类,即被
@Aspect
标的类)上增加方法,在方法上使用@Pointcut
注解定义切入点表达式, - 在增强注解中引用切入点表达式所在的方法
- 在增强类(切面类,即被
- 示例:
@Aspect
@Component("myAdvice1")
public class MyAdvice1 {//定义切入点表达式@Pointcut("execution(void com.guang.service..*.save())")public void myPointcut(){}//引用切入点表达式//完整写法:com.guang.aop.MyAdvice.myPointcut()//简单写法:myPointcut(), 引入当前类里定义的表达式,可以省略包类和类名不写@Before("myPointcut()")public void before(){System.out.println("前置通知...");}@AfterReturning("myPointcut()")public void afterReturning(){System.out.println("后置通知");}@After("myPointcut()")public void after(){System.out.println("最终通知");}@AfterThrowing("myPointcut()")public void afterThrowing(){System.out.println("抛出异常通知");}/*@Around("myPointcut()")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("前置通知...");//切入点方法执行Object proceed = pjp.proceed();System.out.println("后置通知...");return proceed;}*/
}
3) 小结
- 在通知类上加注解
@Aspect
,声明成一个切面 - 在通知类里方法上加注解
@Before/@AfterReturning/@AfterThrowing/@After/@Around
,配置切入点表达式 - 在xml里开启aop的自动代理:
<aop:aspectj-autoproxy/>
六、纯注解的AOP
- 主要是把XML的配置,放到核心配置类上
@Configuration
@ComponentScan(basePackages="com.guang")//开启组件扫描
@EnableAspectJAutoProxy //开启AOP的自动代理
public class AppConfig{}
@Aspect
@Component("myAdvice2")
public class MyAdvice2 {//定义切入点表达式@Pointcut("execution(void com.guang.service..*.save())")public void myPointcut(){}@Before("myPointcut()")public void before(){System.out.println("前置通知...");}@AfterReturning("myPointcut()")public void afterReturning(){System.out.println("后置通知");}@After("myPointcut()")public void after(){System.out.println("最终通知");}@AfterThrowing("myPointcut()")public void afterThrowing(){System.out.println("异常通知");}
}
七、Spring事务管理
在Spring事务管理中,我推荐使用的是编程式事务,而不是声明式事务。
所谓的编程式事务就是我们手动来控制事务,而声明式事务则是有Spring来帮助我们来实现的。
而事务又是我们日常操作过程中最为常用且常用的,我们不知道Spring事务如何给我们操作的,所以建议不要将事务由Spring来进行处理,而是我们手动的来进行管理!!!
编程式事务管理
- 所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中:
- 给定的事务规则:用
TransactionDefinition
表示 - 按照…来执行提交或回滚操作:用
PlatformTransactionManager
来完成 TransactionStatus
用于表示一个运行着的事务的状态
- 给定的事务规则:用
对于PlatformTransactionManager和TransactionStatus来说,是固定的套路,但是我们最需要关注的是这里的事务规则,也就是TransactionDefinition,在下面会重点关注和介绍。
关于编程式事务的说明以及API介绍
-
编程式事务管理:通过编写代码的方式实现事务管理
-
编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用,但是与SpringAOP结合起来使用更佳。
spring 2.0 就已经提供了 xml配置的声明式事务管理的支持
-
-
以下API仅做介绍了解,用于了解Spring事务相关的API,并回顾事务相关的概念
PlatformTransactionManager
- 是Spring提供的事务管理器接口,它提供了我们常用的操作事务的方法:开启事务、提交事务等
- 注意:
PlatformTransactionManager
是接口类型,不同的dao层技术有不同的实现,例如:- dao层是jdbcTemplate或Mybatis时,实现类是:
DataSourceTransactionManager
。而我们最常用的就是mybatis,所以肯定使用的是DataSourceTransactionManager。 - dao层是Hibernate时,实现类是:
HibernateTransactionManager
- dao层是jdbcTemplate或Mybatis时,实现类是:
方法 | 返回值 | 说明 |
---|---|---|
getTransaction(TransactionDefinition td) | TransactionStatus | 开启事务,并得到事务状态 |
commit(TransactionStatus status) | 提交事务 | |
rollback(TransactionStatus status) | 回滚事务 |
TransactionDefinition
- 事务的定义信息对象,提供了以下常用方法:
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
getIsolationLevel() | int | 获取事务的隔离级别 | |
getPropogationBehavior() | int | 获取事务的传播行为 | |
getTimeout() | int | 获取超时时间 | |
isReadOnly() | boolean | 是否只读的事务 |
事务的隔离级别:
ISOLATION_DEFAULT
:默认事务隔离级别- MySql默认隔离级别:
repeatable read
- Oracle默认隔离级别:
read committed
- MySql默认隔离级别:
ISOLATION_READ_UNCOMMITTED
:读未提交–存在脏读、不可重复读、幻读ISOLATION_READ_COMMITTED
:读已提交–存在不可重复读、幻读ISOLATION_REPEATABLE_READ
:重复读–存在幻读ISOLATION_SERIALIZABLE
:串行化–没有并发问题
事务的传播行为:
用于解决业务方法调用业务方法时,事务的统一性问题的
也就是说service中的methodA方法调用methodB方法的时候,对于methodB来说,叫做传播行为,决定使用怎么样的事务来调用methodB方法。
以下三个,是要当前事务的
PROPAGATION_REQUIRED
:需要有事务。默认- 如果有事务,就使用这个事务
- 如果没有事务,就创建事务。
PROPAGATION_SUPPORTS
:支持事务- 如果有事务,就使用当前事务,
- 如果没有事务,就以非事务方式执行(没有事务)
PROPAGATION_MANDATORY
:强制的- 如果有事务,就使用当前事务
- 如果没有事务,就抛异常
以下三个,是不要当前事务的
PROPAGATION_REQUIRES_NEW
:新建的- 如果有事务,就把事务挂起,再新建事务
- 如果没有事务,新建事务
PROPAGATION_NOT_SUPPORTED
:不支持的- 如果有事务,就把事务挂起,以非事务方式执行
- 如果没有事务,就以非事务方式执行
PROPAGATION_NEVER
:非事务的- 如果有事务,就抛异常
- 如果没有事务,就以非事务方式执行
最后一个,是特殊的
PROPAGATION_NESTED
:嵌套的- 如果有事务,就在事务里再嵌套一个事务执行
- 如果没有事务,就是类似
REQUIRED
的操作
事务运行的超时时间:
超时后事务自动回滚
- 默认值-1,表示没有超时限制
- 如果有,可以以秒为单位进行设置
是否只读:
- 如果设置为只读,那么方法只能查询,不能增删改
- 通常是查询方法设置为只读
TransactionStatus
- 提供了查询事务具体运行状态的方法,常用方法如下:
方法 | 返回值 | 说明 |
---|---|---|
hasSavePoint() | boolean | 事务是否有回滚点 |
isCompleted() | boolean | 事务是否已经完成 |
isNewTransaction() | boolean | 是否是新事务 |
isRollbackOnly() | boolean | 事务是否是要回滚的状态 |
小结
- PlatformTransactionManager接口:
- 如果dao层用的是Mybatis、JdbcTemplate:用DataSourceTransactionManager
- 如果dao层用的是Hibernate:用HibernateTransactionManager
- 事务定义信息:
- 事务的隔离级别:通常使用默认
ISOLATION_DEFAULT
- 事务的传播行为:通常使用默认
PROPAGATION_REQUIRED
- 事务的超时时间:如果事务执行超时,会回滚。单位是秒。值为-1表示永不超时
- 事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改
- 事务的隔离级别:通常使用默认
- 事务状态接口
- 事务是否有回滚点
- 事务是否已经完成
- 是否是新事务
- 事务是否是要回滚的状态
示例代码一
下面也是来给出例子来进行说明展示:
给出实体类:
public class Account {private Integer id;private String name;private Float money;// 省略get/set方法
}
给出业务逻辑代码:
public interface AccountService {void transfer(String from, String to, Float money);
}
具体实现类中的代码:
@Service("accountService")
public class AccountServiceImpl implements AccountService {// 引入事务操作@Autowiredprivate AccountDao accountDao;@Autowiredprivate TransactionDefinition txDefinition;@Autowiredprivate PlatformTransactionManager txManager;@Overridepublic void transfer(String from, String to, Float money) {//开启事务,得到事务状态TransactionStatus txStatus = txManager.getTransaction(txDefinition);try {//操作daoAccount fromAccount = accountDao.findByName(from);Account toAccount = accountDao.findByName(to);fromAccount.setMoney(fromAccount.getMoney() - money);toAccount.setMoney(toAccount.getMoney() + money);accountDao.edit(fromAccount);accountDao.edit(toAccount);//提交事务txManager.commit(txStatus);} catch (Exception e) {//回滚事务txManager.rollback(txStatus);// 出现异常,进行补偿机制}}
}
那么给出dao代码:
public interface AccountDao {void edit(Account account) throws SQLException;Account findByName(String name) throws SQLException;
}
实现类代码:
package com.guang.dao.impl;import com.guang.dao.AccountDao;
import com.guang.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;import java.sql.SQLException;
import java.util.List;@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic void edit(Account account) throws SQLException {jdbcTemplate.update("update account set name=?, money=? where id=?", account.getName(), account.getMoney(), account.getId());}@Overridepublic Account findByName(String name) throws SQLException {List<Account> accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<>(Account.class), name);if (accounts == null || accounts.size() == 0) {return null;}else{return accounts.get(0);}}
}
然后给出配置文件代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.guang"/><!--事务的定义--><bean id="txDefinition" class="org.springframework.transaction.support.DefaultTransactionDefinition"><!--事务传播特性--><property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/><!--事务隔离级别--><property name="isolationLevelName" value="ISOLATION_DEFAULT"/><!--事务超时时间--><property name="timeout" value="-1"/><!--事务是否只读--><property name="readOnly" value="false"/></bean><!--定义事务管理器--><bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!--定义连接池--><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/spring"/><property name="username" value="root"/><property name="password" value="root"/></bean><!--定义JdbcTemplate--><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><constructor-arg name="dataSource" ref="dataSource"/></bean>
</beans>
从上面来看业务逻辑操作的话,可以看到代码侵入性太高!很明显不适合来使用。
@Service("accountService")
public class AccountServiceImpl implements AccountService {@Autowiredprivate TransactionDefinition txDefinition;@Autowiredprivate PlatformTransactionManager txManager;@Overridepublic void transfer(String from, String to, Float money) {//开启事务,得到事务状态TransactionStatus txStatus = txManager.getTransaction(txDefinition);try {// ....//提交事务txManager.commit(txStatus);} catch (Exception e) {//回滚事务txManager.rollback(txStatus);e.printStackTrace();}}
}
但是从这里来看的话,无非就是多用了PlatformTransactionManager和TransactionDefinition而已。
- 那么我的想法是将上面的代码抽取起来写成一个通知类型,然后配置PointCut和Advice组成Advisor。然而在我即将要动手去写的时候,在查看DefaultTransactionDefinition类的时候,我发现有继承类TransactionTemplate,我一看这玩意儿不是我们在项目中经常使用的一个类吗?以前也写过,也遇到过,但是在这里遇到的时候,我才恍然大悟。原来我们一直就是这么用的,但是从来没有去想过背后的原理,所以这也是写这篇博客的收获之一!非常幸运。
那么首先来分析一下我原来的思路(即按照切面类的想法)
-
1、针对读写类型来定义事务规则,这里涉及到四个属性,在DefaultTransactionDefinition中存在:
- propagationBehavior,默认值是PROPAGATION_REQUIRED,即表示有事务就使用这个事务,没有事务就不用;
- isolationLevel,默认值是:ISOLATION_DEFAULT,即表示默认使用数据库的事务。如果使用的是MySQL,那么就用MySQL的RR,可重复读级别;
- timeout:默认值是:TIMEOUT_DEFAULT(-1),表示永不超时,超时时间以秒为单位;
- readOnly:默认值是:false,表示的是增删改查询使用事务,查询不使用事务;
之前我想的是定义多个事务规则对象,然后配置多个环绕通知方法,在开启事务的时候,根据不同的事务规则对象来获取得到事务。
-
2、注入事务管理器,然后传入事务规则定义,获取得到当前的事务状态;
-
3、利用事务管理器通过状态来进行提交或者是回滚事务;
上面可以通过AOP切面类来进行配置,但是看到了TransactionTemplate之后,我发现根本就不需要使用到切面类就可以来进行操作。
那么首先来看下TransactionTemplate类的继承体系:
分析一下继承体系:
-
1、继承了DefaultTransactionDefinition类,所以可以用来定义事务规则信息对象;
-
2、实现了InitializingBean接口,那么肯定是要在初始化方法来做操作;
-
3、实现了TransactionOperations接口,从接口名称中可以知道这里代表的是事务;
而在TransactionTemplate类中,只有一个属性PlatformTransactionManager,而这是又是我们所需要的事务管理器。
而在InitializingBean接口的初始化方法中做了校验,如下所示:
public void afterPropertiesSet() {if (this.transactionManager == null) {throw new IllegalArgumentException("Property 'transactionManager' is required");}}
因为TransactionTemplate中存在无参方法,没有注入transactionManager对象,说明我们需要手动注入当前容器中的transactionManager对象。那么配置TransactionTemplate的时候可以配置如下所示:
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager" ref="transactionManager" /><!--针对于不同的类型来配置传播行为、隔离级别、超时时间和是否只读,因为可以继承父类中的属性--><!--还可以配置传播行为、隔离级别、超时时间和是否只读,因为可以继承父类中的属性-->
</bean>
如下所示,配置多个事务定义规则:
<bean id="transactionTemplate1" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager" ref="transactionManager" /><!--事务传播特性--><property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/><!--事务隔离级别--><property name="isolationLevelName" value="ISOLATION_DEFAULT"/><!--事务超时时间--><property name="timeout" value="-1"/><!--事务是否只读--><property name="readOnly" value="false"/>
</bean><bean id="transactionTemplat2" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager" ref="transactionManager" /><!--事务传播特性--><property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/><!--事务隔离级别--><property name="isolationLevelName" value="ISOLATION_DEFAULT"/><!--事务超时时间--><property name="timeout" value="3"/><!--事务是否只读--><property name="readOnly" value="false"/>
</bean> <bean id="transactionTemplat3" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager" ref="transactionManager" /><!--事务传播特性--><property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/><!--事务隔离级别--><property name="isolationLevelName" value="ISOLATION_DEFAULT"/><!--事务超时时间--><property name="timeout" value="3"/><!--事务是否只读--><property name="readOnly" value="true"/>
</bean>
那么只需要在使用的时候,通过以下方式注入即可:
@Autowired
@Qulifier("transactionTemplat1")
private TransactionTemplate transactionTemplate@Autowired
@Qulifier("transactionTemplat2")
private TransactionTemplate transactionTemplate@Autowired
@Qulifier("transactionTemplat3")
private TransactionTemplate transactionTemplate
通过这种方式来进行注入即可,也是比较方便的。
那么上面说完事务规则定义这块,这块应该有个疑问:在TransactionTemplate从哪里根据事务管理器获取得到事务状态的代码呢?
类似如下的代码:
@Service("accountService")
public class AccountServiceImpl implements AccountService {@Autowiredprivate TransactionDefinition txDefinition;@Autowiredprivate PlatformTransactionManager txManager;@Overridepublic void transfer(String from, String to, Float money) {//开启事务,得到事务状态TransactionStatus txStatus = txManager.getTransaction(txDefinition);try {// ....//提交事务txManager.commit(txStatus);} catch (Exception e) {//回滚事务txManager.rollback(txStatus);e.printStackTrace();}}
}
来从TransactionTemplate中找,发现就提供了一个方法:execute方法,那么重点就关注一下execute方法即可。
这个方法也是非常的简单,重点是这里的TransactionCallback接口,提前看下:
public interface TransactionCallback<T> {T doInTransaction(TransactionStatus status);
}
接口中只有一个抽象方法,又被称之为函数式接口。不懂的话接着向下看
这里的方法也是非常简单呐,相当于是已经帮助我们做好了。
给出一个使用代码示例:
这里给的是没有返回值的,所以这里给的是Void类型。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;@Service
public class TransactionalService {@Autowiredprivate TransactionTemplate transactionTemplate;public void performTransactionalOperation() {// 不建议使用下面两行代码!因为如果忘记了设置会默认值,那么后面再次使用的时候默认使用的九种方式/**nsactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);**/transactionTemplate.execute(new TransactionCallback<Void>() {public Void doInTransaction(TransactionStatus status) {// 在这里执行事务操作// 可以进行数据库操作、调用其他需要事务支持的方法等return null;}});}
}
再给出一个有值的代码且可以自己来处理事务状态。不利用事务管理中的回滚方法而已!
public Object getObject(String str) {/** 执行带有返回值<Object>的事务管理*/transactionTemplate.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus transactionStatus) {try {...//....... 业务代码return new Object();// 注意:这里没有提交代码的逻辑} catch (Exception e) {// 回滚,这里并不是利用事务管理器进行提交的,这里只是设置了一个标记而已// 在事务管理进行事务提交的时候会来检查这里的状态而已transactionStatus.setRollbackOnly();return null;}}});
}
示例代码二
在上面的分析代码中,给出示例代码二:
首先给出配置xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.guang.dao"/><context:component-scan base-package="com.guang.service.impl1."/><!--事务的定义--><bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"><!--配置事务管理器--><property name="transactionManager" ref="txManager"/><!--事务传播特性!如果调用其他方法的时候,直接使用当前方法中的事务--><property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/><!--事务隔离级别,使用数据库的操作--><property name="isolationLevelName" value="ISOLATION_DEFAULT"/><!--事务超时时间--><property name="timeout" value="-1"/><!--事务是否只读!因为是有更新操作,所以这里进行修改操作--><property name="readOnly" value="false"/></bean><!--定义事务管理器--><bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!--定义连接池--><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/spring"/><property name="username" value="root"/><property name="password" value="root"/></bean><!--定义JdbcTemplate--><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><constructor-arg name="dataSource" ref="dataSource"/></bean>
</beans>
然后给出代码:
package com.guang.service.impl1;import com.guang.dao.AccountDao;
import com.guang.domain.Account;
import com.guang.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;@Service("accountService")
public class AccountServiceImpl1 implements AccountService {@Autowiredprivate AccountDao accountDao;@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic void transfer(String from, String to, Float money) {transactionTemplate.execute((status)-> {try {// 来做事务操作Account fromAccount = accountDao.findByName(from);Account toAccount = accountDao.findByName(to);fromAccount.setMoney(fromAccount.getMoney() - money);toAccount.setMoney(toAccount.getMoney() + money);accountDao.edit(fromAccount);int i = 1 / 0;accountDao.edit(toAccount);} catch ( Exception exception) {System.out.println("执行SQL阶段出现异常!不能够进行提交");status.setRollbackOnly();}return null;});System.out.println("使用事务管理器来执行代码完成");}
}
然后将代码放置到仓库中去