本章主要讨论:AOP(Aspect Oriented Programming,面向切面编程)。
上一章《15.Spring Framework [1/2]》 主要讨论:
IoC(Inverse Of Control,控制反转) DI(Dependency Injection,依赖注入)
动态代理
AOP,Aspect Oriented Programming,面向切面编程,是一种编程范式。其特点是,在不改动原始代码的基础上为其进行功能增强。
(与之类似的,还有OOP,Object Oriented Programming,面向对象编程,也是一种编程范式。)
Spring对AOP的实现方式,就是利用动态代理。
在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,从而完成功能的增强。
在Python中,也有一种方式,可以在不改动原始代码的基础上为其进行功能增强,装饰器,其基于的是闭包。
具体可以参考《基于Python的后端开发入门:3.拷贝、类型注解、闭包、装饰器和一些常用的包》 。
动态代理技术
常用的动态代理技术有两种:
JDK代理:基于接口的动态代理技术(必须要有接口)
cglib代理:基于父类的动态代理技术(可以没有接口)
cglib是为目标对象,动态的生成子对象。注意,这不是继承,是动态的生成子对象。
JDK的动态代理
目标类接口:
1 2 3 4 5 package com.kakawanyifan.proxy.jdk;public interface TargetInterface { public void method () ; }
目标类:
1 2 3 4 5 6 7 8 package com.kakawanyifan.proxy.jdk;public class TargetInterfaceImpl implements TargetInterface { @Override public void method () { System.out.println("method" ); } }
动态代理
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.kakawanyifan.proxy.jdk;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class ProxyApp { public static void main (String[] args) { TargetInterfaceImpl targetInterfaceImpl = new TargetInterfaceImpl(); TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( targetInterfaceImpl.getClass().getClassLoader(), targetInterfaceImpl.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object invoke = method.invoke(targetInterfaceImpl, args); return invoke; } } ); proxy.method(); } }
运行结果:
但这好像不太对啊?
光有代理,没有增强啊。
我们新建一个类Advice
。
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan.proxy.jdk;public class Advice { public void before () { System.out.println("before method" ); } public void after () { System.out.println("after method" ); } }
然后在ProxyApp
中的Object invoke = method.invoke(targetInterfaceImpl, args);
的前后分别执行advice.before();
和advice.after();
,这样就增强了。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.kakawanyifan.proxy.jdk;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class ProxyApp { public static void main (String[] args) { TargetInterfaceImpl targetInterfaceImpl = new TargetInterfaceImpl(); Advice advice = new Advice(); TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( targetInterfaceImpl.getClass().getClassLoader(), targetInterfaceImpl.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { advice.before(); Object invoke = method.invoke(targetInterfaceImpl, args); advice.after(); return invoke; } } ); proxy.method(); } }
运行结果:
1 2 3 before method method after method
cglib的动态代理
第一步,导包?
如果我们已经导过Spring的包的话,不需要额外再导入cglib的包,因为Spring集成了cglib的包。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.kakawanyifan.proxy.cglib;import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class ProxyApp { public static void main (String[] args) { Advice advice = new Advice(); TargetInterfaceImpl TargetInterfaceImpl = new TargetInterfaceImpl(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(TargetInterfaceImpl.class ) ; enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept (Object o, Method method, Object[] objects,MethodProxy methodProxy) throws Throwable { advice.before(); Object invoke = method.invoke(TargetInterfaceImpl, objects); advice.after(); return invoke; } }); TargetInterfaceImpl proxy = (TargetInterfaceImpl) enhancer.create(); proxy.method(); } }
运行结果:
1 2 3 before method method after method
Spring中的AOP
那么,Spring采用的是哪种方式呢?cglib?Spring中都有cglib的包。
在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
开始
概述
Spring对动态代理的代码进行了封装,封装后我们只需要关注的部分代码的编写,并通过注解或配置的方式,对指定目标的方法进行增强。
相关概念如下:
Target(目标对象):代理的目标对象。
Proxy(代理):一个类被AOP织入增强后,就产生一个代理类。
Joinpoint(连接点):连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。
Pointcut(切入点):切入点是指我们要对哪些Joinpoint进行拦截的定义
Advice(通知/增强):通知是指拦截到Joinpoint之后所要做的事情就是通知
Aspect(切面):是切入点和通知(引介)的结合
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。
环境准备
假设,现在存在一个Spring项目,如下:
pom.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version="1.0" encoding="UTF-8"?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.kakawanyifan</groupId > <artifactId > s_aop</artifactId > <version > 1.0-SNAPSHOT</version > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.22.RELEASE</version > </dependency > </dependencies > </project >
SpringConfig.java
:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan ("com.kakawanyifan" )public class SpringConfig {}
BookDao.java
:
1 2 3 4 5 6 7 8 package com.kakawanyifan.dao;public interface BookDao { public void save () ; public void delete () ; public void update () ; public void select () ; }
BookDaoImpl.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.kakawanyifan.dao.impl;import com.kakawanyifan.dao.BookDao;import org.springframework.stereotype.Repository;import java.time.Instant;@Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(Instant.now()); System.out.println("save" ); } public void delete () { System.out.println("delete" ); } public void update () { System.out.println("update" ); } public void select () { System.out.println("select" ); } }
App.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.save(); } }
运行结果:
1 2 2022-10-26T10:07:59.532Z save
在上文中,先打印当前时间,然后打印save
。
现在,我们来对其他方法进行增强。
添加依赖
1 2 3 4 5 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.9.1</version > </dependency >
解释说明:
在上一步导入的spring-context
中,已经包含了spring-aop
,所以不需要再单独导入spring-aop
。
aspectjweaver
是AspectJ的jar包。虽然Spring也有AOP实现,但是相比于AspectJ来说比较麻烦,在本文中,采用Spring整合ApsectJ的方式进行AOP开发。
定义通知类和通知
通知是将共性功能抽取出来后形成的方法。
在我们的这个案例中,共性功能指的就是打印当前时间。
示例代码:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;import java.time.Instant;public class MyAdvice { public void Method () { System.out.println(Instant.now()); } }
定义切入点
定义切入点依赖一个不具有实际意义(无参数、无返回值、方法体无实际逻辑)的方法。
在这里我们以增强BookDaoImpl
中的delete
方法为例,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import org.aspectj.lang.annotation.Pointcut;import java.time.Instant;public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} public void Method () { System.out.println(Instant.now()); } }
pt()
方法就是切入点。
@Pointcut
注解,我们会在下文专门讨论。
制作切面
切面是用来描述通知和切入点之间的关系。
根据通知类型,制作切面的注解也很多。在这个案例中,是先打印时间,制作切面的方法为在通知方法上加上注解@Before("pt()")
,pt()
为切入点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import java.time.Instant;public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @Before ("pt()" ) public void Method () { System.out.println(Instant.now()); } }
将通知类配给容器并标识其为切面类
在通知类上增加@Component
和@Aspect
两个注解。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.kakawanyifan;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.time.Instant;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @Before ("pt()" ) public void Method () { System.out.println(Instant.now()); } }
开启AOP功能
在Spring的配置类上新增注解@EnableAspectJAutoProxy
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan ("com.kakawanyifan" )@EnableAspectJAutoProxy public class SpringConfig {}
运行程序
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.delete(); } }
运行结果:
1 2 2022-10-27T00:01:29.905Z delete
至此,我们通过AOP完成了对原始方法的功能增强。
XML方式
需要在applicationContext.xml
添加如下的三行,导入aop命名空间。
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
并在applicationContext.xml
中配置织入关系。
applicationContext.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?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" 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" > <bean id ="myAdvice" class ="com.kakawanyifan.MyAdvice" > </bean > <bean id ="bookDao" class ="com.kakawanyifan.dao.impl.BookDaoImpl" > </bean > <aop:config > <aop:aspect ref ="myAdvice" > <aop:before method ="Method" pointcut ="execution(void com.kakawanyifan.dao.BookDao.delete())" > </aop:before > </aop:aspect > </aop:config > </beans >
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml" ); BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.save(); } }
运行结果:
1 2 2022-10-27T07:35:24.774Z save
机制
过程
我们讨论Spring的工作过程。
一、Spring容器启动,加载Bean
包括需要被增强的类(如:BookDaoImpl
)、通知类(如:MyAdvice
)。
但在这一步,Bean对象还没有被创建。
二、读取所有切面配置中的切入点
注意,是切面配置中的切入点。
例如,在下面的代码中,只有pt()
会被读取,pt2nd()
不会被读取。
如果用的是配置文件,不是注解的话,整体方式差不多。只是从读取注解改成了读取配置文件。
另外,根据我们关于配置文件的例子,我们很容易理解"只有pt()
会被读取,pt2nd()
不会被读取。"。
因为在配置文件中,这个配置都不全,pointcut
属性没有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.time.Instant;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.update())" ) public void pt2nd () {} @Before ("pt()" ) public void Method () { System.out.println(Instant.now()); } }
三、初始化Bean
在这一步,会去判断Bean对应的类中的方法是否匹配到切入点。
如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
验证
一、方法会增强,获取类的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; System.out.println(bookDao); System.out.println(bookDao.getClass()); } }
运行结果:
1 2 com.kakawanyifan.dao.impl.BookDaoImpl@4009e306 class com.sun.proxy.$Proxy19
二、修改MyAdvice类,不增强,获取类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.kakawanyifan;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.time.Instant;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} public void Method () { System.out.println(Instant.now()); } }
运行结果:
1 2 com.kakawanyifan.dao.impl.BookDaoImpl@60d8c9b7 class com.kakawanyifan.dao.impl.BookDaoImpl
切入点pt()
并没有被使用,所以不会被读取。因为不属于切面配置中的切入点,因此不会被增强。
不能直接打印对象,直接打印对象利用的是对象的toString方法,这个不管是不是代理对象打印的结果都是一样的,因为对toString方法进行了重写。
配置
切入点表达式
格式
1 动作关键字(访问修饰符 返回类型 包名.类名/接口.方法名(参数值类型) 异常名)
例如:
1 execution(public User com.kakawanyifan.service.UserService.findById(int))
其中:
execution
:动作关键字,描述切入点的行为动作。
public
:访问修饰符。可以是public,private等,也可以省略。
User
:返回类型。
com.kakawanyifan.service
:包名。
UserServic
:类名/接口名。
findById
:方法名。
int
:参数值类型,多个类型用逗号隔开。
通配符
*
,单个独立的任意符号
示例代码:1 2 ``` Java execution(public * com.kakawanyifan.*.UserService.find*(*))
解释说明:匹配"com.kakawanyifan"包下的,任意包中的,UserService类或接口中,所有find开头的,带有一个参数的,返回值任意的,方法。
..
:多个连续的任意符号
示例代码:1 execution(public User com..UserService.findById(..))
解释说明:匹配"com"包下的,任意包中的,UserService类或接口中,所有名称为findById的,参数任意,返回值类型是User的,方法。
通知类型
通知类型一共有5种:
前置通知
后置通知
环绕通知
返回后通知
抛出异常后通知
如果是配置方式的话,整体格式如下
1 <aop:通知类型 method ="切面类中方法名" pointcut =“切点表达式 "> </aop:通知类型 >
前置通知
注解:@Before
配置:<aop:before>
在上文,我们已经用过了前置通知,这里不做太多讨论。
后置通知
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @After ("pt()" ) public void after () { System.out.println("...after.." ); } }
环绕通知
注解:@Around
配置:<aop:around>
使用方法
@Around
,来吧!
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @Around ("pt()" ) public void around () { System.out.println("around before advice ..." ); System.out.println("around after advice ..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.delete(); } }
运行结果:
1 2 around before advice ... around after advice ...
好像不太对。
通知的内容打印出来了,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用。
使用Aroud
环绕通知,需要同时依赖ProceedingJoinPoint
及其方法proceed()
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.kakawanyifan;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.delete())" ) public void pt () {} @Around ("pt()" ) public void around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..." ); pjp.proceed(); System.out.println("around after advice ..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.delete(); } }
运行结果:
1 2 3 around before advice ... delete around after advice ...
返回值的处理
如果原始方法是有返回值的呢?
例如,我们的select
方法是有返回值的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan.dao.impl;import com.kakawanyifan.dao.BookDao;import org.springframework.stereotype.Repository;import java.time.Instant;import java.util.UUID;@Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(Instant.now()); System.out.println("save" ); } public void delete () { System.out.println("delete" ); } public void update () { System.out.println("update" ); } public String select () { System.out.println("select" ); return UUID.randomUUID().toString(); } }
我们用一个变量去接收原始方法的返回值,然后在通知方法中返回即可。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.kakawanyifan;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select())" ) public void pt () {} @Around ("pt()" ) public String around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..." ); Object ret = pjp.proceed(); System.out.println("around after advice ..." ); return ret.toString().toUpperCase(); } }
返回后通知
注解:@AfterReturning
配置:<aop:after-returning>
顾名思义,一定要返回后才会执行。如果被增强的方法出现了异常,没有返回,那么返回后通知是不会被执行。
而我们上文讨论的后置通知,只要是被增强的方法运行结束,不管有没有抛出异常,都会被执行。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select())" ) public void pt () {} @AfterReturning ("pt()" ) public void afterReturning () { System.out.println("afterReturning advice ..." ); } }
异常后通知
注解:@AfterThrowing
配置:<aop:throwing>
故名思义,一定要抛出异常了,才会被执行;如果原始方式不抛出异常,直接try-catch了,也不会被执行。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select())" ) public void pt () {} @AfterThrowing ("pt()" ) public void afterReturning () { System.out.println("afterThrowing advice ..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kakawanyifan.dao.impl;import com.kakawanyifan.dao.BookDao;import org.springframework.stereotype.Repository;import java.time.Instant;import java.util.UUID;@Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(Instant.now()); System.out.println("save" ); } public void delete () { System.out.println("delete" ); } public void update () { System.out.println("update" ); } public String select () { System.out.println("select" ); String s = "abc" ; return s.substring(2 ,4 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select())" ) public void pt () {} @AfterThrowing ("pt()" ) public void afterReturning () { System.out.println("afterThrowing advice ..." ); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 select afterThrowing advice ... Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 4 at java.lang.String.substring(String.java:1963) at com.kakawanyifan.dao.impl.BookDaoImpl.select(BookDaoImpl.java:27) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 【部分运行结果略】
切点配置方式
1 2 3 4 5 <aop:config > <aop:aspect ref ="切面类" > <aop:before method ="通知方法名称" pointcut ="切点表达式" > </aop:before > </aop:aspect > </aop:config >
当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用pointcut-ref
属性代替pointcut
属性来引用抽取后的切点表达式。
1 2 3 4 5 6 7 <aop:config > <aop:aspect ref ="myAdvice" > <aop:pointcut id ="myPointcut" expression ="execution(* com.kakawanyifan.*(..))" /> <aop:before method ="before" pointcut-ref ="myPointcut" > </aop:before > </aop:aspect > </aop:config >
获取数据
获取的数据有3类:
参数(入参)
返回值(出参)
异常
在上文,我们说通知有5类。
这5类通知和3类数据的关系如下:
获取参数(入参),所有的通知类型都可以获取参数
前置、后置、返回后和抛出异常后,利用JoinPoint
获取参数。
环绕利用ProceedingJoinPoint
获取参数。
获取返回值(出参),前置和抛出异常后,没有返回值;后置可有可无;主要关注:返回后、环绕。
获取异常,前置和返回后,不会获取异常,后置可有可无;主要关注:抛出异常后、环绕。
获取参数
JoinPoint
前置、后置、返回后和抛出异常后,利用JoinPoint
获取参数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.util.Arrays;@Component @Aspect public class MyAdvice { @Pointcut ("execution(void com.kakawanyifan.dao.BookDao.select(..))" ) public void pt () {} @Before ("pt()" ) public void before (JoinPoint joinPoint) { System.out.println("before" ); Object[] args = joinPoint.getArgs(); System.out.println(Arrays.toString(args)); } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan.dao.impl;import com.kakawanyifan.dao.BookDao;import org.springframework.stereotype.Repository;import java.time.Instant;import java.util.UUID;@Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(Instant.now()); System.out.println("save" ); } public void delete () { System.out.println("delete" ); } public void update () { System.out.println("update" ); } public void select (String id) { System.out.println("select" ); System.out.println(id + ":" + UUID.randomUUID().toString()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; bookDao.select("1" ); } }
运行结果:
1 2 3 4 before [1] select 1:9efd8992-fd18-4146-9fed-e22dda9ddb68
解释说明:因为一个方法的参数的个数是不固定的,所以使用数组更通配一些。
特别的,如果我们想在获取参数后,还对参数进行统一的处理呢?那就需要环绕通知了。
ProceedingJoinPoint
环绕利用ProceedingJoinPoint
获取参数。
需要注意的是,proceedingJoinPoint.proceed()
有两个方法,一个是无参的,一个是含参的。
在下面的例子中,利用的是含参的。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.kakawanyifan;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.util.Arrays;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select(..))" ) public void pt () {} @Around ("pt()" ) public String around (ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("before" ); Object[] args = proceedingJoinPoint.getArgs(); System.out.println(Arrays.toString(args)); args[0 ] = "abc" ; System.out.println(Arrays.toString(args)); Object ret = proceedingJoinPoint.proceed(args); return ret.toString(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan.dao.impl;import com.kakawanyifan.dao.BookDao;import org.springframework.stereotype.Repository;import java.time.Instant;import java.util.UUID;@Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(Instant.now()); System.out.println("save" ); } public void delete () { System.out.println("delete" ); } public void update () { System.out.println("update" ); } public String select (String id) { System.out.println("select" ); return id + ":" + UUID.randomUUID().toString(); } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; System.out.println(bookDao.select("1" )); } }
运行结果:
1 2 3 4 5 before [1] [abc] select abc:1238c08f-e6e7-4307-9a4f-5c6556cc8cad
获取返回值
对于返回值,只有返回后AfterReturing
和环绕Around
这两个通知类型可以获取返回值。
环绕通知获取返回值
关于环绕通知获取返回值,我们在上文都已经讨论过了。
我们不仅可以获取返回值,还可以对返回值进行修改。
这里不再赘述。
返回后通知获取返回值
返回后通知获取返回值,需要在通知方法中新增一个参数。并且:
该参数的名称需要和注解中配置的一致。
在同时有"JoinPoint"类型的参数的时候,必须在"JoinPoint"之后。
至于参数的数据类型,则没有要求。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import java.time.Instant;import java.util.Arrays;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select(..))" ) public void pt () {} @AfterReturning (value = "pt()" ,returning = "ret" ) public void afterReturning (JoinPoint joinPoint,Object ret) { System.out.println("afterReturning" ); Object[] args = joinPoint.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("afterReturning : " + ret); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import com.kakawanyifan.dao.BookDao;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class App { public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class ) ; BookDao bookDao = ctx.getBean(BookDao.class ) ; System.out.println("return : " + bookDao.select("1" )); } }
运行结果:
1 2 3 4 5 select afterReturning [1] afterReturning : 1:da7dd56d-4cf6-4dcd-9dd4-5d9cab2016c8 return : 1:da7dd56d-4cf6-4dcd-9dd4-5d9cab2016c8
获取异常
对于获取抛出的异常,只有环绕Around
和抛出异常后AfterThrowing
,这两个通知类型可以获取。
环绕通知获取异常
之前用环绕通知的时候,我们会抛出异常,现在改为用catch
获取异常。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.kakawanyifan;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.time.Instant;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select(..))" ) public void pt () {} @Around ("pt()" ) public String around (ProceedingJoinPoint proceedingJoinPoint) { Object[] args = proceedingJoinPoint.getArgs(); Object ret = "" ; try { ret = proceedingJoinPoint.proceed(args); } catch (Throwable e) { e.printStackTrace(); } String str = ret.toString().split(":" )[0 ] + ":" + Instant.now(); return str; } }
抛出异常后通知获取异常
需要利用一个参数获取通知,且该参数的名字需要和注解中配置的一样。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut ("execution(String com.kakawanyifan.dao.BookDao.select(..))" ) public void pt () {} @AfterThrowing (value = "pt()" ,throwing = "t" ) public void afterThrowing (Throwable t) { System.out.println("afterThrowing advice ..." + t); } }
事务
简介
Spring提供了声明式事务,顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在注解或配置的方式。
事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可。
在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便。
Spring为此,提供了一个平台事务管理器PlatformTransactionManager
,其部分源代码如下:
1 2 3 4 5 6 7 8 9 10 11 package org.springframework.transaction;import org.springframework.lang.Nullable;public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction (@Nullable TransactionDefinition definition) throws TransactionException ; void commit (TransactionStatus status) throws TransactionException ; void rollback (TransactionStatus status) throws TransactionException ; }
其中:
commit是用来提交事务的
rollback是用来回滚事务的
而PlatformTransactionManager
只是一个接口,不同的Dao层技术则有不同的实现类,例如Dao层技术是JDBC时,具体的实现是DataSourceTransactionManager
,部分源代码如下:
1 2 3 4 5 6 7 8 9 10 package org.springframework.jdbc.datasource;【部分代码略】 public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager , InitializingBean { 【部分代码略】 }
正如其名字所描述的,我们只需要给它一个DataSource对象,它就可以帮我们在业务层管理事务。
我们直接通过例子,来讨论转账。
环境准备
创建表
1 2 3 4 5 6 7 8 9 10 11 12 create database spring_db character set utf8;use spring_db;create table account ( id int primary key auto_increment, name varchar (35 ), money double ); insert into account values (1 , 'Tom' , 1000 );insert into account values (2 , 'Jerry' , 1000 );
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.22.RELEASE</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.2.12</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.10</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.30</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.3.23</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 2.0.7</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.2.22.RELEASE</version > <scope > test</scope > </dependency >
根据表创建模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan.pojo;import java.io.Serializable;public class Account implements Serializable { private Integer id; private String name; private Double money; 【Getter和Setter代码略】 @Override public String toString () { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}' ; } }
创建Dao接口
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan.dao;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Update;public interface AccountDao { @Update ("update account set money = money + #{money} where name = #{name}" ) void inMoney (@Param("name" ) String name, @Param ("money" ) Double money) ; @Update ("update account set money = money - #{money} where name = #{name}" ) void outMoney (@Param("name" ) String name, @Param ("money" ) Double money) ; }
创建Service接口和实现类
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan.service;public interface AccountService { public void transfer (String out,String in ,Double money) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.AccountDao;import com.kakawanyifan.service.AccountService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class AccountServiceImpl implements AccountService { @Autowired AccountDao accountDao; public void transfer (String out, String in, Double money) { accountDao.outMoney(out,money); accountDao.inMoney(in,money); } }
jdbc.properties
1 2 3 jdbc.url =jdbc:mysql://127.0.0.1:3306/spring_db jdbc.username =root jdbc.password =MySQL@2022
JdbcConfig配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.PropertySource;import javax.sql.DataSource;@PropertySource ("classpath:jdbc.properties" )public class JdbcConfig { @Value ("${jdbc.url}" ) private String url; @Value ("${jdbc.username}" ) private String userName; @Value ("${jdbc.password}" ) private String password; @Bean public DataSource dataSource () { DruidDataSource ds = new DruidDataSource(); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }
MybatisConfig配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.kakawanyifan.config;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.mapper.MapperScannerConfigurer;import org.springframework.context.annotation.Bean;import javax.sql.DataSource;public class MybatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); mapperScannerConfigurer.setBasePackage("com.kakawanyifan.dao" ); return mapperScannerConfigurer; } }
SpringConfig配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;@Configuration @ComponentScan ("com.kakawanyifan" )@PropertySource ("classpath:jdbc.properties" )@Import ({JdbcConfig.class ,MybatisConfig .class }) public class SpringConfig {}
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.kakawanyifan.service;import com.kakawanyifan.config.SpringConfig;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import java.io.IOException;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes = SpringConfig.class ) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer () throws IOException { accountService.transfer("Tom" ,"Jerry" ,100 D); } }
步骤
在需要被事务管理的方法上添加注解@Transactional
@Transactional
可以写在接口类上、接口方法上、实现类上和实现类方法上。
如果写在接口类上
该接口的所有实现类的所有方法都会有事务
如果写在接口方法上
该接口的所有实现类的该方法都会有事务
如果写在实现类上
该类中的所有方法都会有事务
如果写在实现类方法上
该方法上有事务
建议写在接口方法上。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.AccountDao;import com.kakawanyifan.service.AccountService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service public class AccountServiceImpl implements AccountService { @Autowired AccountDao accountDao; @Transactional public void transfer (String out, String in, Double money) { accountDao.outMoney(out,money); accountDao.inMoney(in,money); } }
在JdbcConfig类中配置事务管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.kakawanyifan.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.PropertySource;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;@PropertySource ("classpath:jdbc.properties" )public class JdbcConfig { @Value ("${jdbc.url}" ) private String url; @Value ("${jdbc.username}" ) private String userName; @Value ("${jdbc.password}" ) private String password; @Bean public DataSource dataSource () { DruidDataSource ds = new DruidDataSource(); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } @Bean public PlatformTransactionManager transactionManager (DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
重点关注public PlatformTransactionManager transactionManager(DataSource dataSource)
方法,该方法配置事务管理器。
开启事务
在SpringConfig的配置类中添加注解@EnableTransactionManagement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration @ComponentScan ("com.kakawanyifan" )@PropertySource ("classpath:jdbc.properties" )@Import ({JdbcConfig.class ,MybatisConfig .class }) @EnableTransactionManagement public class SpringConfig {}
运行
最后,我们可以修改AccountServiceImpl
的transfer
方法,使其报错。示例代码:
1 2 3 4 5 public void transfer (String out, String in, Double money) { accountDao.outMoney(out,money); int i = 1 /0 ; accountDao.inMoney(in,money); }
然后,重新运行test方法,运行结果:
1 2 3 4 5 6 7 8 9 java.lang.ArithmeticException: / by zero at com.kakawanyifan.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:16) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) 【部分运行结果略】
再观察一下数据库中的account表,数据没有被修改,说明事务生效了。
角色
有两个角色:
事务管理员
事务协调员
。
以刚刚的转账为例。
1 2 3 accountDao.outMoney(out,money); int i = 1 /0 ;accountDao.inMoney(in,money);
在没有开启Spring事务时:
outMoney
,会开启一个事务T1。
inMoney
,会开启一个事务T2。
而整个transfer
方法没有事务。如果在运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确;如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行;就会导致数据出现错误。
开启Spring的事务管理后:
transfer
方法上有一个@Transactional
注解,因此,在该方法上会有一个事务T。
outMoney
方法的事务T1会加入到transfer的事务T中。
inMoney
方法的事务T2也会加入到transfer的事务T中。
即,在同一个事务中。如果当业务层中出现异常,整个事务就会回滚,从而保证数据的准确性。
对于,发起事务方,我们称之为事务管理员
,在Spring中通常是业务层开启事务的方法。
对于,加入事务方,我们称之为事务协调员
,在Spring中通常指数据层方法,也可能是业务层方法。
配置
在@Transactional
注解上,可以进行参数配置。
readOnly
true
,只读事务,通常查询设为true
。
false
,读写事务,通常增删改设为false
。
timeout
:设置超时时间,单位秒。
在指定的时间之内事务没有提交成功就自动回滚。
-1
表示不超时。
rollbackFor
:当出现指定异常进行事务回滚,值为异常的类(XXX.class)。
noRollbackFor
:当出现指定异常不进行事务回滚,值为异常的类(XXX.class)。
rollbackForClassName
等同于rollbackFor
,只不过属性值为异常的类全名字符串。
noRollbackForClassName
等同于noRollbackFor
,只不过属性值为异常的类全名字符串。
isolation
事务的隔离级别,有5个枚举值:
DEFAULT
:默认隔离级别,会采用数据库的隔离级别。
READ_UNCOMMITTED
:读未提交。
READ_COMMITTED
:读已提交。
REPEATABLE_READ
:重复读取。
SERIALIZABLE
:串行化。
(关于事务的隔离级别,可以参考我们在《MySQL从入门到实践:6.事务》 的讨论。)
我们重点讨论一下rollbackFor
等和事务会滚相关的四个属性。
首先,需要强调的是,不是所有的异常都会导致事务会滚 ,比如下面的代码就不会回滚。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.AccountDao;import com.kakawanyifan.service.AccountService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.io.IOException;@Service public class AccountServiceImpl implements AccountService { @Autowired AccountDao accountDao; public void transfer (String out, String in, Double money) throws IOException { accountDao.outMoney(out,money); if (true ){ throw new IOException(); } accountDao.inMoney(in,money); } }
因为,Spring的事务只会对Error异常
和RuntimeException异常
及其子类进行事务会滚,其他的异常类型是不会回滚的。
在上例中的IOException
不符合上述条件所以不回滚。
如果一定要让IOException
也会滚,可以通过rollbackFor属性来设置。在设置后,原有的会回滚的异常不受会影响。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.AccountDao;import com.kakawanyifan.service.AccountService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.io.IOException;@Service public class AccountServiceImpl implements AccountService { @Autowired AccountDao accountDao; @Transactional (rollbackFor = IOException.class ) public void transfer (String out , String in , Double money ) throws IOException { accountDao.outMoney(out,money); if (true ){ throw new IOException(); } accountDao.inMoney(in,money); } }
传播行为
环境准备
现在,假设我们不但需要转账,还需要同时在一张日志表中记录下来。
而且,无论转账是否成功,都需要在日志表中记录。
一、创建日志表
1 2 3 4 5 6 create table log ( id int primary key auto_increment, info varchar (255 ), createDate datetime )
二、添加LogDao接口
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan.dao;import org.apache.ibatis.annotations.Insert;public interface LogDao { @Insert ("insert into log (info,createDate) values(#{info},now())" ) void log (String info) ; }
三、LogService接口与实现类
1 2 3 4 5 6 7 package com.kakawanyifan.service;public interface LogService { void log (String out, String in, Double money) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.LogDao;import com.kakawanyifan.service.LogService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service public class LogServiceImpl implements LogService { @Autowired LogDao logDao; @Override public void log (String out, String in, Double money) { logDao.log("trans:" + money + " from:" + out + " to:" + in); } }
在转账的业务中添加记录日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.LogDao;import com.kakawanyifan.service.LogService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service public class LogServiceImpl implements LogService { @Autowired LogDao logDao; @Override public void log (String out, String in, Double money) { logDao.log("trans:" + money + " from:" + out + " to:" + in); } }
运行
在转账业务之间出现了异常,account
表确实回滚,但是log
表未添加数据,这不符合我们说的无论转账是否成功,都要记录。
那么,为什么会失败呢?
因为日志的记录与转账操作隶属同一个事务,同成功同失败。
那么,怎么解决呢?
这就涉及到事务传播行为了。
propagation
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
配置事务传播行为,需要利用propagation
属性。
我们为log
方法添加@Transactional
注解,并配置propagation
属性为REQUIRES_NEW
。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.kakawanyifan.service.impl;import com.kakawanyifan.dao.LogDao;import com.kakawanyifan.service.LogService;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 public class LogServiceImpl implements LogService { @Autowired LogDao logDao; @Override @Transactional (propagation = Propagation.REQUIRES_NEW) public void log (String out, String in, Double money) { logDao.log("trans:" + money + " from:" + out + " to:" + in); } }
这样就能实现我们想要的结果,不管转账是否成功,都会记录日志。
propagation的可选值有:
REQUIRED(默认)
事务管理员开启事务T时,事务协调员加入T。
事务管理员无操作时,事务协调员新建事务T2。
REQUIRES_NEW
事务管理员开启事务T时,事务协调员依然新建事务T2。
事务管理员无操作时,事务协调员新建事务T2。
SUPPORTS
事务管理员开启事务T时,事务协调员加入T。
事务管理员无操作时,事务协调员无操作。
NOT_SUPPORTED
事务管理员开启事务T时,事务协调员无操作。
事务管理员无操作时,事务协调员无操作。
MANDATORY
事务管理员开启事务T时,事务协调员加入T。
事务管理员无操作时,事务协调员报错。
NEVER
事务管理员开启事务T时,事务协调员报错。
事务管理员无操作时,事务协调员无操作。
NESTED
设置savePoint,一旦事务回滚,事务将回滚到savePoint处。
XML方式
我们以《15.Spring Framework [1/2]》 的"整合MyBatis"的"XML方式"的例子为基础,讨论事务的"XML方式"。
导入tx命名空间
需要在applicationContext.xml
添加如下的三行,导入aop命名空间。
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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" >
配置事务增强
1 2 3 4 5 6 7 8 9 10 11 <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="*" /> </tx:attributes > </tx:advice >
配置事务AOP织入
1 2 3 4 5 <aop:config > <aop:pointcut id ="myPointcut" expression ="execution(* com.kakawanyifan.service.*.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="myPointcut" > </aop:advisor > </aop:config >
所以,还需要导入AOP相关的包。
1 2 3 4 5 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.9.1</version > </dependency >
测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan.service;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import java.io.IOException;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (locations ={"classpath:applicationContext.xml" })public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer () throws IOException { accountService.transfer("Tom" ,"Jerry" ,100 D); } }
切点方法的事务参数
1 2 3 4 5 <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="*" /> </tx:attributes > </tx:advice >
其中,<tx:method>
代表切点方法的事务参数的配置,例如:
1 2 3 4 5 6 <tx:method name ="transfer" isolation ="REPEATABLE_READ" propagation ="REQUIRED" timeout ="-1" read-only ="false" />
name
:切点方法名称
isolation
:事务的隔离级别
propogation
:事务的传播行为
timeout
:超时时间
read-only
:是否只读
常见不当使用
有三种常见的不当使用:
不必要
不生效
不回滚
不必要
无需事务的业务
在没有事务操作的业务方法上使用@Transactional
注解,比如:用在仅有查询或者一些 HTTP 请求的方法,虽然加上影响不大,但从编码规范的角度来看还是不够严谨,建议去掉。
反例:
1 2 3 4 5 @Transactional public String testQuery () { standardBak2Service.getById(1L ); return "testB" ; }
事务范围过大
有些同学为了省事直接将@Transactional
注解加在了类上或者抽象类上,这样做导致的问题就是类内的方法或抽象类的实现类中所有方法全部都被事务管理。增加了不必要的性能开销或复杂性,建议按需使用,只在有事务逻辑的方法上加@Transactional
。
反例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Transactional public abstract class BaseService {} @Slf 4j@Service public class TestMergeService extends BaseService { private final TestAService testAService; public String testMerge () { testAService.testA(); return "ok" ; } }
如果在类中的方法上添加@Transactional
注解,将覆盖类级别的事务配置。
例如,类级别上配置了只读事务,方法级别上的@Transactional
注解也会覆盖该配置,从而启用读写事务。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Transactional (readOnly = true )public class TestMergeService { private final TestBService testBService; private final TestAService testAService; @Transactional public String testMerge () { testAService.testA(); testBService.testB(); return "ok" ; } }
不生效
方法权限问题
不要把@Transactional
注解加在private
级别的方法上!
我们知道@Transactional
注解依赖于SpringAOP切面来增强事务行为,这个AOP是通过代理来实现的,而private
方法恰恰不能被代理的,所以AOP对 private
方法的增强是无效的,@Transactional
也就不会生效。
反例:
1 2 3 4 5 6 7 8 9 @Transactional private String testMerge () { testAService.testA(); testBService.testB(); return "ok" ; }
那如果我在public的testMerge()
方法内调用private
的方法事务会生效吗?
这个会事务会生效。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Transactional public String testMerge () throws Exception { ccc(); return "ok" ; } private void ccc () { testAService.testA(); testBService.testB(); }
被final、static修饰方法
被final、static修饰的方法上加@Transactional
也不会生效。
final
修饰的方法不能被子类重写,事务相关的逻辑无法插入到final
方法中,代理机制无法对final
方法进行拦截或增强。
static
静态方法属于类本身的而非实例,因此代理机制是无法对静态方法进行代理或拦截的。
示例代码:
1 2 3 4 5 6 7 @Transactional public static void b () {} @Transactional public final void b () {}
同类内部方法调用问题
很多资料都会说"在一个方法内部调用另一个同类的方法时,这种调用是不会经过代理的,因此事务管理不会生效。"
但这说法比较片面,要分具体情况。
也有可能生效
例如,testMerge()
方法开启事务,调用同类非事务的方法a()
和b()
,此时b()
抛异常,根据事务的传播性a()
、b()
事务均生效。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Transactional public String testMerge () { a(); b(); return "ok" ; } public void a () { standardBakService.save(testAService.buildEntity()); } public void b () { standardBak2Service.save(testBService.buildEntity2()); throw new RuntimeException("b error" ); }
如果testMerge()
方法未开启事务,并且在同类中调用了非事务方法a()
和事务方法b()
,当b()
抛出异常时,a()
和b()
的事务都不会生效。因为这种调用直接通过this
对象进行,未经过代理,因此事务管理无法生效。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public String testMerge () { a(); b(); return "ok" ; } public void a () { standardBakService.save(testAService.buildEntity()); } @Transactional public void b () { standardBak2Service.save(testBService.buildEntity2()); throw new RuntimeException("b error" ); }
独立的Service类
要想b()
方法的事务生效也容易,最简单的方法将它剥离放在独立的Service类注入使用,交给Spring管理就行了。不过,这种方式会创建很多类。
示例代码:
1 2 3 4 5 6 7 8 9 10 @Slf 4j@Service public class TestBService { @Transactional public void b () { standardBak2Service.save(testBService.buildEntity2()); throw new RuntimeException("b error" ); } }
自注入方式
或者通过自己注入自己的方式解决,尽管解决了问题,逻辑看起来很奇怪,它破坏了依赖注入的原则,虽然Spring支持我们这样用,还是要注意下循环依赖的问题。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Slf 4j@Service public class TestMergeService { @Autowired private TestMergeService testMergeService; public String testMerge () { a(); testMergeService.b(); return "ok" ; } public void a () { standardBakService.save(testAService.buildEntity()); } @Transactional public void b () { standardBak2Service.save(testBService.buildEntity2()); throw new RuntimeException("b error" ); } }
手动获取代理对象
我们手动获取代理对象调用b()
方法也可以。
通过AopContext.currentProxy()
方法返回当前的代理对象实例,这样调用代理的方法时,就会经过AOP的切面,@Transactional
注解就会生效了。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf 4j@Service public class TestMergeService { public String testMerge () { a(); ((TestMergeService) AopContext.currentProxy()).b(); return "ok" ; } public void a () { standardBakService.save(testAService.buildEntity()); } @Transactional public void b () { standardBak2Service.save(testBService.buildEntity2()); throw new RuntimeException("b error" ); } }
Bean未被Spring管理
因为@Transactional
注解通过AOP管理事务,而AOP依赖于代理机制;所以,Bean必须由Spring管理实例。
要确保为类加上如@Controller
、@Service
或@Component
注解,让其被Spring所管理。
异步线程调用
如果我们在testMerge()
方法中使用异步线程执行事务操作,通常也是无法成功回滚的。
例如,testMerge()
方法在事务中调用了testA()
,testA()
方法中开启了事务。接着,在testMerge()
方法中,我们通过一个新线程调用了testB()
,testB()
中也开启了事务,并且在testB()
中抛出了异常。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Transactional public String testMerge () { testAService.testA(); new Thread(() -> { try { testBService.testB(); } catch (Exception e) { throw new RuntimeException(); } }).start(); return "ok" ; } @Transactional public String testB () { DeepzeroStandardBak2 entity2 = buildEntity2(); dataImportJob2Service.save(entity2); throw new RuntimeException("test2" ); } @Transactional public String testA () { DeepzeroStandardBak entity = buildEntity(); standardBakService.save(entity); return "ok" ; }
在上文的例子中,testA()
不回滚,testB()
回滚。
testA()
无法回滚是因为没有捕获到新线程中testB()
抛出的异常;testB()
方法可以回滚,是因为事务管理器只对当前线程中的事务有效,因此在新线程中执行的事务会回滚。
由于在多线程环境下,Spring的事务管理器不会跨线程传播事务,事务的状态(如事务是否已开启)是存储在线程本地的ThreadLocal来存储和管理事务上下文信息。
这意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。
不回滚
异常被吞了
在代码中手动try...catch
捕获了异常,没抛出异常。
例如,testMerge()
方法开启了事务,并调用了非事务方法testA()
和testB()
,同时在testMerge()
中捕获了异常。如果testB()
中发生了异常并抛出,但testMerge()
捕获了这个异常而没有继续抛出,Spring事务将无法捕获到异常,从而无法进行回滚。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @RequiredArgsConstructor @Slf 4j@Service public class TestMergeService { private final TestBService testBService; private final TestAService testAService; @Transactional public String testMerge () { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); } return "ok" ; } } @Service public class TestAService { public String testA () { standardBakService.save(entity); return "ok" ; } } @Service public class TestBService { public String testB () { standardBakService.save(entity2); throw new RuntimeException("test2" ); } }
为了确保Spring事务能够正常回滚,需要我们在catch块中主动重新抛出它能够处理的RuntimeException或者Error类型的异常。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Transactional public String testMerge () { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); throw new RuntimeException(e); } return "ok" ; }
事务无法捕获的异常
Spring的事务默认会回滚RuntimeException及其子类,以及Error类型的异常。
如果抛出的是其他类型的异常,继承自Exception
但不继承自RuntimeException
的异常。例如:SQLException
、DuplicateKeyException
,事务将不会回滚。
所以,我们在主动抛出异常时,要确保该异常是事务能够捕获的类型。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Transactional public String testMerge () throws Exception { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); throw new Exception(e); } return "ok" ; }
如果一定要抛出默认情况下不会导致事务回滚的异常,务必在@Transactional
注解的rollbackFor
参数中明确指定该异常,这样才能进行回滚。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Transactional (rollbackFor = Exception.class ) public String testMerge () throws Exception { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); throw new Exception(e); } return "ok" ; }
当然,为了减少出现BUG的风险,可以考虑在使用@Transactional
注解时,将rollbackFor
参数设置为Exception
或Throwable
,这样可以扩大事务回滚的范围。
(毕竟,谁会记得哪些异常属于运行时异常,哪些属于检查型异常呢?)
自定义异常范围问题
针对不同业务定制异常类型是比较常见的做法,@Transactional
注解的rollbackFor
参数支持自定义的异常,但我们往往习惯于将这些自定义异常继承自RuntimeException
。
那么这就出现和上文类似的问题,事务的范围不足,许多异常类型仍然无法触发事务回滚。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Transactional (rollbackFor = CustomException.class ) public String testMerge () throws Exception { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); throw new Exception(e); } return "ok" ; }
想要解决这个问题,可以在catch中主动抛出我们自定义的异常。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 @Transactional (rollbackFor = CustomException.class ) public String testMerge () throws Exception { try { testAService.testA(); testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); throw new CustomException(e); } return "ok" ; }
嵌套事务问题
例如,我们在testMerge()
方法中调用了事务方法testA()
和事务方法testB()
,此时不希望testB()
抛出异常让整个testMerge()
都跟着回滚;这就需要单独try catch
处理testB()
的异常,不让异常在向上抛。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @RequiredArgsConstructor @Slf 4j@Service public class TestMergeService { private final TestBService testBService; private final TestAService testAService; @Transactional public String testMerge () { testAService.testA(); try { testBService.testB(); } catch (Exception e) { log.error("testMerge error:{}" , e); } return "ok" ; } } @Service public class TestAService { @Transactional public String testA () { standardBakService.save(entity); return "ok" ; } } @Service public class TestBService { @Transactional public String testB () { standardBakService.save(entity2); throw new RuntimeException("test2" ); } }