SpringBoot 事务&AOP入门
1.SpringBoot事务管理
1.1 事务
事务是一个不可分割的整体,要么同时成功,要么同时失败
1.2事务的操作:
- 开启事务:start transaction / begin
- 提交事务:commit
- 回滚事务:rollback
1.3 SpringBoot中的事务
在SprintBoot项目中开启事务只需要在类、接口、方法上添加@Transactional
注解就可以开启事务
@Transactional注解在方法上的作用
在方法执行前开启事务,方法执行完成后提交事务,如果中途出现运行时异常则回滚事务
1.3.1 异常回滚的属性:rollbackFor
当我们在方法上加了@Transactional
注解进行事务管理,出现运行时异常时会回滚事务,从而保证事务操作前后数据是一致的,但是当出现编译时异常时,事务会正常提交,假如我们想要所有的异常都会回滚,就需要配置 @Transactional
注解中的 rollbackFor
属性
// 默认只会回滚运行时异常 而rollbackFor可以指定出现那种异常时回滚事务
@Transactional(rollbackFor = Exception.class)
1.3.2 异常传播行为:propagation
首先我们先说一下什么是事务的传播行为,事务的传播行为就是,当一个事务方法被另一个事务方法调用时,被调用的方法是如何进行事务控制的。
可能听起来比较抽象,那我们举个例子,例如,A方法有事务,B方法也有一个事务,A方法调用了B方法,那么此时B方法是使用A方法的事务,还是自己创建一个呢,这个就是事务的传播行为。
传播行为的分类
属性值 | 描述 |
---|---|
REQUIRED | 默认值 有则加入,没有则创建 |
REQUIRES_NEW | 无论有没有,都会自己创建新事务 |
SUPPORTS | 有则加入,没有就在没有事务的状态中运行 |
NOT_SUPPORTED | 如果有事务,则将事务挂起 |
MANDATORY | 必须有事务,没有就抛异常 |
NEVER | 必须没有事务,有则抛异常 |
:------------------------站在事务B方法的角度 (前两个开发中常用)------------------------
:
propagation值的使用
// 有则加入,没有则创建
@Transactional(propagation = Propagation.NESTED)
// 无论有没有,都会创建一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
2.SpringBoot AOP入门
2.1 AOP概述
AOP(Aspect Oriented Programming):面向切面编程,大白话就是面向特定的方法编程。
那么面向特定方法编程是什么,在说白一点,如果我们需要测试一些方法进行性能测试,需要拿到每个
方法执行的耗时,应该怎么实现呢,在每个方法前后加上一个获得当前时间然后取差就可以完成,但是
一个项目中有成千上万个方法,先不说工作量,光是这些重复的动作就已经很恶心了。
而AOP就是在不改变原来方法的前提下,对方法进行增强。
2.2 AOP核心概述
下面说一下AOP的几个专业的核心概念
- 连接点:
JoinPoint
可以被AOP控制的方法 - 切入点:
PoinCut
已经被控制AOP的方法 - 通知:
Advice
重复的逻辑,共性内容 - 切面:
Aspect
切入点+通知 - 目标对象:
Target
被增强方法所在的对象
2.3 通知类型
@Before
:前置通知,此注解标注的通知方法在目标方法前被执行
@After
:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@Around
:坏绕通知,此注解标注的通知方法在目标方法前、后都会被执行
@AfterReturning
:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing
:异常后通知,此注解标注的通知方法只有发生异常后才执行
//前置通知
@Before("切入点表达式")
//环绕通知
@Around("切入点表达式")
//后置通知
@After("切入点表达式")
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("切入点表达式")
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("切入点表达式")
代码演示
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("前置通知执行了...");
}
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("坏绕后通知执行了...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("坏绕前通知执行了 ...");
return result;
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("后置通知执行了...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("返回后通知执行了...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("异常通知执行了...");
}
}
注意事项
@Around
环绕通知需要自己调用ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行
@Around
坏绕通知方法的返回值
,必须指定为Object
来接收原始方法
的返回值,否则原始方法执行完毕,是获取不到返回值的
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
log.info("环绕前通知...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("环绕后置通知...");
return result;
}
在上述代码中切入点表达式一摸一样,并且需要修改时需要一个一个修改,较为繁琐,我们可以将这些切入点表达式进行抽取
而Spring为我们提供了一个@PointCut
注解,这个注解的作用就是将公共切入点表达式抽取出来,使用时引用就可以,但是如果切入点方法使用了private
修饰,就只能在当前切面类中引用,如果其他切面类也想引用,就需要把private
修改为public
,而引用时需要使用全类名
引用
格式:全类名.方法名()
// 本类
@Slf4j
@Component
@Aspect
public class MyAspect1 {
// 切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
}
// 环绕通知(引用切入点)
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
log.info("环绕前通知...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
// 原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("环绕后置通知...");
return result;
}
}
// 其他类
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.itheima.aspect.MyAspect1.pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
log.info("环绕前通知...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
// 原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("环绕后置通知...");
return result;
}
}
2.4 通知顺序
当我们定义了多个切面类,而多个切入点都匹配到了同一个目标方法上,此时当目标方法执行时,那么那个切面类当中的通知方法会先执行呢(多个通知,对应的切入点一样
)
顺序:默认是按照切面类 类名的自然排序
决定先后顺序
而目标方法前的通知方法:字母靠前先执行
而目标方法后的通知方法:字母靠后先执行
控制通知的执行顺序有两种方式:
- 修改切面类的类名(繁琐,不推荐)
- 使用Spring提供的@Order注解
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
2.5 切入点表达式
切入点表达式: 描述切入点方法的一种表达式
形式:
execution
(……):根据方法的签名来匹配@annotation
(……) :根据注解匹配
2.5.1 execution
(1) 语法:
// ?号部分可以省略
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
// 示例
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
(2) 通配符:
*
:可以通配单个层级或者单个参数
. .
:可以通配多个连续的层级或者多个参数
(3) 切入点表达式的语法规则:
- 方法的访问修饰符可以省略
- 返回值可以使用
*
号代替(任意返回值类型) - 包名可以使用
*
号代替,代表任意包(一层包使用一个*
) - 使用
..
配置包名,标识此包以及此包下的所有子包 - 类名可以使用
*
号代替,标识任意类 - 方法名可以使用
*
号代替,表示任意方法 - 可以使用
*
配置参数,一个任意类型的参数 - 可以使用
..
配置参数,任意个任意类型的参数
示例:
省略方法的修饰符号
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*
代替返回值类型
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*
代替包名(一层包使用一个*
)
execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
使用..
省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))
使用*
代替类名
execution(* com..*.delete(java.lang.Integer))
使用*
代替方法名
execution(* com..*.*(java.lang.Integer))
使用 *
代替参数
execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
使用..
省略参数
execution(* com..*.*(..))
注意事项:
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
切入点表达式的书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
2.5.2 @annotation
在匹配多个无规则的目标方法是,execution
就闲的不是很方便了,而我们借助@annotation
就可以简化切入点表达式的书写
实现步骤:
- 编写自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
- 在业务类要做为连接点的方法上添加自定义注解
// 业务类中的目标方法
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void test() {
}
// 切面类中的通知方法
@Before("@annotation(com.itheima.anno.MyLog)")
public void before(){
log.info("annotation注解测试 ...");
}
总结
execution
切入点表达式
根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
annotation
切入点表达式
基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了