AOP介绍
什么是AOP?
AOP(Aspect Oriented Programming)直译过来就是面向切面编程,指导开发者如何组织程序结构。
我们先回顾一下OOP(Object Oriented Programming)面向对象编程,OOP作为面向对象编程的模式,获得了巨大的成果,OOP的主要功能是数据封装、继承和多态。
而AOP是一种编程思想,是面向对象编程(OOP)的一种补充,面向对象编程将程序抽象成各个层次,而面向切面编程将程序抽象成各个切面。
作用:在不惊动原始设计的基础上为其进行功能增强
。
Spring理念:无侵入式/无入侵式
。
从该图可以很形象地看到,所谓漆面,相当于应用对象的横切点,我们可以将其单独抽象为单独的模块。
为什么需要AOP?
我们在做开发时,经常会遇到在多个模块之间有某段重复的代码,在传统的面向过程编程中,我们一般的做法是将这些重复的代码封装成一个方法,然后在需要的地方直接调用这个方法,这样当这段代码需要修改时,我们只需要修改这个方法就可以了。
然而需求总是多变的,有一天,我们需要在这个重复的代码进行增加某功能,但原来的方法不能改变,我们又需要将新功能加进去,再独立一个方法出来,然后在需要的地方分别调用这个新方法,又或者说,哪一天我们不需要这个方法了,我们还得删除每一处调用该方法的地方,这样的处理方式,显然是非常繁琐的。
AOP就出现了,在涉及到多个地方具有相同的修改问题我们都可以通过AOP解决。
AOP术语?
AOP领域中的特性术语:
连接点(JoinPoint)
:程序执行过程中的能够插入切面的任意位置(或者叫点),这个点(粒度)可以是方法的调用、异常抛出、设置变量。
通知(Advice)
:在切入点处执行的操作,也就是共性功能。
在SpringAOP中,功能最终以方法的形势呈现。
通知类
:定义通知的类。
切入点(Pointcut)
:匹配连接点的式子。
在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法。
一个具体方法: site.hikki.dao
包下的BookDao接口中的无形参返回值的save方法。
**匹配多个方法:**所有的save,所有的get开头的方法,所有以Dao结尾的接口中任意方法,所有带有一个参数的方法。
切面(Aspect)
:描述通知和切入点的对应关系。
引入(Introduction)
:引入允许我们向享有的类添加新的方法或属性。
织入(Weaving)
:将增强处理添加到目标对象中,并创建一个呗增强的对象,这个过程叫织入。
AOP实现分类
AOP要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件增加某种通用的功能。
AOP的本质是由AOP框架修改业务组件的多个方法的源代码,我们叫他为代理模式。
静态AOP实现。AOP框架在编译阶段对程序源代码进行修改,生成了静态的AOP代理类(生成的.class文件已经被改掉了,需要使用特定的编译器),比如AspectJ。
动态AOP实现。AOP框架在运行阶段对动态生成代理对象(在内存中已JDK动态代理,或Cglib动态地生成AOP代理类),如SpringAOP。
AOP实现比较:
类别
机制
原理
优点
缺点
静态AOP
静态织入
在编译期,切面直接以字节码的形式编译到目标字节码文件中
对系统性能影响
灵活性不够
动态AOP
JDK动态代理
在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中
相对静态AOP更加灵活
切入的关注点需要实现接口。对系统有一点性能影响
动态字节码生成
CGLIB
在运行期,目标类加载后,动态生成目标类的子类,将且没按逻辑加入到子类中
没有接口也可以织入
扩展类的示例方法用final修饰时,则无法进行织入
自定义类加载器
在运行期,目标类加载前,将切面逻辑加到目标字节码里
可以对绝大部分类进行织入
代码中如果使用了其他类加载器,则这些类不会织入
字节码转换
在运行期,所有类加载器加载字节码前进行拦截
可以对所有类进行织入
AOP案例
需求
创建一个简单的Spring项目,在不改变updateStudent()方法的前提下,在调用updateStudent()方法执行时,先执行findByNum()方法进行数据的查找。
步骤
导入AOP相关依赖
定义通知类,制作通知
定义切入点
绑定切入点和通知关系
定义通知类收Spring容器管理,并且定义当前类为切面类
开启Spring注解对AOP注解驱动支持
项目结构分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 E:. │ App.java │ ├─advice │ DaoAdvice.java │ ├─config │ SpringConfig.java │ └─dao │ StudentDao.java │ └─impl StudentDaoImpl.java
导入相关依赖
需要两个依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.4</version > </dependency >
定义Dao接口和实现类
StudentDao接口
1 2 3 4 5 6 7 8 9 package site.hikki.dao;public interface StudentDao { void findAll () ; void findByNum (int num) ; void updateStudent () ; void addStudent () ; void delStudent () ; }
StudentDaoImpl实现类
并表明该类是一个bean对象。(在数据层一般使用@Repository而不使用@Controller)
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 site.hikki.dao.impl;import org.springframework.stereotype.Repository;import site.hikki.dao.StudentDao;@Repository public class StudentDaoImpl implements StudentDao { @Override public void findAll () { System.out.println("findALl.." ); } @Override public void findByNum (int num) { System.out.println("findByNum:" +num); } @Override public void updateStudent () { System.out.println("updateStudent..." ); } @Override public void addStudent () { System.out.println("addStudent..." ); } @Override public void delStudent () { System.out.println("delStudent..." ); } }
定义通知类
定义通知
定义通知的意思就是:需要执行的方法或操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 package site.hikki.advice;import org.springframework.beans.factory.annotation.Autowired;import site.hikki.dao.StudentDao;public class DaoAdvice { @Autowired private StudentDao studentDao; public void before () { studentDao.findByNum(5 ); } }
定义切入点
定义切入点的意思就是:在哪里进行操作,在运行到哪一个方法进行上面的通知操作。
继续在DaoAdvice
类添加以下内容:
1 2 @Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())") private void pt () {}
绑定通知和切入点的关系 & 定义切面类
绑定关系 ,只需要在通知方法上添加通知类型即可。
绑定关系后,还需要在该类上面声明该类受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 package site.hikki.advice;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import site.hikki.dao.StudentDao;@Component @Aspect public class DaoAdvice { @Autowired private StudentDao studentDao; @Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())") private void pt () {} @Before("pt()") public void before () { studentDao.findByNum(5 ); } }
定义配置类 & 开启AOP切面功能
@Configuration
:表示该类是配置类
@ComponentScan("site.hikki")
:扫描bean对象范围
@EnableAspectJAutoProxy
:开启AOP切面功能
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan("site.hikki") @EnableAspectJAutoProxy public class SpringConfig {}
程序入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package site.hikki;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import site.hikki.config.SpringConfig;import site.hikki.dao.StudentDao;public class App { public static void main (String[] args) { AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao bean = stx.getBean(StudentDao.class); bean.updateStudent(); } }
从结果可以看到,我们在调用updateStudent()方法前,先执行了findByNum()方法。
AOP工作流程
流程图
Spring容器启动
读取所有切面配置中的切入点
初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
匹配失败,创建对象
匹配成功,创建原始对象(目标对象 )的代理 对象
获取bean执行方法
流程说明
1.Spring容器启动
Spring容器启动:
很好理解,在程序入口AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext(SpringConfig.class);
已经创建了Ioc容器。
2.读取所有切面配置中的切入点
读取所有切面配置中的切入点:
即那些定义好了切入点,并且也和通知绑定了关系才会被读取。
如果有部分是定义好了切入点,但是没有和通知绑定关系的,则不会被读取。
如下案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Autowired private StudentDao studentDao;@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())") private void ptx () {}@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())") private void pt () {}@Before("pt()") public void before () { studentDao.findByNum(5 ); }
在上面代码中,只有pt()方法和通知绑定了关系,ptx()方法没有被使用,即没有和通知绑定关系,这时,Spring只会读取pt这个切入点。
其实很好理解,你可以这么想,如果我定义了一千一万个切入点,但是他们都没有被使用,我如果都扫描,岂不是需要消耗我大量的时间,并且这些切入点都没没有被使用,读取了也是浪费时间,我为什么要读取?是吧,所以,我们只读取那些会被使用的切入点,这样才能提高运行效率。
3.初始化bean
初始化bean,判定bean对应的类中的方法是否匹配到任意切入点:
如果匹配失败了,Spring会自动创建一个新的对象。
Q:什么是匹配失败了?
A:好比我们使用下面的代码,我们定义的切入点的位置不存在就是匹配失败,找不到这个切入点,或者这个切入点的路径写错了,反正就是找不到这个切入点的位置。
匹配失败后,Spring就会帮我们新建一个对象。
Q:那新建的对象是什么呢?
A:新建的对象是在程序入口调用的那个Bean对象对应的实现类。
下面定义的切入点具体到方法是updateStudent1111()
,实际上我们是没有这个方法的,我们去打印一下该对象看看是否会自动新建对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component @Aspect public class DaoAdvice { @Autowired private StudentDao studentDao; @Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent1111())") private void pt () {} @Before("pt()") public void before () { studentDao.findByNum(5 ); } }
打印对象结果:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao studentDao = stx.getBean(StudentDao.class); System.out.println(studentDao); System.out.println(studentDao.getClass()); }
从打印结果来看,匹配失败了,Spring自动帮我们新建了一个对象,并且和原来的对象不是同一个。
匹配成功后,Spring会创建一个原始对象(目标对象)的代理对象。
简单来说,就是copy原来的对象形成一个新的对象,后续的操作都在新对象操作,这个新对象也叫代理对象
查看代理对象:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao studentDao = stx.getBean(StudentDao.class); System.out.println(studentDao); System.out.println(studentDao.getClass()); }
从打印结果来看,新对象确实是一个代理对象。
4.获取bean执行方法
获取bean执行方法:
在第三步中,我们可以知道,匹配成功后会得到一个代理对象,我们之后的操作都是基于这个代理对象运行原始方法和增强的内容,这也是AOP的核心本质,使用代理对象进行操作,不影响原来的方法。
做到了不侵入式/不入侵式
。
AOP核心概念
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
代理(Proxy):目标对象无法直接完成工作。需要对其进行功能回填,通过原始对象的代理对象实现。
AOP切入点表达式
我们上面有说道切入点的简单使用方法,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component @Aspect public class DaoAdvice { @Autowired private StudentDao studentDao; @Pointcut("execution(void site.hikki.dao.StudentDao.updateStudent())") private void pt () {} @Before("pt()") public void before () { studentDao.findByNum(5 ); } }
切入点可以只描述一个具体的方法,也可以匹配多个方法,以上的例子只是描述一个具体的方法,如果我们现在有个需求,需要在多个方法内增加某个功能,这时候不可能每一个方法都添加一个切入点,这样效率太低了。
这时候就需要切入点表达式了,切入点表达式,可以通过通配符
匹配多个描述方法。
切入点:要进行增强的方法。
切入点表达式:要进行增强的方法的描述方式。
切入点的接口和实现类
StudentDao接口:
1 2 3 public interface StudentDao { void findAll () ; }
StudentDao接口实现类:
1 2 3 4 5 6 7 8 9 10 11 12 package site.hikki.dao.impl;import org.springframework.stereotype.Repository;import site.hikki.dao.StudentDao;@Repository public class StudentDaoImpl implements StudentDao { @Override public void findAll () { System.out.println("findALl.." ); } }
描述方式一:
执行site.hikki.dao
包下StudentDao
接口中的无参数findAll
方法
1 2 @Pointcut("execution(void site.hikki.dao.StudentDao.findAll())") private void pt () {}
描述方式二:
执行site.hikki.dao.impl
包下StudentDaoImpl
实现类中的无参数findAll
方法
1 2 @Pointcut("execution(void site.hikki.dao.impl.StudentDaoImpl.findAll())") private void pt () {}
我们在描述方法时,按照接口和实现类的方式都是可以的。
但有一点不同,在描述实现类时,只会在运行到该类的切入点才会被执行。
如果描述的是接口,那在运行到该接口的切入点都会被执行。如果有这个接口有多个实现类,那这多个实现类在运行到某切入点都是会被执行的。
切入点表达式格式
切入点表达式标准格式:
1 2 动作关键字 (访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名) execution (public void site.hikki.dao.StudentDao.findByNum(int))
参数
备注
动作关键词
描述切入点的行为动作,例如execution表示执行到指定切入点
访问修饰符
访问修饰符,如public、private等,可省略(一般默认是public)
返回值
包名
类/接口名
方法名
参数
异常名
方法定义中抛出执行异常,可省略
切入点通配符
我们可以使用通配符描述切入点,快速描述。
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
1 2 @Pointcut("execution(public void site.*.*.*.findByNum(int))") private void pt () {}
**匹配内容:**匹配site
包下的任意包中含有一个int类型参数的findByNum()
所有方法。
..
:多个连接的任意符号,可以独立出现,常用于简化包名与参数的书写。
1 2 @Pointcut("execution(public void site..findByNum(int))") private void pt () {}
**匹配内容:**匹配site
包下的任意包中含有一个int类型参数的findByNum()
所有方法。
+
:专用于匹配子类类型
1 2 @Pointcut("execution(public void site.hikki.dao.StudentDao+.*(..))") private void pt () {}
**匹配内容:**匹配site.hikki.dao
包下的StudentDao
接口下的所有方法
书写技巧
所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通常描述接口 ,而不描述实现类
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述 )
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*
通配快速描述
包名 书写尽量不使用..
匹配 ,效率过低,常用*
做单个包描述匹配,或精准匹配
接口名 /类名书写名称与模块相关的采用*
匹配 ,例如UserService
书写成*Service
,绑定业务层接口名
方法名 书写以动词 进行精准匹配 ,名词采用*
匹配,例如getById
书写成getBy*
,selectAll
书写成selectAll
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常 作为匹配 规则
AOP通知类型
通知类型
Spring AOP 中有 5 中通知类型,分别如下:
注解
通知
@Before
通知方法会在目标方法调用之前执行
@After
通知方法会在目标方法返回或异常后调用
@Around
通知方法前会将目标方法封装起来
@AfterReturning
通知方法会在目标方法返回后调用
@AfterThrowing
通知方法会在目标方法抛出异常后调用
初始化项目
项目结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 E:. └─site └─hikki │ App.java │ ├─aop │ StudentAdvice.java │ ├─config │ SpringConfig.java │ └─dao │ StudentDao.java │ └─impl StudentDaoImpl.java
Spring配置类 :
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan("site.hikki") @EnableAspectJAutoProxy public class SpringConfig {}
StudentDao接口 :
1 2 3 4 5 6 package site.hikki.dao;public interface StudentDao { void findAll () ; int update () ; }
StudentDaoImpl实现类 :
记得设置为bean对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package site.hikki.dao.impl;import org.springframework.stereotype.Repository;import site.hikki.dao.StudentDao;@Repository public class StudentDaoImpl implements StudentDao { @Override public void findAll () { System.out.println("findAll..." ); } @Override public int update () { System.out.println("update..." ); return 200 ; } }
App程序入口:
1 2 3 4 5 6 7 8 public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao studentDao = ctx.getBean(StudentDao.class); studentDao.findAll(); }
@Before & @After
属性
名称:@Before
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行。
相关属性:value(默认):切入点方法名,格式为类名.方法名()
案例:
1 2 3 4 @Before("pt()") public void Before () { System.out.println("Before..." ); }
名称:@After
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行。
相关属性:value(默认):切入点方法名,格式为类名.方法名()
案例:
1 2 3 4 @After("pt()") public void After () { System.out.println("After..." ); }
实操
在StudentAdvice切面类定义通知和切入点。
在site.hikki.dao.StudentDao.findAll()
方法定义切入点,切入点方法命名为pt
。
定义两个通知,分别在原始切入点方法前运行、在切入点方法后运行。预期结果应该是如下的:
1 2 3 Before... findAll... 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 package site.hikki.aop;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect @Component public class StudentAdvice { @Pointcut("execution(void site.hikki.dao.StudentDao.findAll())") void pt () {} @Before("pt()") public void Before () { System.out.println("Before..." ); } @After("pt()") public void After () { System.out.println("After..." ); } }
重新运行项目:
输出结果如下:
1 2 3 Before... findAll... After...
看到结果和预期一样,先是运行@Before(“pt()”)通知方法,再运行@After(“pt()”)通知方法。
环绕通知(重点)
属性
名称:@Around(重点、常用)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行。
案例:
1 2 3 4 5 6 7 @Around("pt()") public Object Around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("Around before ..." ); Object proceed = pjp.proceed(); System.out.println("Around after ..." ); return proceed; }
实操
在定义通知时,有时候,可能会遇到原始方法
有返回值
和没有返回值
,接下来我们看看这个案例有返回值和没有返回值有什么区别。
在StudentAdvice切面类定义通知和切入点。
在site.hikki.dao.StudentDao.findAll()
方法定义切入点,切入点方法命名为pt
,该原始方法没有返回值。
定义一个通知,方法内容在切入点的前后运行。
编写通知:
1 2 3 4 5 6 7 8 9 @Pointcut("execution(void site.hikki.dao.StudentDao.update())") void pt () {} @Around("pt()") public Object Around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("Around before ..." ); Object proceed = pjp.proceed(); System.out.println("Around after ..." ); return proceed; }
注意点:
设置通知类型
。通知方法的返回类型需要用Object类型,因为你不知道原始方法是否有返回值,如果有,可以使用Object代替,如果没有返回值,也无所谓。
调用原始方法
。引入ProceedingJoinPoint pjp
参数,pjp.proceed();代表的就是对原始方法的调用。
抛出异常
。在调用原始方法时,需要抛出异常,因为你不知道原始方法会不会出错。
返回值
。调用pjp.proceed();原始方法后需要接收返回值,可能有返回值,可能也没有返回值,如果有返回值,则在通知最后返回原始值。
我们根据上面的案例追加一个情况,将find()方法改为update()方法,并且
在site.hikki.dao.StudentDao.update()
方法定义切入点,切入点方法命名为pt
,该原始方法有返回值。
定义一个通知,方法内容在切入点的前后运行。
1 2 3 4 5 6 7 8 9 10 @Pointcut("execution(int site.hikki.dao.StudentDao.update())") void pt () {}@Around("pt()") public Object Around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("Around before ..." ); Object proceed = pjp.proceed(); System.out.println("Around after ..." ); return proceed; }
在这个切入点和通知中,有一点点不一样。在切入点中的返回类型由之前的void改成了int,表示在执行原始方法(update方法)后会返回一个值。
而在通知方法中,我们知道pjp.proceed();
是调用原始方法的,那调用原始方法就会返回一个值啊,这个值原本是int类型的,现在我们使用Object来接收也是可以的,如果我们修改通知方法的返回结果会怎么样?(我们在update方法默认返回值是200)
很明显,修改通知方法的结果,在运行到update()方法时,返回的值不再是200了,而是我们修改后的值了。
@Around注意事项:
环绕通知必须依赖形参ProceedingJoinpPoint才能实现对原始方法的调用,进而原始方法调用前后同时添加通知
通知中如果过未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
对原始方法的调用可以不接受返回值,通知方法设置成void即可,如果接受返回值,必须设定为Object类型
原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置为Object
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
正确的案例代码:
1 2 3 4 5 6 7 @Around("pt()") public Object Around (ProceedingJoinPoint pjp) throws Throwable { System.out.println("Around before ..." ); Object proceed = pjp.proceed(); System.out.println("Around after ..." ); return proceed; }
抛出异常后通知
1 2 3 4 @AfterThrowing("pt()") public void afterThrowing () { System.out.println("afterThrowing..." ); }
相关属性:value(默认):切入点方法名,格式为类名.方法名()
返回后通知
1 2 3 4 @AfterReturning("pt()") public void afterReturning () { System.out.println("afterReturning..." ); }
相关属性:vallue(默认):切入点方法名,格式为类名.方法名()
AOP案例一:测量业务层接口万次执行效率
需求分析
需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:
①:业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
②:通知类型选择前后均可以增强的类型——环绕通知
代码实现
环境准备
Spring整合mybatis对spring_db数据库中的Student进行CRUD操作
Spring整合Junit测试CRUD是否OK。
在pom.xml中添加aspectjweaver切入点表达式依赖
… …
编写通知类
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 site.hikki.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect @Component public class StudentAdvice { @Pointcut("execution(* site.hikki.service.StudentService.find())") private void pt () {} @Around("StudentAdvice.pt()") public Object runSpeed (ProceedingJoinPoint pjp) throws Throwable { Signature signature = pjp.getSignature(); String MethodName = signature.getName(); String typeName = signature.getDeclaringTypeName(); long start = System.currentTimeMillis(); for (int i = 0 ; i < 20000 ; i++) { pjp.proceed(); } long end = System.currentTimeMillis(); System.out.println(typeName+"." +MethodName+"运行两万次需要消耗的时间:" +(end-start)+"ns" ); return pjp.proceed(); } }
在SpringConfig配置类上开启AOP注解功能
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.config;import org.springframework.context.annotation.*;@Configuration @ComponentScan("site.hikki") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class, MybatisConfig.class}) @EnableAspectJAutoProxy 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 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 51 52 53 package site.hikki.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 site.hikki.config.SpringConfig;import site.hikki.entity.Student;import java.util.List;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class StudentServiceTest { @Autowired private StudentService studentService; @Test public void find () { List<Student> studentList = studentService.find(); for (Student s :studentList) { System.out.println(s); } } @Test public void findByNum () { Student student = studentService.findByNum("3" ); System.out.println(student); } @Test public void addStudent () { Student student = new Student (); student.setAge("25" ); student.setMajor("网络工程" ); student.setName("小码同学" ); int i = studentService.addStudent(student); System.out.println(i); } @Test public void delStudentBynum () { int i = studentService.delStudentBynum("6" ); System.out.println(i); } @Test public void updateStudentBynum () { studentService.updateStudentBynum("吴小黑" ,"5" ); } }
AOP通知获取数据
初始化项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 E:. └─site └─hikki │ App.java │ ├─aop │ MyAdvice.java │ ├─config │ SpringConfig.java │ └─dao │ StudentDao.java │ └─impl StudentDaoImpl.java
导入依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.4</version > </dependency >
创建StudentDao接口
1 2 3 4 5 6 package site.hikki.dao;public interface StudentDao { void find () ; String update (int num,String name) ; }
实现StudentDao接口
同时,在该类创建bean
对象(在类上方注解@Repository
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package site.hikki.dao.impl;import org.springframework.stereotype.Repository;import site.hikki.dao.StudentDao;@Repository public class StudentDaoImpl implements StudentDao { @Override public void find () { System.out.println("find..." ); } @Override public String update (int num,String name) { System.out.println("update...num:" +num+",name:" +name); return "https://blog.hikki" ; } }
创建AOP切面类
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 package site.hikki.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component @Aspect public class MyAdvice { @Pointcut("execution(void site.hikki.dao.StudentDao.find())") private void pt () {} @Before("pt()") public void Before () { System.out.println("before.." ); } @After("pt()") public void after () { System.out.println("after..." ); } @Around("pt()") public Object around (ProceedingJoinPoint pjp) { System.out.println("around...start" ); Object proceed = null ; try { proceed = pjp.proceed(); } catch (Throwable e) { System.out.println("around:" +e); } System.out.println("around...end" ); return proceed; } @AfterReturning("pt()") public void afterReturning () { System.out.println("afterReturning..." ); } @AfterThrowing("pt()") public void afterThrowing () { System.out.println("afterThrowing..." ); } }
开启AOP注解功能
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.EnableAspectJAutoProxy;import org.springframework.test.context.ContextConfiguration;@ContextConfiguration @ComponentScan("site.hikki") @EnableAspectJAutoProxy public class SpringConfig {}
程序入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package site.hikki;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import site.hikki.config.SpringConfig;import site.hikki.dao.StudentDao;public class App { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao studentDao = ctx.getBean(StudentDao.class); studentDao.find(); } }
我们刚刚运行的项目都是没有参数的,也没有返回值的,我们在实际的项目开发中,在原始方法中,一般都有参数的,有返回值的,我们在使用AOP通知方法时,应该怎么获取这些参数呢?可以更改通知方法的返回值吗?
在上面的案例中,我们在程序入口调用find()方法,使用了AOP增强find的方法,我们下面调用update()方法试试,该方法有返回值,也有参数。
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (SpringConfig.class); StudentDao studentDao = ctx.getBean(StudentDao.class); String update = studentDao.update(2 , "小码同学" ); System.out.println(update); }
我们正常的调用方法,得到的返回值是这样的,我们还没有使用AOP给update()设置增强方法,下面我们试试。
AOP获取参数数据
设置切入点
在MyAdvice
切面类添加一个切入点,方法名命名为pt2
,方法返回值类型使用*
通配符,参数使用..
,匹配任意多个参数。
1 2 @Pointcut("execution(* site.hikki.dao.StudentDao.update(..))") private void pt2 () {}
设置前置通知(@Before)
在通知方法的参数添加JoinPoint jp
可以拿到原始方法的参数,然后通过getArgs()
拿到,我们一般将拿到的参数一般是用Object类型定义。
JoinPoint
对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数。
1 2 3 4 5 6 @Before("pt2()") public void Before2 (JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before2.." ); }
运行结果:
1 2 3 4 [2, 小码同学] before2.. update...num:2,name:小码同学 https://blog.hikki
结果可以看到,我们拿到参数是一个数组类型,我们看看JoinPoint对象和ProceedingJoinPoint对象有什么区别,从名字上看着很像。
设置环绕通知(@Around)
Around
通知方法也是可以获取到原始方法的参数数据的。Around
需要使用ProceedingJoinPoint
对象,获取数据的方法一样,都是使用getArgs()
Ctrl +H
,可以看到,ProceedingJoinPoint
是JoinPoint
的子类。
1 2 3 4 5 6 7 @Around("pt2()") public Object around2 (ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); Object ret = pjp.proceed(); return ret; }
AOP通知获取返回值数据
设置返回后通知(@AfterReturning)
我们在编写通知方法时,一般在注解中,我们只是写上切入点的方法名,这个方法名是默认value值的,如果需要添加多个值,可以点击注解,查看注解的参数类型有哪些,比如@AfterReturning
注解有value、pointcut、returning、argNames
四个参数类型,根据需求可以设置不同的参数。
其中returning
是获取返回值,value
我们也用过,也就是切入点。
1 2 3 4 @AfterReturning(value = "pt2()",returning = "ret") public void afterReturning2 (String ret) { System.out.println("afterReturning2..." +ret); }
注释掉其他通知方法,只留下afterReturning2
,查看运行效果:
1 2 update...num:2,name:小码同学 afterReturning2...https://blog.hikki
运行结果可以看到,我们使用returning = "ret"
可以获取到原始方法的返回值。
设置环绕通知(@Around)
上面我们也举例过Around获取原始方法的参数数据,不仅如此,Around还可以获取到原始方法的返回值。
1 2 3 4 5 6 @Around("pt2()") public Object around3 (ProceedingJoinPoint pjp) throws Throwable { Object ret = pjp.proceed(); System.out.println("around2..." +ret); return ret; }
AOP通知获取异常数据(了解)
设置返回后通知(@AfterReturning)
1 2 3 4 @AfterThrowing(value = "pt2()",throwing = "e") public void afterThrowing2 (Throwable e) { System.out.println("afterThrowing..." +e); }
设置AfterReturning
通知方法了,我们要怎么捕捉错误呢?错误哪里来?
所以我们先去new
一个错误,写BUG
我在行,这就来写一个。
在StudentDaoImpl
实现类中,在update方法的返回前,添加if (true) throw new NullPointerException();
,表示抛出一个空指针错误。这回错误有了,运行测试一下。
1 2 3 4 5 6 @Override public String update (int num,String name) { System.out.println("update...num:" +num+",name:" +name); if (true ) throw new NullPointerException (); return "https://blog.hikki" ; }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 update...num:2,name:小码同学 afterThrowing...java.lang.NullPointerException Exception in thread "main" java.lang.NullPointerException at site.hikki.dao.impl.StudentDaoImpl.update(StudentDaoImpl.java:15) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) at com.sun.proxy.$Proxy25 .update(Unknown Source) at site.hikki.App.main(App.java:11) 进程已结束,退出代码1
抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象。
设置环绕通知(@Around)
使用Around可以同样捕捉异常数据,我们之前使用throws Throwable
抛出异常的,现在使用try{}catch{}
捕捉异常。
1 2 3 4 5 6 7 8 9 10 @Around("pt2()") public Object around4 (ProceedingJoinPoint pjp) { Object ret = null ; try { ret = pjp.proceed(); } catch (Throwable e) { e.printStackTrace(); } return ret; }
抛出异常后通知可以获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象。
总结
获取切入点方法的参数
获取切入点方法返回值
获取切入点方法运行异常信息
AOP案例二:百度云盘密码数据兼容处理
需求分析
需求:对百度网盘分享链接输入提取码时尾部多输入的空格做兼容处理。
1 2 3 链接: https://pan.baidu.com/s/13iYjxy02TnBhyhA_0JObKA?pwd=root 提取码: root 复制这段内容后打开百度网盘手机App,操作更方便哦
分析:
在业务方法执行前对所有的输入参数进行格式处理,使用trim方法
使用处理后的参数调用原始方法–环绕通知中存在对原始方法的调用。
代码实现
项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 E:. └─site └─hikki │ App.java │ ├─aop │ DataAdvice.java │ ├─config │ SpringConfig.java │ ├─dao │ │ CodeDao.java │ │ │ └─impl │ CodeDaoImpl.java │ └─service │ DataStr.java │ └─impl DataStrImpl.java
编写数据层
创建一个CodeDao接口:
1 2 3 4 5 package site.hikki.dao;public interface CodeDao { boolean readPassword (String url,String password) ; }
CodeDaoImpl实现类:
我们不使用连接数据库的方式来验证提取码,直接将提取码写死,模拟一下业务即可。
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.dao.impl; import org.springframework.stereotype.Repository; import site.hikki.dao.CodeDao; @Repository public class CodeDaoImpl implements CodeDao { @Override public boolean readPassword(String url, String password) { return password.equals("root"); } }
编写业务层
接口:
1 2 3 4 5 package site.hikki.service;public interface DataStr { boolean URL (String url,String password) ; }
实现类:
记得给该类配置bean对象,并且添加CodeDao自动装配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package site.hikki.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import site.hikki.dao.CodeDao;import site.hikki.service.DataStr;@Service public class DataStrImpl implements DataStr { @Autowired private CodeDao codeDao; @Override public boolean URL (String url,String password) { return codeDao.readPassword(url,password); } }
编写切面类
@Aspect:定义该类为切面类
@Component:定义该类为bean,收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 package site.hikki.aop;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;@Aspect @Component public class DataAdvice { @Pointcut("execution(* site.hikki.service.DataStr.URL(..))") private void pt () {} @Around("pt()") public Object trimString (ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); for (int i = 0 ; i < args.length; i++) { if (args[i].getClass().equals(String.class)){ args[i]=args[i].toString().trim(); } } return pjp.proceed(args); } }
开启AOP注解功能
1 2 3 4 5 6 7 8 9 10 11 package site.hikki.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan("site.hikki") @EnableAspectJAutoProxy public class SpringConfig {}
创建入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package site.hikki;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import site.hikki.config.SpringConfig;import site.hikki.service.DataStr;public class App { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (SpringConfig.class); DataStr dataStr = ctx.getBean(DataStr.class); boolean b = dataStr.URL("https://pan.baidu.com/s/13iYjxy02TnBhyhA_0JObKA?pwd=root" ," root " ); System.out.println(b); } }
可以看出,我们在提取码增加空格,系统也是可以识别到提取码的内容。
做到在不改变原始的方法下,给该方法增加功能。
AOP总结
概念
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式。
作用
在不惊动原始设计的基础上为方法进行进行功能增强
核心概念
代理(Proxy):SpringAOP的核心本质是采用代理模式
实现的。
连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行。
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述。
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现在一个方法。
切面(Aspect):描述通知与切入点的对应关系。
目标对象(Target):被代理的原始对象成为目标对象。(也就是原始方法+增加的方法=全新的方法,叫做目标对象)
切入点表达式
切入点表达式标准格式
动作关键词 (访问修饰符 返回值 报名.类/接口.方法名 (参数) 异常名)
如:execution(* site.hikki.service.DataStr.URL(..))
通配符
通配符
备注
*
匹配任意一个名称或符号(常用)
…
匹配多个连续的任意符号(常用)
+
匹配子类类型
书写技巧
按标准规范
开发
查询操作的返回键建议使用*
匹配
减少使用..
的形式描述包
对接口进行描述
,使用*
表示模块名,比如UserService
的匹配描述为*Service
方法名写保留动词,比如get
,使用*
表示名刺。例如getById
匹配描述为getBy*
参数根据实际情况灵活调整
通知类型
注解
名称
通知
@Before
前置通知
通知方法会在目标方法调用之前执行
@After
后置通知
通知方法会在目标方法返回或异常后调用
@Around (重点)
环绕通知
通知方法前会将目标方法封装起来
@AfterReturning
返回后通知
通知方法会在目标方法返回后调用
@AfterThrowing
抛出异常通知
通知方法会在目标方法抛出异常后调用
通知获取数据
获取切入点方法的参数
JoinPoint
:适用于前置、后置、返回后、抛出异常后通知,设置为方法的第一个形参。
ProceedJoinPoint
:使用于环绕通知
获取切入点方法返回值
获取切入点方法运行异常信息
Spring事务管理
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
接下来我们使用一个模拟银行账户间转账业务
案例来讲解一下Spring的事务管理。
需求分析
需求:实现任意两个账户间转账操作
需求微缩:A账户减钱,B账户加钱
分析:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
结果分析:
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
代码实现
导入依赖
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 <dependency > <groupId > org.mariadb.jdbc</groupId > <artifactId > mariadb-java-client</artifactId > <version > 3.1.0</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.2.15</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.7</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 1.3.0</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.2.10.RELEASE</version > </dependency >
新建jdbc.properties
数据库连接资源
数据库SQL文件在该章节最后,数据库连接用户名和密码,根据自己的情况调整
1 2 3 4 jdbc.driver =org.mariadb.jdbc.Driver jdbc.url =jdbc:mariadb://localhost:3307/spring_db?useSSL=false jdbc.username =root jdbc.password =root
创建实体类Account
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 package site.hikki.entity;public class Account { private Integer id; private String name; private String money; @Override public String toString () { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money='" + money + '\'' + '}' ; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } public String getMoney () { return money; } public void setMoney (String money) { this .money = money; } }
创建Dao层
创建Dao层,对账户余额进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 package site.hikki.dao;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Update;public interface AccountDao { @Update("update tb_account set money = money - #{money} where name =#{outName}") int outMoney (@Param("outName") String outName, @Param("money") Double money) ; @Update("update tb_account set money = money + #{money} where name =#{inName}") int inMoney (@Param("inName") String inName, @Param("money") Double money) ; }
创建service层
AccountService接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package site.hikki.service;import org.springframework.transaction.annotation.Transactional;public interface AccountService { @Transactional public void transfer (String out,String in ,Double money) ; }
AccountServiceImpl实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package site.hikki.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import site.hikki.dao.AccountDao;import site.hikki.service.AccountService;@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Override public void transfer (String out,String in,Double money) { accountDao.outMoney(out,money); int i = 1 /0 ; accountDao.inMoney(in,money); } }
创建MybatisConfig第三方Bean
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 site.hikki.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 sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean (); ssfb.setTypeAliasesPackage("site.hikki.entity" ); ssfb.setDataSource(dataSource); return ssfb; } @Bean public MapperScannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer msc = new MapperScannerConfigurer (); msc.setBasePackage("site.hikki.dao" ); return msc; } }
创建JdbcConfig第三方Bean
在JdbcConfig配置中,我们比往常要多增加一个方法transactionManager
,该方法表示设置Spring事务管理器
,其中,我们需要给Spring设置事务技术,由于Mybatis框架使用的是JDBC事务,我们在该方法中,添加一个DataSource
即可,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 package site.hikki.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @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.setDriverClassName(driver); 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; } }
开启注解事务驱动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package site.hikki.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("site.hikki.service") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class, MybatisConfig.class}) @EnableTransactionManagement 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 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 site.hikki.config.SpringConfig;import site.hikki.service.AccountService;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("小码同学" ,"吴小白" ,100D ); } }
测试结果
1 2 3 4 5 6 7 java.lang.ArithmeticException: / by zero at site.hikki.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:15) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) ......省略后面结果
从结果可以看到,报错了,报错是肯定的,因为我们在AccountServiceImpl
实现类主动制造了错误,添加了int i = 1/0;
,按照正常逻辑,在该语句前面的方法已经是执行了的,该语句后面的方法则没有被执行。
用户的初始值都是1000
,在执行后,该余额仍然是没有改变,符合我们的要求,在没有转账中失败了,两个用户的余额都应该是没有改变的。
事务管理的原理是什么?
事务角色
事务管理分两个角色:事务管理员和事务协调员。
原本接口中的两个方法都是独立的事务,每一次的调用都是一个独立事务。
但Spring事务中,使用@Transactional
注解表示,在该方法中,调用到其他的全部方法,都统一加入到一个新的事务中,即该方法中,只有一个事务,如果该事务执行的过程中发生异常,则该方法的全部执行效果都不起作用。
事务管理配置
在@Transactional
注解中,可以添加其他配置项(Ctrl + 左键),可以配置其他功能。
属性
作用
示例
readOnly
设置是否为只读事务
readOnly=true 只读事务
timeout
设置事务超时时间
timeout = -1(永不超时)
rollbackFor
设置事务回滚异常(class)
rollbackFor = {NullPointException.class}
rollbackForClassName
设置事务回滚异常(String)
同上格式为字符串
noRollbackFor
设置事务不回滚异常(class)
noRollbackFor = {NullPointException.class}
noRollbackForClassName
设置事务不回滚异常(String)
同上格式为字符串
propagation
设置事务传播行为
……
说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。
转账业务追加日志
需求分析
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现效果预期:
存在的问题:
实现效果预期改进:
事务传播行为
我们既然可以将多个事务统一加入到某个事务中,那么也可以决定某个事务不要加入到统一的事务中。
代码实现
新建Dao层
将日志写入到数据库。
1 2 3 4 5 6 7 8 9 10 package site.hikki.dao;import org.apache.ibatis.annotations.Insert;import org.springframework.stereotype.Repository;@Repository public interface LogDao { @Insert("insert into tb_log (info,createDate) values(#{info},now())") void log (String info) ; }
新建LogService接口
我们一般将@Transactional
写在接口,而不是写在实现类中。
1 2 3 4 5 6 7 package site.hikki.service;public interface LogService { @Transactional(propagation = Propagation.REQUIRES_NEW) void log (String out,String in,Double money) ; }
新建LogServiceImpl实现类
在创建log
方法时,需要添加 @Transactional
注解,并且属性和值分别添加propagation
、Propagation.REQUIRES_NEW
,表示该事务独立新建一个事务,不统一添加到被调用的事务中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package site.hikki.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import site.hikki.dao.LogDao;import site.hikki.service.LogService;@Service public class LogServiceImpl implements LogService { @Autowired private LogDao logDao; @Override public void log (String out, String in, Double money) { logDao.log("转账操作由" +out+"到" +in+",金额:" +money); } }
修改AccountServiceImpl实现类
使用try{}finally{}
括起来,保证log方法
无论如何都会被执行。
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 site.hikki.service.impl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import site.hikki.dao.AccountDao;import site.hikki.service.AccountService;import site.hikki.service.LogService;@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Autowired private LogService logService; @Override public void transfer (String out,String in,Double money) { try { accountDao.outMoney(out,money); int i = 1 /0 ; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); } } }
测试方法后,即使出现错误,也可以将转账记录写入到数据中,这对于日志的存储非常有帮助。