Sping项目AOP通知类型

发布于:2022-10-28 ⋅ 阅读:(447) ⋅ 点赞:(0)
@Before("ptSelect()")

它所代表的含义是将通知添加到切入点方法执行的前面

除了这个注解外,还有没有其他的注解,换个问题就是除了可以在前面加,能不能在其他的地方加?

1. 类型介绍

回顾下AOP通知:

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

通知具体要添加到切入点的哪里?

共提供了5种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)

为了更好的理解这几种通知类型,我们来看一张图

在这里插入图片描述

(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容;

(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容;

(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加;

(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加;

(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,下面就可以介绍。

2. 环境准备

  • 创建一个Maven项目

  • pom.xml添加Spring依赖

        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.2.15.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.6</version>
            </dependency>
        </dependencies>
    
  • 添加UserDao和UserDaoImpl类

    public interface UserDao {
        public void select();
        public void update();
    }
    
    @Repository
    public class UserDaoImpl implements UserDao {
    
    
        public String  select() {
            System.out.println("UserDao select, running .....");
            return "UserDao select";
        }
    
        public String update() {
            System.out.println("UserDao update, running ......");
            return "UserDao update";
        }
    }
    
    
  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.dcxuexi")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  • 创建通知类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
        public void ptSelect(){}
    
        @Before("ptSelect()")
        public void before(){
            System.out.println("MyAdvice before() , running .....");
        }
    
        public void after(){
            System.out.println("MyAdvice after() , running .....");
        }
    
        public void around(){
            System.out.println("MyAdvice around() , running .....");
        }
    
        public void afterReturning(){
            System.out.println("MyAdvice afterReturning() , running .....");
        }
    
        public void afterThowing(){
            System.out.println("MyAdvice afterThowing() , running .....");
        }
    
    
    }
    
  • 编写SpringAopAdviceType运行类

    public class SpringAopAdviceType {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserDao userDao = context.getBean(UserDao.class);
            userDao.select();
        }
    }
    

最终创建好的项目结构如下:

在这里插入图片描述

3. 通知类型的使用

前置通知

修改MyAdvice,在before方法上添加@Before注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    //此处也可以写成 @Before("MyAdvice.ptSelect()"),不建议
    @Before("ptSelect()")
    public void before(){
        System.out.println("MyAdvice before() , running .....");
    }
}

在这里插入图片描述

后置通知

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    //此处也可以写成 @Before("MyAdvice.ptSelect()"),不建议
    @Before("ptSelect()")
    public void before(){
        System.out.println("MyAdvice before() , running .....");
    }

    @After("ptSelect()")
    public void after(){
        System.out.println("MyAdvice after() , running .....");
    }
}

在这里插入图片描述

环绕通知

基本使用

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    //此处也可以写成 @Before("MyAdvice.ptSelect()"),不建议
    @Before("ptSelect()")
    public void before(){
        System.out.println("MyAdvice before() , running .....");
    }

    @After("ptSelect()")
    public void after(){
        System.out.println("MyAdvice after() , running .....");
    }

    @Around("ptSelect()")
    public void around(){
        System.out.println("MyAdvice around() , running .....");
    }
}

在这里插入图片描述

运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体如何实现?

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    //此处也可以写成 @Before("MyAdvice.ptSelect()"),不建议
    @Before("ptSelect()")
    public void before(){
        System.out.println("MyAdvice before() , running .....");
    }

    @After("ptSelect()")
    public void after(){
        System.out.println("MyAdvice after() , running .....");
    }

    @Around("ptSelect()")
    public void around(ProceedingJoinPoint proceedingJoinPointp) throws Throwable {
        System.out.println("MyAdvice around() , running .....");
        System.out.println("Before ....");
        //表示对原始操作的调用
        proceedingJoinPointp.proceed();
        System.out.println("End ......");
    }
}

**说明:**proceed()为什么要抛出异常?

原因很简单,看下源码就知道了

在这里插入图片描述

再次运行,程序可以看到原始方法已经被执行了

在这里插入图片描述

注意事项

(1)原始方法有返回值的处理

  • 修改MyAdvice,对UserDao中的update方法添加环绕通知,
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    @Pointcut("execution(void com.dcxuexi.dao.UserDao.update())")
    public void ptUpdate(){}


    @Around("ptUpdate()")
    public void around(ProceedingJoinPoint proceedingJoinPointp) throws Throwable {
        System.out.println("MyAdvice around() , running .....");
        System.out.println("Before ....");
        proceedingJoinPointp.proceed();
        System.out.println("End ......");
    }
}
  • 修改SpringAopAdviceType类,调用select方法
public class SpringAopAdviceType {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserDao userDao = context.getBean(UserDao.class);
        String result = userDao.select();
        System.out.println(result);
    }
}

运行后会报错,错误内容为:

在这里插入图片描述

Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.dcxuexi.dao.UserDao.update() at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:226) at com.sun.proxy.$Proxy19.update(Unknown Source) at com.dcxuexi.SpringAopAdviceType.main(SpringAopAdviceType.java:22)

错误大概的意思是:空的返回不匹配原始方法的int返回

  • void就是返回Null
  • 原始方法就是UserDao下的update方法

所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    @Pointcut("execution(int com.dcxuexi.dao.UserDao.update())")
    public void ptUpdate(){}


    @Around("ptUpdate()")
    public Object around(ProceedingJoinPoint proceedingJoinPointp) throws Throwable {
        System.out.println("MyAdvice around() , running .....");
        System.out.println("Before ....");
        //表示对原始操作的调用
        Object proceed = proceedingJoinPointp.proceed();
        System.out.println("End ......");
        return proceed;
    }
}

说明:

​ 为什么返回的是Object而不是int的主要原因是Object类型更通用。

​ 在环绕通知中是可以对原始方法返回值就行修改的。

返回后通知

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    @Pointcut("execution(int com.dcxuexi.dao.UserDao.update())")
    public void ptUpdate(){}

    @AfterReturning("ptUpdate()")
    public void afterReturning(){
        System.out.println("MyAdvice afterReturning() , running .....");
    }
}

在这里插入图片描述

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

异常后通知

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    @Pointcut("execution(int com.dcxuexi.dao.UserDao.update())")
    public void ptUpdate(){}


    @AfterThrowing("ptUpdate()")
    public void afterThowing(){
        System.out.println("MyAdvice afterThowing() , running .....");
    }
}

在这里插入图片描述

**注意:**异常后通知是需要原始方法抛出异常,可以在update()方法中添加一行代码int i = 10/0即可。如果没有抛异常,异常后通知将不会被执行。

介绍完完这5种通知类型,我们来思考下环绕通知是如何实现其他通知类型的功能的?

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

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.dcxuexi.dao.UserDao.select())")
    public void ptSelect(){}

    @Pointcut("execution(int com.dcxuexi.dao.UserDao.update())")
    public void ptUpdate(){}
    
    @Around("ptUpdate()")
    public Object around(ProceedingJoinPoint proceedingJoinPointp)  {
         // 代码1   ## 增强代码写在代码1位置就是前置通知 ,效果如 @Before("ptUpdate()")
         Object proceed = null;
         try {
             // 代码2   ## 增强代码写在代码2位置就是前置通知 ,效果如 @Before("ptUpdate()")

             //表示对原始操作的调用
             proceed = proceedingJoinPointp.proceed();

             // 代码3   ## 增强代码写在代码3位置就是返回后通知 ,效果如@AfterReturning("ptUpdate()")

         } catch (Throwable throwable) {
             // 代码4   ## 增强代码写在代码4位置就是异常后通知 ,效果如 @AfterThrowing("ptUpdate()")
         }
         // 代码5   ## 增强代码写在代码5位置就是后置通知 ,效果如 @After("ptUpdate()")
         return proceed;
    }
}

在这里插入图片描述

通知类型总结

知识点1:@After

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

知识点2:@AfterReturning

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

知识点3:@AfterThrowing

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

知识点4:@Around

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

环绕通知注意事项

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

项目代码