一、什么是AOP?

AOP(Aspect Oriented Programming)指的是面向切面编程,它的出现并不是用来替代OOP(Object Oriented Programming)的,而是基于OOP(Object Oriented Programming)基础之上新的编程思想。

面向切面编程,通俗一点说,就是指将某段代码动态的切入到指定方法的指定位置进行运行的编程方式。

举个例子,假设我们有加减乘除四个方法,并且要在每个方法运行的时候添加日志记录,有以下几种做法:

1.直接编写在方法内部;

  1. public class NumberUtil {
  2. public static int add(int i, int j) {
  3. System.out.println("运行add..,参数是" + i + "," + j);
  4. int result = i + j;
  5. System.out.println("运行add..,结果是" + result);
  6. return result;
  7. }
  8. public static int sub(int i, int j) {
  9. System.out.println("运行sub..,参数是" + i + "," + j);
  10. int result = i - j;
  11. System.out.println("运行sub..,结果是" + result);
  12. return result;
  13. }
  14. public static int mul(int i, int j) {
  15. System.out.println("运行mul..,参数是" + i + "," + j);
  16. int result = i * j;
  17. System.out.println("运行mul..,结果是" + result);
  18. return result;
  19. }
  20. public static int div(int i, int j) {
  21. System.out.println("运行div..,参数是" + i + "," + j);
  22. int result = i / j;
  23. System.out.println("运行div..,结果是" + result);
  24. return result;
  25. }
  26. }

加减乘除是业务核心逻辑,添加日志记录只是系统的辅助功能,直接将日志记录编写在方法内部,导致业务核心逻辑和系统辅助功能代码耦合,代码可读性差,并且修改维护麻烦,所以不推荐这么做。

2.写一个工具类,在方法内部调用工具类

  1. public class NumberUtil {
  2. public static int add(int i, int j) {
  3. LogUtil.logStart(i,j);
  4. int result = i + j;
  5. LogUtil.logEnd(i,j);
  6. return result;
  7. }
  8. public static int sub(int i, int j) {
  9. LogUtil.logStart(i,j);
  10. int result = i - j;
  11. LogUtil.logEnd(i,j);
  12. return result;
  13. }
  14. public static int mul(int i, int j) {
  15. LogUtil.logStart(i,j);
  16. int result = i * j;
  17. LogUtil.logEnd(i,j);
  18. return result;
  19. }
  20. public static int div(int i, int j) {
  21. LogUtil.logStart(i,j);
  22. int result = i / j;
  23. LogUtil.logEnd(i,j);
  24. return result;
  25. }
  26. }
  27. public class LogUtil {
  28. public static void logStart(Object...objects) {
  29. System.out.println("运行" + Arrays.asList(objects));
  30. }
  31. public static void logEnd(Object...objects) {
  32. System.out.println("结束运行" + Arrays.asList(objects));
  33. }
  34. }

写工具类的好处是如果需要修改日志的输出,只需要改日志工具类即可,但是这种方式日志记录和业务逻辑代码还是耦合,而且可能很多功能都需要调用LogUtil,需要考虑不同业务的兼容性问题。

3.采用动态代理的方式

采用动态代理的意思是,假设我们有一个NumberUtil类的代理对象,我们通过代理对象去执行NumberUtil的加减乘除方法,就可以在方法前后做其他操作,而且不会影响NumberUtil类中本身的业务逻辑。

public interface BaseNumberUtil {

    public int add(int i, int j);

    public int sub(int i, int j);

    public int mul(int i, int j);

    public int div(int i, int j);

}

public class NumberUtil implements BaseNumberUtil{

    @Override
    public int add(int i, int j) {
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        return i / j;
    }


}

public class LogUtil {

    public static void logStart(Method method, Object... args) {
        System.out.println(method.getName() + "方法开始执行,传入的参数是" + Arrays.asList(args));
    }

    public static void logEnd(Method method, Object... result) {
        System.out.println(method.getName() + "方法执行结束,返回的结果是" + Arrays.asList(result));
    }

    public static void logException(Method method, Exception e) {
        System.out.println(method.getName() + "方法执行异常,异常信息是" + e.getCause());
    }

    public static void logFinally(Method method) {
        System.out.println(method.getName() + "方法执行结束");
    }
}

/**
 * 帮NumberUtil.java生成代理对象
 */
public class NumberUtilProxy {

    /**
     * 为传入的参数创建一个代理对象
     *
     * @param numberUtil 被代理的对象
     * @return
     */
    public static BaseNumberUtil getProxy(final BaseNumberUtil numberUtil){

        // 被代理对象的类加载器
        ClassLoader loader = numberUtil.getClass().getClassLoader();
        // 对象所实现的所有接口
        Class<?>[] interfaces = numberUtil.getClass().getInterfaces();
        // 方法执行器,帮我们目标对象执行方法
        InvocationHandler h = new InvocationHandler() {
            /**
             * @param proxy 代理对象,给jdk使用,任和时候都不要懂这个对象
             * @param method 当前将要执行的目标对象的方法
             * @param args 这个方法调用时外界传入的参数值
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    LogUtil.logStart(method,args);
                    // 利用反射执行目标方法
                    result = method.invoke(numberUtil, args);
                    LogUtil.logEnd(method,result);
                } catch (Exception e){
                    LogUtil.logException(method,e);
                } finally {
                    LogUtil.logFinally(method);
                }
                // result是目标方法执行后的返回值,将目标方法执行结果返回,只有将执行结果返回出去,外界才能拿到真正执行后的返回值
                return result;
            }
        };

        // Proxy为目标对象创建代理对象,这个方法是jdk的,直接用,作用就是为目标对象创建代理对象
        return (BaseNumberUtil) Proxy.newProxyInstance(loader,interfaces,h);
    }
}

public class TestNumberUtilProxy {

    @Test
    public void test(){

        BaseNumberUtil numberUtil = new NumberUtil();

        // 拿到了NumberUtil的代理对象,这个代理对象最终也会去调用NumberUtil的加减乘除方法
        BaseNumberUtil proxy = NumberUtilProxy.getProxy(numberUtil);

         /**
         * add方法开始执行,传入的参数是[1, 2]
         * add方法执行结束,返回的结果是[3]
         * add方法执行结束
         */
        proxy.add(1,2);
        /**
         * sub方法开始执行,传入的参数是[1, 2]
         * sub方法执行结束,返回的结果是[-1]
         * sub方法执行结束
         */
        proxy.sub(1,2);
        /**
         * mul方法开始执行,传入的参数是[2, 3]
         * mul方法执行结束,返回的结果是[6]
         * mul方法执行结束
         */
        proxy.mul(2,3);
        /**
         * div方法开始执行,传入的参数是[2, 0]
         * div方法执行异常,异常信息是java.lang.ArithmeticException: / by zero
         * div方法执行结束
         */
        proxy.div(2,0);
    }

}

用动态代理,日志记录可以做的非常强大,而且与业务逻辑解耦。但是动态代理存在两个问题。

第一个问题,动态代理写起来很复杂,不同的业务需要创建不同的代理类;

第二个问题,jdk默认的动态代理,如果目标对象没有实现任何接口,是无法为他创建代理对象的。我们在创建代理的时候,传入了一个参数interfaces,这个参数是对象所实现的所有接口,代理对象也实现了被代理对象实现的接口,换句话说,代理对象和被代理对象唯一产生的关联是实现了同一个接口。所以,如果目标对象没有实现任何接口,就无法为他创建代理对象。

Spring为了解决动态代理上述的两个问题,产生了AOP,AOP底层就是动态代理。我们可以利用Spring一句代码都不写的去创建动态代理。AOP实现简单,而且不要求目标对象必须实现某个接口。

二、AOP中的专业术语

以给方法添加日志为例,如果不给方法添加日志,正常情况下,方法会直接运行业务逻辑直至结束。但是我们想要在方法开始前,方法结束后,方法出现异常,方法完全结束的地方添加日志。

横切关注点:方法不同的位置就是我们的横切关注点。

通知方法:业务逻辑方法不同位置调用的日志记录方法就是通知方法。

切面类:写了通知方法的类,就叫做切面类。

连接点:每一个方法的每一个位置都是一个连接点,切面类的方法和业务逻辑方法可以在连接点建立连接。

切入点:可能我们只需要在方法异常的时候记录日志,并不是每一个点都记录日志。我们真正需要执行日志记录的地方就叫做切入点。

切入点表达式:在众多连接点中选出我们感兴趣的地方。就好比sql语句,从众多数据中选出我们需要的一些数据。

3. Aop - 图1

三、AOP的操作

1. AOP的使用步骤

上面介绍了手动编写动态代理的方式实现切面编程,那么如何使用Spring将切面类LogUtil中的通知方法动态的在目标方法运行的各个位置切入呢?我们的目标是在没有自己编写动态代理的情况下,让Spring替我们实现将日志记录插入到方法运行的不同位置。

第一步:导包

// Spring基础包
spring-expression-5.2.6.RELEASE.jar
spring-core-5.2.6.RELEASE.jar
spring-context-5.2.6.RELEASE.jar
spring-beans-5.2.6.RELEASE.jar
spring-aop-5.2.6.RELEASE.jar
commons-logging-1.2.jar
// Spring支持面向切面编程的包——基础版
spring-aspects-5.2.3.RELEASE.jar
// Spring支持面向切面编程的包——增强版(即使目标对象没有实现任何接口,也可以实现动态代理)
com.springsource.org.aspectj.weaver-1.7.2.RELEASE.jar
aopalliance-1.0-sources.jar
cglib-nodep-javadoc-2.2.jar

第二步:写配置

1.将目标类和切面类(封装了通知方法(在目标方法执行前后执行的方法)的类)加入到IOC容器中

<!--开启组件扫描-->
<context:component-scan base-package="com.ysy"></context:component-scan>
@Component
public class LogUtil {}

@Component
public class NumberUtil implements BaseNumberUtil{}

2.告诉Spring,哪个是切面类

@Component
@Aspect // 告诉Spring,这个类是切面类
public class LogUtil {}

3.告诉Spring,通知方法要在何时何地运行

@Component
@Aspect // 告诉Spring,这个类是切面类
public class LogUtil {

    /**
     * 告诉Spring,每个方法都什么时候运行
     * @Before          前置通知
     * @After()         后置通知
     * @AfterReturning  返回通知
     * @AfterThrowing   异常通知
     * @Around()        环绕通知
     */
    // 目标方法开始执行之前执行
    // execution是固定写法,如果你的方法有参数,这里必须也写参数类型,否则找不到该方法
    @Before("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logStart() {
        System.out.println("方法开始执行,传入的参数是");
    }

    // 目标方法正常执行结束,拿到返回结果的时候执行
    @AfterReturning("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logEnd() {
        System.out.println("方法执行结束,返回的结果是");
    }

    // 目标方法发生异常之后执行
    @AfterThrowing("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logException() {
        System.out.println("方法执行异常,异常信息是");
    }

    // 目标方法执行结束之后执行
    @After("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logFinally() {
        System.out.println("方法执行结束");
    }
}

4.开启基于注解的AOP功能

<!--开启基于注解的AOP功能-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

第三步:测试

public class TestNumberUtilProxy {

    ApplicationContext context = new ClassPathXmlApplicationContext("applicatioonContext.xml");

    @Test
    public void test(){
        /**
         * 从IOC容器中拿到目标对象
         * 这里为什么不用new,因为只有从容器中拿的对象才是受容器管理的,才能使用容器强大的功能
         * 如果要用类型获取对象,一定要使用接口类型,不能使用本类
         */
        BaseNumberUtil bean = context.getBean(BaseNumberUtil.class);
        /**
        * logStart——XXX方法开始执行,传入的参数是
        * logFinally——XXX方法执行结束
        * logEnd——XXX方法执行结束,返回的结果是
        */
        bean.add(1,2);
    }

}

2. AOP的使用细节

2.1 IOC容器中保存的组件是代理对象

我们从IOC容器中拿到目标对象,然后打印目标对象,会发现打印的是com.ysy.learn.common.util.NumberUtil@53aad5d5,接着打印目标对象的类型,发现打印的是class com.sun.proxy.$Proxy21,也就是说,容器中保存的组件不是NumberUtil本身,而是它的代理对象$Proxy21。这也侧面反应了AOP的底层实现是动态代理。

public void test(){
    /**
    * 从IOC容器中拿到目标对象
    * 这里为什么不用new,因为只有从容器中拿的对象才是受容器管理的,才能使用容器强大的功能
    * 如果要用类型获取对象,一定要使用接口类型,不能使用本类
    */
    BaseNumberUtil bean = context.getBean(BaseNumberUtil.class);
    // com.ysy.learn.common.util.NumberUtil@53aad5d5
    System.out.println(bean);
    // class com.sun.proxy.$Proxy21
    System.out.println(bean.getClass());
    bean.add(1,2);
}

为什么说如果要用类型获取对象,一定要使用接口类型获取,不能使用本类获取呢?

因为容器中保存的组件不是NumberUtil本身,而是它的代理对象$Proxy21,很显然,NumberUtil$Proxy21不是同一个对象,使用本类NumberUtil获取,就会报如下错误:

NoSuchBeanDefinitionException: No qualifying bean of type 'com.ysy.learn.common.util.NumberUtil' available

而代理对象与目标对象唯一的关联就是他俩都实现了同一个接口,所以我们在用类型获取对象的时候,一定要使用接口类型,不能使用本类。

我们的接口BaseNumberUtil上面并没有添加Spring创建对象的注解,说明BaseNumberUtil没有加入Spring容器中,那我们为什么还能从Spring容器中获取到对象呢?

Spring会在容器中找已存在的组件,然后看哪一个组件是BaseNumberUtil类型。NumberUtilLogUtil都是容器中已经存在的组件,但是NumberUtil实现了BaseNumberUtil接口,所以很显然NumberUtilBaseNumberUtil类型,所以可以获取到对象。

为什么不在BaseNumberUtil接口上面添加注解,让BaseNumberUtil也加入Spring容器中呢?

实际上,BaseNumberUtil接口也可以添加创建对象的注解,而且即使加了注解,Spring也不会去为接口创建对象(无法为接口创建对象)。在接口上添加创建对象的注解,相当于告诉Spring,IOC容器中可能有这种类型的组件。

当然,只有某个类需要被动态切入其他方法,才会给这个类创建代理对象,如果我们没有切面类,就意味着这个类不需要被切入,就不会创建代理对象,所以此时获取到的对象类型就是他本身的类型。

@Component
//Aspect // 注释切面类的注解,就不存在切面类
public class LogUtil {}

public void test(){

    BaseNumberUtil bean = context.getBean(BaseNumberUtil.class);
    // com.ysy.learn.common.util.NumberUtil@2145b572
    System.out.println(bean);
    // class com.ysy.learn.common.util.NumberUtil
    System.out.println(bean.getClass());
    bean.add(1,2);
}

上述方式中,NumberUtil实现了BaseNumberUtil接口,我们在上面说过,AOP的增强包,即使目标对象没有实现任何接口,也可以实现动态代理。现在我们还是创建NumberUtil,不实现BaseNumberUtil接口,然后直接用NumberUtil本类在容器中获取对象,查看这个对象的类型,发现当前对象的类型是com.ysy.learn.common.util.NumberUtilEnhancerBySpringCGLIB36a3457f,这意味着我们拿到的类型是NumberUtil,然后CGLIB帮我们在这个类中创建了一个内部类EnhancerBySpringCGLIB,这个内部类实现了NumberUtil的所有方法,这个内部类就是NumberUtil的代理对象。

/**
* NumberUtil没有实现BaseNumberUtil接口,我们还是使用LogUtil作为切面类
*/
@Component
public class NumberUtil /*implements BaseNumberUtil*/{

    //@Override
    public int add(int i, int j) {
        return i + j;
    }

    //@Override
    public int sub(int i, int j) {
        return i - j;
    }

    //@Override
    public int mul(int i, int j) {
        return i * j;
    }

    //@Override
    public int div(int i, int j) {
        return i / j;
    }

}

/**
* 测试是否打印日志信息,并且查看拿到的目标对象的Class
*/
public void test1(){

    NumberUtil bean = context.getBean(NumberUtil.class);
    // com.ysy.learn.common.util.NumberUtil@437da279
    System.out.println(bean);
    // class com.ysy.learn.common.util.NumberUtil$$EnhancerBySpringCGLIB$$36a3457f
    System.out.println(bean.getClass());
    // logStart——XXX方法开始执行,传入的参数是
    // logFinally——XXX方法执行结束
    // logEnd——XXX方法执行结束,返回的结果是
    bean.add(1,2);
}

2.2 切入点表达式写法

切入点表达式的固定格式:execution(访问权限符 返回值类型 方法全类名(参数列表))

@AfterReturning("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
public static void logEnd() {
    System.out.println("logEnd——XXX方法执行结束,返回的结果是");
}

*

  1. 匹配一个或多个字符:execution(public int com.ysy.learn.common.util.Number*.*(int,int))
  2. 匹配任意一个参数:execution(public int com.ysy.learn.common.util.Number*.*(int,*))
  3. 如果放在路径中,只能匹配一层路径:`execution(public int com.ysy.learn.common..Number.(int,*))`
  4. *不能放在访问权限符的位置,如果要表示任意权限,访问权限符可以不写(public可选,private也切不了)

..

  1. 匹配任意多个,任意类型参数:execution(public int com.ysy.learn.common.util.Number*.*(..))
  2. 匹配多层路径:execution(public int com.ysy.learn.common..Number*.*(int,*))

最精确的写法:execution(public int com.ysy.learn.common.util.NumberUtil.add(int,int))

最模糊的写法:execution(* *.*(..))

&&、||、!

  1. &&:必须同时满足两个表达式execution(* *.*(..)) && execution(* NumberUtil.*(..))
  2. ||:满足其中一个即可execution(* *.*(..)) || execution(* NumberUtil.*(..))
  3. !:只要不是表达式中的方法,都做切入

2.3 通知方法的执行顺序

不同的注解表示执行的位置如下所示:

如果方法正常,执行的顺序是:@Before(前置通知)——>@After(后置通知)——> @AfterReturning(正常返回)

方法异常,执行的顺序是:@Before(前置通知)——>@After(后置通知)——> @AfterThrowing(方法异常)

try{
    @Before
    method.invoke(obj,args)
    @AfterReturning
} catch {
    @AfterThrowing
} finally {
    @After
}

2.4 获取目标方法的各种信息


@Component
@Aspect // 告诉Spring,这个类是切面类
public class LogUtil {

    /**
     * 告诉Spring,每个方法都什么时候运行
     *
     * @Before 前置通知
     * @After() 后置通知
     * @AfterReturning 返回通知
     * @AfterThrowing 异常通知
     * @Around() 环绕通知
     */
    // 目标方法开始执行之前执行
    // execution是固定写法,如果你的方法有参数,这里必须也写参数类型,否则找不到该方法
    @Before("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logStart(JoinPoint joinPoint) {
        // 获取目标方法的参数
        Object[] args = joinPoint.getArgs();
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("logStart——" + method + "方法开始执行,传入的参数是" + Arrays.asList(args));
    }

    // 目标方法正常执行结束,拿到返回结果的时候执行,用returning告诉Spring,result是用来接收方法返回结果的
    @AfterReturning(value = "execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))", returning = "result")
    // 这里的方法返回值类型如果我们写了int,就接不到其他类型的返回值
    public static void logEnd(JoinPoint joinPoint, Object result) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("logEnd——" + method + "方法执行结束,返回的结果是" + result);
    }

    // 目标方法发生异常之后执行,用throwing告诉Spring,e是用来接收方法返回异常的
    // 注意这里的Exception指定方法接收哪一种异常,如果我们这里写的NullPointerException,就接不到其他异常
    @AfterThrowing(value = "execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))", throwing = "e")
    public static void logException(JoinPoint joinPoint, Exception e) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("logException——" + method + "方法执行异常,异常信息是");
    }

    // 目标方法执行结束之后执行
    @After("execution(public int com.ysy.learn.common.util.NumberUtil.*(int,int))")
    public static void logFinally(JoinPoint joinPoint) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("logFinally——" + method + "方法执行结束");
    }
}

补充内容:Spring对通知方法的约束

为什么我们在通知方法logEnd()随便加了一个Object result参数就可以接收到方法的返回值?

首先,Spring对通知方法的约束不严格,通知方法中我们写不写static,返回值是void还是其他类型,访问权限是public还是private都不会影响通知方法的执行,唯一会影响的是通知方法的参数列表。因为通知方法是Spring通过反射调用的,每次调用通知方法,Spring都要确定这个方法参数列表的值,所以Spring得知道方法的每一个参数代表了什么。像JoinPoint,是Spring自己的接口,所以它肯定认识,我们加了一个Object result,如果不告诉Spring这个result代表什么,Spring不认识这个result,就会报错,正是因为我们用returning = "result"告诉了Spring,result是用来接收方法返回值的,所以Spring可以正确的将方法返回值给到result

补充内容:抽取可重用的切入点表达式

我们在每一个通知方法上面都写了切入点表达式,可能很多方法上面的切入点表达式是一样的,后续如果想修改表达式,不方便修改。那么如何将所有相同的切入点表达式抽取出来,实现重用呢?

// 1. 声明一个没有实现的返回void的空方法
// 2. 在方法上用@Pointcut注解,写上需要重用的切入点表达式
@Pointcut(value = "execution(public int com.ysy.learn.common.util.NumberUtil.*(..))")
public void executionCommon(){};

// 3. 在通知方法切入点表达式的位置,直接写声明的空方法的方法名
@AfterThrowing(value = "executionCommon()", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e) {
    // 获取目标方法的签名
    Signature signature = joinPoint.getSignature();
    String method = signature.getName();
    System.out.println("logException——" + method + "方法执行异常,异常信息是"+ e);
}

2.5 环绕通知

环绕通知是Spring中最强大的通知,将前置通知、返回通知、后置通知、异常通知四合一就是环绕通知,环绕通知就相当于我们的动态代理。


@Component
@Aspect // 告诉Spring,这个类是切面类
public class LogUtil {

    /**
     * @param pjp JoinPoint的子接口,可以拿到目标方法的各种信息
     * @return
     * @throws Throwable
     */
    @Around(value = "executionCommon()")
    public static Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        Object proceed = null;
        String name = "";
        try {
            name = pjp.getSignature().getName();
            Object[] args = pjp.getArgs();
            // 这个相当于@Before
            System.out.println("[环绕前置]" + name + "开始执行,参数是" + Arrays.asList(args));
            // 这个方法就相当于method.invoke(obj,args),返回的值就是调用的目标方法的返回值
            proceed = pjp.proceed(args);
            // 这个相当于@AfterReturning
            System.out.println("[返回通知]" + name + "执行正常结束,结果是" + proceed);
        } catch (Exception e) {
            throw new RuntimeException(e);
            // 这个相当于@AfterThrowing
            System.out.println("[异常通知]" + name + "执行异常,异常信息是" + e);
        } finally {
            // 这个相当于@After
            System.out.println("[环绕后置]" + name + "最终结束");
        }

        // 将目标方法的返回值返回出去,外界才能接得到
        return proceed;
    }

    @Pointcut(value = "execution(public int com.ysy.learn.common.util.NumberUtil.*(..))")
    public void executionCommon() {
    }

}

补充内容:如果环绕通知和普通通知同时开启,它们的执行顺序是什么样的?

从例子中可以看出环绕通知的执行顺序是优先于普通通知的。而且如果方法出现异常,环绕通知可以捕捉到异常信息,普通通知会认为方法正常结束,因为环绕通知中已经把异常捕捉了,到普通通知时,没有接收到异常,所以为了让普通通知接收到异常,需要throw new RuntimeException(e)

环绕通知因为相当于动态代理,所以它可以修改参数,修改返回结果等,可以影响到目标方法,但是普通通知不会影响到目标方法,所以一般如果只是记录日志,建议用普通通知,如果确定需要影响目标方法的逻辑等,就用环绕通知。

@Test
public void test1(){
  果要用类型获取对象,一定要使用接口类型,不能使用本类

    NumberUtil bean = context.getBean(NumberUtil.class);
    /*
        [环绕前置]add开始执行,参数是[1, 2]
        logStart——add方法开始执行,传入的参数是[1, 2]
        [环绕返回通知]add执行正常结束,结果是3
        [环绕后置]add最终结束
        logFinally——add方法执行结束
        logEnd——add方法执行结束,返回的结果是3
     */
    bean.add(1,2);
    System.out.println("----------------------------");
    /*
        [环绕前置]div开始执行,参数是[1, 0]
        logStart——div方法开始执行,传入的参数是[1, 0]
        [环绕异常通知]div执行异常,异常信息是java.lang.ArithmeticException: / by zero
        [环绕后置]div最终结束
        logFinally——div方法执行结束
        logEnd——div方法执行结束,返回的结果是null
    */
    bean.div(1,0);
}

2.6 多切面运行顺序

多切面意思就是有多个切面类同时切入目标方法。所以除了LogUtil切面类,我们再写一个切面类CheckUtil,让这两个类同时切入NumberUtil类中的方法。注意:如果要在类中使用另一个类的切入点表达式,只需要写抽取的切入点表达式方法的全签名com.ysy.learn.common.LogUtil.executionCommon()即可。

@Component
@Aspect // 告诉Spring,这个类是切面类
public class CheckUtil {

    @Before("com.ysy.learn.common.LogUtil.executionCommon()")
    public static void logStart(JoinPoint joinPoint) {
        // 获取目标方法的参数
        Object[] args = joinPoint.getArgs();
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("[CheckUtil---]logStart——" + method + "方法开始执行,传入的参数是" + Arrays.asList(args));
    }

    // 目标方法正常执行结束,拿到返回结果的时候执行
    @AfterReturning(value = "com.ysy.learn.common.LogUtil.executionCommon()", returning = "result")
    public static void logEnd(JoinPoint joinPoint, Object result) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("[CheckUtil---]logEnd——" + method + "方法执行结束,返回的结果是" + result);
    }

    // 目标方法发生异常之后执行
    @AfterThrowing(value = "com.ysy.learn.common.LogUtil.executionCommon()", throwing = "e")
    public static void logException(JoinPoint joinPoint, Exception e) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("[CheckUtil---]logException——" + method + "方法执行异常,异常信息是" + e);
    }

    // 目标方法执行结束之后执行
    @After("com.ysy.learn.common.LogUtil.executionCommon()")
    public static void logFinally(JoinPoint joinPoint) {
        // 获取目标方法的签名
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        System.out.println("[CheckUtil---]logFinally——" + method + "方法执行结束");
    }

}

两个切面类同时切入NumberUtil类中的方法,之后进行测试,发现输出的日志如下:先进来的切面类后出,后进来的切面类方法先出。

@Test
public void test1(){

    NumberUtil bean = context.getBean(NumberUtil.class);

    /*
        [CheckUtil---]logStart——add方法开始执行,传入的参数是[1, 2]      
        [LogUtil---]logStart——add方法开始执行,传入的参数是[1, 2]        
        [LogUtil---]logFinally——add方法执行结束
        [LogUtil---]logEnd——add方法执行结束,返回的结果是3
        [CheckUtil---]logFinally——add方法执行结束
        [CheckUtil---]logEnd——add方法执行结束,返回的结果是3
    */
    bean.add(1,2);

}

先进后出,后进先出是什么意思呢?

大概就是如图所示,NumberUtil的方法运行的时候,首先被CheckUtil切入,之后被LogUtil切入,之后执行NumberUtil的方法,方法执行完之后,后切入的LogUtil先执行结束,之后先切入的CheckUtil执行结束。

3. Aop - 图2

为什么是先执行CheckUtil,而不是先执行LogUtil呢?

这个默认是根据累的首字母判断的,CheckUtil的首字母CLogUtil首字母L之前,所以先执行CheckUtil,不过可以手动设置他们的执行顺序。在切面类上用注解@Order可以设置切面类的执行顺序。进行如下设置之后,LogUtil就会先执行。

@Component
@Aspect // 告诉Spring,这个类是切面类
@Order(1)
public class LogUtil {

@Component
@Aspect // 告诉Spring,这个类是切面类
@Order(2)
public class CheckUtil {}

如果切面类中既有普通通知,又有环绕通知,那么执行顺序是怎样的?

按照之前的理解,环绕通知要执行方法,所以应该是先执行两个切面类的前置通知,接着执行环绕通知,最后执行两个切面类的后置通知或异常通知。但是实际上不是这样,他的执行顺序还是按照先进后出的规则,前置通知—>CheckUtil的后置通知—>CheckUtil的返回通知—>LogUtil的环绕通知—>LogUtil的后置通知—>LogUtil的返回通知。因为这个环绕通知是LogUtil的环绕通知,和CheckUtil没有关系,所以它不会穿插在CheckUtil的普通通知之间。所以说,环绕通知只会影响当前切面。

@Test
public void test1(){

    NumberUtil bean = context.getBean(NumberUtil.class);

    /*
        [LogUtil---][环绕前置]add开始执行,参数是[1, 2]
        [LogUtil---]logStart——add方法开始执行,传入的参数是[1, 2]
        [CheckUtil---]logStart——add方法开始执行,传入的参数是[1, 2]
        ---方法内部执行---
        [CheckUtil---]logFinally——add方法执行结束
        [CheckUtil---]logEnd——add方法执行结束,返回的结果是3
        [LogUtil---][环绕返回通知]add执行正常结束,结果是3
        [LogUtil---][环绕后置]add最终结束
        [LogUtil---]logFinally——add方法执行结束
        [LogUtil---]logEnd——add方法执行结束,返回的结果是3
    */
    bean.add(1,2);

}

四、AOP的使用场景

  1. AOP加日志保存到数据库
  2. 做权限验证
  3. AOP做安全检查
  4. AOP做事务控制