Spring AOP

Spring AOP

AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。

OOP(Object Oriented Programming)面向对象编程,一种编程思想
AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式。

AOP 作用:在不惊动原始设计的基础上为其进行功能增强,类似于代理模式

举个栗子:

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        //记录程序当前执行执行(开始时间)
            Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0;i<10000;i++) {
        System.out.println("book dao save ...");
        }
        //记录程序当前执行时间(结束时间)
        Long endTime = System.currentTimeMillis();
        //计算时间差
        Long totalTime = endTime-startTime;
        //输出信息
        System.out.println("执行万次消耗时间:" + totalTime + "ms");
    }
    public void update(){
        System.out.println("book dao update ...");
    }
    public void delete(){
        System.out.println("book dao delete ...");
    }
    public void select(){
        System.out.println("book dao select ...");
    }
}

代码的内容是对于 save 方法中有计算万次执行消耗的时间。

在 App 类中从容器中获取 bookDao 对象后,分别执行其 save , delete , update 和 select 方法后会 有如下的打印结果:

在这里插入图片描述

这个案例中其实就使用了Spring的AOP,在不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加:对于计算万次执行消耗的时间只有 save 方法有,但是 delete、update 方法没有这个逻辑的代码也有该功能,而 select 方法却没有

这个也就是Spring的理念:

  • 无入侵式/无侵入式

对于 AOP 有几个核心概念要介绍下:

  1. 连接点(JoinPoint): 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在 SpringAOP 中,理解为方法的执行
  2. 切入点(Pointcut): 匹配连接点的式子
    • 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可以匹配多个方法
      • 一个具体的方法:如 com.XXX.dao 包下的 BookDao 接口中的无形参无返回值的 save 方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
  3. 通知(Advice): 在切入点处执行的操作,也就是共性功能
    • 在 SpringAOP 中,功能最终以方法的形式呈现
  4. 通知类: 定义通知的类
  5. 切面(Aspect): 描述通知与切入点的对应关系。

该如何实现?

在这里插入图片描述

  • 对于上面的案例中 BookServiceImpl 中有save , update , delete和select方法,这些原始方法叫连接点
  • BookServiceImpl 的四个方法中,update 和 delete 只有打印没有计算万次执行消耗时间, 但是在运行的时候已经有该功能,那也就是说 update 和 delete 方法都已经被增强,需要增强的方法叫切入点
  • 执行 BookServiceImpl 的 update 和 delete 方法的时候都被添加了一个计算万次执行消耗时间 的功能,将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,叫通知
  • 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,对于通知和切入点之间的关系描述叫切面
  • 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类叫通知类

入门案例

使用 SpringAOP 的注解方式完成在方法执行前打印出当前系统时间

  1. 添加依赖

    <dependencies>
        <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>
    </dependencies>
    

    在这里插入图片描述

    • spring-context中已经导入了spring-aop ,所以不需要再单独导入spring-aop
    • 导入 AspectJ 的 jar 包,AspectJ 是 AOP 思想的一个具体实现,Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以直接采用 Spring 整合 ApsectJ 的方式进行 AOP 开发。
  2. 定义接口与实现类

    public interface BookDao {
        public void save();
        
        public void update();
    }
    
    @Repository
    public class BookDaoImpl implements BookDao {
        public void save() {
            System.out.println(System.currentTimeMillis());
            System.out.println("book dao save ...");
        }
        public void update(){
            System.out.println("book dao update ...");
        }
    }
    
  3. 定义通知类和通知

    public class MyAdvice {
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  4. 定义切入点

    public class MyAdvice {
        @Pointcut("execution(void com.XXX.dao.BookDao.update())")
        private void pt(){}
        
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
    • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法无实际逻辑
  5. 制作切面

    public class MyAdvice {
        @Pointcut("execution(void com.XXX.dao.BookDao.update())")
        private void pt(){}
        
        @Before("pt()")
        public void method(){
        System.out.println(System.currentTimeMillis());
        }
    }
    
    • 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

    • 在这里插入图片描述

    • @Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行

  6. 将通知类配给容器并标识其为切面类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.XXX.dao.BookDao.update())")
        private void pt(){}
        
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  7. 开启注解格式 AOP 功能

    @Configuration
    @ComponentScan("com.XXX")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
名称@EnableAspectJAutoProxy
类型配置类注解
位置配置类定义上方
作用开启注解格式AOP功能
名称@Aspect
类型类注解
位置切面类定义上方
作用设置当前类为AOP切面类
名称@Pointcut
类型方法注解
位置切入点方法定义上方
作用设置切入点方法
属性value:切入点表达式
名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

AOP工作流程

由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起

  1. Spring 容器启动

    • 容器启动就需要去加载 bean
    • 被增强的类,如:BookServiceImpl
    • 通知类,如:MyAdvice
    • 此时对象还没有创建成功
  2. 读取所有切面配置中的切入点

    • 只会读取被使用的切入点,比如:
    • 在这里插入图片描述
  3. 初始化 bean

    • 在这里插入图片描述

    • 判定要被实例化的 bean 对象对应的类中方法是否匹配到任意切入点

      • 匹配失败,创建原始对象,如:UserDao
        • 匹配失败说明不需要增强,直接调用原始对象的方法即可
      • 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
        • 匹配成功说明需要对其进行增强
        • 对哪个类做增强,这个类对应的对象就叫做目标对象
        • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
        • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
  4. 获取 bean 执行方法

    • 获取的 bean 是原始对象时,调用方法并执行,完成操作
    • 获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

AOP核心概念

在上面介绍 AOP 的工作流程中,提到了两个核心概念,分别是:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终的工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

简单来说,目标对象就是要被增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运 行,只能说它在运行的过程中对于要增强的内容是缺失的。SpringAOP 是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。

AOP配置管理

AOP切入点表达式

在这里插入图片描述

语法格式

要明确两个概念:

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

对于切入点的描述,有两种方式:

在这里插入图片描述

  • 描述方法一:执行 com.XXX.dao 包下的 BookDAO 接口中的无参 update 方法

    execution(void com.XXX.dao.BookDao.update())
    
  • 描述方式二:执行 com.XXX.dao.impl 包下的 BookDAOImpl 接口中的无参 update 方法

    execution(void com.XXX.dao.impl.BookDaoImpl.update())
    

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。

对于切入点表达式的语法为:

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.[类/接口名].方法名(参数) 异常)

比如:

execution(public User com.XXX.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • com.XXX.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以此处的简化是通过通配符

通配符

使用通配符描述切入点,主要的目的就是简化之前的配置

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution(public * com.XXX.*.UserService.find*(*))
    

    匹配 com.XXX 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution(public User com..UserService.findById(..))
    

    匹配 com 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法

  • +:专用于匹配子类类型

    execution(* *..*Service+.*(..))
    

    使用率较低,描述子类的,做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。

接下来通过一个案例熟悉一下

在这里插入图片描述

execution(void com.XXX.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.XXX.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.XXX.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.XXX.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加
参数
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.XXX.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.XXX.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配
书写技巧
  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用 public 描述(可省略访问控制修饰符描述)
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用*匹配,例如 UserService 书写成 *Service,绑定业务 层接口名
  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如 getById 书写成 getBy*,selectAll书写成 select*
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

AOP通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合 理的位置,AOP 提供了5种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知
  • 返回后通知
  • 抛出异常后通知

在这里插入图片描述

  • 前置通知:追加功能到方法执行前,类似于在代码1或者代码2添加内容
  • 后置通知:追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
  • 返回后通知:追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会添加
  • 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出现异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
  • 环绕通知,环绕通知功能比较强大,他可以追加功能到方法执行的前后,这也是比较常用的方法,它可以实现其他四种通知类型的功能

通知类型的使用

前置通知

在通知类的前置方法上加@Before注解

@Component
@Aspect
public class MyAdvice {
	@Pointcut("execution(void com.XXX.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()")
    public void before() {
        System.out.println("before advice ...");
    }
}
名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法之前运行
后置通知

在通知类的后置方法上加@After注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.XXX.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    
    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
}
名称@After
类型方法注解
位置通知方法定义上方
作用设置当前通知方法方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
环绕通知

在通知类的后置方法上加@Around注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.XXX.dao.BookDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

在这会有个问题,运行后,原始方法的内容没有被执行,因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体实现:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.XXX.dao.BookDao.update())")
    private 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 ...");
    }
}

因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可 以实现不同的通知类型的功能,如:

在这里插入图片描述

  • 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  • 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
  • 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好设定为 Object 类型
  • 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
  • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常

proceed() 需要抛出异常,我们来看下:

在这里插入图片描述

此时原始方法就会被执行了

注意事项:

  • 原始方法有返回值的处理

    • 如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值

    • @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(int com.XXX.dao.BookDao.select())")
          private void pt2(){}
          
          @Around("pt2()")
          public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
              System.out.println("around before advice ...");
              //表示对原始操作的调用
              Object ret = pjp.proceed();
              System.out.println("around after advice ...");
              return ret;
          }
      }
      
    • 但是原始方法是 void 的就是返回 Null

    • 建议都使用 Object 作为返回值类型

    • 返回的是 Object 而不是 int 的主要原因是 Object 类型更通用,在环绕通知中是可以对原始方法返回值进行修改的。

名称@Around
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的关系绑定,当前通知方法在原始切入点方法前后运行

Tip:

  • // 获取执行签名信息
    Signature  signature  =  proceedingJoinPoint.getSignature();
    
    // /通过签名获取执行操作名称(接口名)
    String  className  =  signature.getDeclaringTypeName();
    
    // 通过签名获取执行操作名称(方法名)
    String  methodName  =  signature.getName();
    
  • 可以通过以上 API 来环绕通知中获取执行的方法的接口路径和方法名称

返回后通知

在通知类的返回后方法上加@AfterReturning注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.XXX.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

注意: 返回后通知是需要在原始方法正常执行后才会被执行,如果原始方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。

名称@AfterReturning
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始方法切入点方法正常执行完毕后执行
异常后通知

在通知类的异常后方法上加@@AfterThrowing注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.XXX.dao.BookDao.select())")
    private void pt2(){}
    
    @@AfterThrowing("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

注意: 异常后通知是需要原始方法抛出异常,如果没有抛异常,异常后通知将不会被执行。

名称@AfterThrowing
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

AOP通知获取数据

对于通知也会有参数、返回值、异常

  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常通知是没有返回值,后置通知可有可无
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回值通知是没有的,后置通知可有可无
    • 抛出异常后通知
    • 环绕通知
获取参数
非环绕通知获取方式

在方法上添加 JoinPoint,通过 JoinPoint 来获取参数

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @Before("pt()")
    public void before(JoinPoint jp){
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }
    //...其他的略
}

注意: 因为参数的个数是不固定的,所以使用数组更适配些,而且 JoinPoint 方法获取参数适用于前置、后置、返回后、抛出异常后通知。

环绕通知获取方式

环绕通话使用的是 ProceedingJoinPoint,因为 ProceedingJoinPoint 是 JoinPoint 类的子类,所以对于 ProceedingJoinPoint 类中应该也会有对应的 getArgs() 方法

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp)throws Throwable {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        Object ret = pjp.proceed();
        return ret;
    }
}

注意:

  • pjp.proceed() 方法是有两个构造方法,分别是:

  • 在这里插入图片描述

    • 调用无参数的 proceed,当原始方法有参数,会在调用的过程中自动传入参数

    • 所以调用这两个方法的任意一个都可以完成功能

    • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:

    • @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
          private void pt(){}
          
          @Around("pt()")
          public Object around(ProceedingJoinPoint pjp) throws Throwable{
              Object[] args = pjp.getArgs();
              System.out.println(Arrays.toString(args));
              args[0] = 666;
              Object ret = pjp.proceed(args);
              return ret;
          }
          //其他的略
      }
      
    • 有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

获取返回值

对于返回值,只有返回后 AfterReturing 和环绕 Around 这两个通知类型可以获取

环绕通知获取返回值
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
        return ret;
    }
}

上述代码中,ret就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修 改。

返回后通知获取返回值
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..."+ret);
    }
}

注意:

  • 参数名的问题

    • 在这里插入图片描述
  • afterReturning 方法参数类型的问题

    • 参数类型可以写成 String,但是为了能匹配更多的参数类型,建议写成 Object 类型
  • afterReturning 方法参数的顺序问题

    • 在这里插入图片描述
获取异常

对于获取抛出的异常,只有抛出异常后 AfterThrowing 和环绕 Around 这两个通知类型可以获取

环绕通知获取异常

只需要将异常捕获,就可以获取到原始方法的异常信息了

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }
}

在 catch 方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和业务需求有关了。

抛出异常后通知获取异常
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
    private void pt(){}
    
    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }
}

注意:

在这里插入图片描述

AOP事务管理

事务作用:在数据层保障一系列的数据库操作同成功同失败

Spring事务简介

Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

在这里插入图片描述

commit是用来提交事务,rollback是用来回滚事务。

PlatformTransactionManager只是一个接口,Spring 还为其提供了一个具体的实现:

在这里插入图片描述

从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。其内部采用的是 JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理器来管理你的事务,而Mybatis内部采用的就是JDBC的事务。

Spring管理事务具体步骤

  1. 在需要被事务管理的方法上添加注解

    在类上加@Transactional注解

    • @Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上
    • 写在接口类上,该接口的所有实现类的所有方法都会有事务
    • 写在接口方法上,该接口的所有实现类的该方法都会有事务
    • 写在实现类上,该类中的所有方法都会有事务
    • 写在实现类方法上,该方法上有事务
    • 建议写在实现类或实现类的方法上
  2. 在非主配置类中配置事务管理器

    //配置事务管理器,mybatis使用的是jdbc事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource
    dataSource){
        DataSourceTransactionManager transactionManager = new
        DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
    

    注意: 事务管理器要根据使用技术进行选择,Mybatis 框架使用的是 JDBC 事务,可以直接使用 DataSourceTransactionManager

  3. 开启事务注解

    // 在说配置类中配置事务管理器
    @EnableTransactionManagement
    // 加上该注解即可
    
名称@EnableTransactionManagement
类型配置类注解
位置配置类定义上方
作用设置当前 Spring 环境中开启注解式事务支持
名称@Transactional
类型接口注解 类注解 方法注解
位置业务层接口上方 业务层实现类上方 业务方法上方
作用为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)
### Spring事务角色

事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法

事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以是业务层方法

举个例子:

未开事务前:

在这里插入图片描述

  • AccountDao 的 outMoney 因为是修改操作,会开启一个事务T1
  • AccountDao 的 inMoney 因为是修改操作,会开启一个事务T2
  • AccountService的transfer没有事务
    • 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行,就会导致数据出现错误

开启事务后:

在这里插入图片描述

  • transfer 上添加了@Transactional注解,在该方法上就会有一个事务
  • AccountDao 的 outMoney 方法的事务T1加入到 transfer 的事务T中
  • AccountDao 的 inMoney 方法的事务T2加入到 transfer 的事务T中
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性

事务配置

在这里插入图片描述

上面这些属性都可以在@Transactional注解的参数上进行设置

  • readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true
  • timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间
  • rollbackFor:当出现指定异常进行事务回滚
  • noRollbackFor:当出现指定异常不进行事务回滚
    • 并不是所有的异常都会回滚事务,Spring 的事务只会对Error异常和RuntimeException异常及其子类进行事务回滚,其他的异常类型是不会回滚的
  • rollbackForClassName 等同于 rollbackFor,只不过属性为异常的类全名字符串
  • noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串
  • isolation 设置事务的隔离级别
    • DEFAULT:默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED:读未提交
    • READ_COMMITTED:读已提交
    • REPEATABLE_READ:重复读取
    • SERIALIZABLE:串行化

事务的传播行为

在这里插入图片描述

  • log方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务T1,T2,T3

  • transfer 因为加了@Transactional注解,也开启了事务T

  • 现在需要让 inMoney 和 outMoney 方法加入事务T,而 log 方法不加入(开启一个新事务)

  • @Service
    public class LogServiceImpl implements LogService {
        @Autowired
        private LogDao logDao;
        //propagation设置事务属性:传播行为设置为当前操作需要新事务
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void log(String out,String in,Double money ) {
            logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
        }
    }
    

事务传播行为:事务协调员对事务管理员所携带事务的处理态度。

事务传播机制的可选值:

在这里插入图片描述

对于开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了
性为异常的类全名字符串

  • noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串
  • isolation 设置事务的隔离级别
    • DEFAULT:默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED:读未提交
    • READ_COMMITTED:读已提交
    • REPEATABLE_READ:重复读取
    • SERIALIZABLE:串行化

事务的传播行为

[外链图片转存中…(img-HRirldMA-1716951605866)]

  • log方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务T1,T2,T3

  • transfer 因为加了@Transactional注解,也开启了事务T

  • 现在需要让 inMoney 和 outMoney 方法加入事务T,而 log 方法不加入(开启一个新事务)

  • @Service
    public class LogServiceImpl implements LogService {
        @Autowired
        private LogDao logDao;
        //propagation设置事务属性:传播行为设置为当前操作需要新事务
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void log(String out,String in,Double money ) {
            logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
        }
    }
    

事务传播行为:事务协调员对事务管理员所携带事务的处理态度。

事务传播机制的可选值:

[外链图片转存中…(img-OsZ4kJqf-1716951605866)]

对于开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我可是万西西呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值