8.Spring系列之AOP

一、什么是AOP?


 AOP是面向切面编程(Aspect-Oriented Programming),它是一种新的方法论,是对传统的面向对象编程的一种补充,更具体的说是在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

引用知乎用户的描述:地址https://www.zhihu.com/question/24863332/answer/48376158
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。
从技术上来说,AOP基本上是通过代理机制实现的。

二、需求


从AOP角度来看,我们开发过程中有哪些现有的需求可以改造成以Spring AOP来实现?以下举个简单例子:

首先,定义一个计算器接口

public interface Calculator {

    // 加法接口
    int add(int x,int y);
    
    // 减法接口 
    int sub(int x,int y);
}

接着,定义一个计算器实现接口,并在其中加入操作日志

@Service
public class CalculatorImpl implements Calculator{

    @Override
    public int add(int x, int y) {
        System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y));
        int z = x + y;
        System.out.println(String.format("接口执行结果,z=%s", z));
        return z;
    }

    @Override
    public int sub(int x, int y) {
        System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y));
        int z = x - y;
        System.out.println(String.format("接口执行结果,z=%s", z));
        return z;
    }

}

然后,在IOC容器上添加注解扫描包

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
    <!-- 开启注解扫描包 -->
    <context:component-scan base-package="com.spring"></context:component-scan>
</beans>

最后,写个测试方法

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator cal = ctx.getBean(Calculator.class);
        cal.add(20, 10);
        cal.sub(20, 10);
        /**
         * 执行结果:
         * 接口接收参数,x=20,y=10
         * 接口执行结果,z=30
         * 接口接收参数,x=20,y=10
         * 接口执行结果,z=10
         */
    }
}

我们在每个接口执行真正的业务之前都添加了日志输出,在真正的业务执行之后也添加了日志输出,看着很简单,但如果一个接口内几百个乃至上千个接口呢,那么就得写好多日志;再者,如果你拼命的把日志写好了,发现里面有个错别字或者不符合规范,又

得重新改几百个乃至几千个日志输出的信息。

解决方案:

1.使用java动态代理来完成这个事情

2.使用Spring的AOP面向切面编程

三、动态代理


 首先,我们先将service上的打印日志输出注释掉,引入动态代理类:

public class CalculatorProxy {
    
    // 要代理的对象(注意:代理的是接口)
    private Calculator target;
    
    // 初始化
    public CalculatorProxy(Calculator target) {
        super();
        this.target = target;
    }

    // 返回代理对象
    public Calculator getLoggingProxy(){
        Calculator proxy = null;
        
        ClassLoader loader = target.getClass().getClassLoader();
        Class[] interfaces = new Class[]{Calculator.class};
        InvocationHandler handler = new InvocationHandler() {
            /**
             * proxy: 代理对象。 一般不使用该对象
             * method: 正在被调用的方法
             * args: 调用方法传入的参数
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
                String methodName = method.getName();
                //打印日志
                System.out.println(String.format("接口接收参数,x=%s,y=%s", Arrays.asList(args).get(0),Arrays.asList(args).get(1)));
                //调用目标方法
                Object result = null;
                result = method.invoke(target, args);
                //打印日志
                int res = 0;
                if("add".equals(methodName)) {
                    res = Integer.parseInt(Arrays.asList(args).get(0).toString()) 
                        + Integer.parseInt(Arrays.asList(args).get(1).toString());
                }else {
                    res = Integer.parseInt(Arrays.asList(args).get(0).toString()) 
                        - Integer.parseInt(Arrays.asList(args).get(1).toString());
                }
                System.out.println(String.format("接口执行结果,z=%s", res));
                return result;
            }
        };
        
        /**
         * loader: 代理对象使用的类加载器。 
         * interfaces: 指定代理对象的类型. 即代理代理对象中可以有哪些方法. 
         * h: 当具体调用代理对象的方法时, 应该如何进行响应, 实际上就是调用 InvocationHandler 的 invoke 方法
         */
        proxy = (Calculator) Proxy.newProxyInstance(loader, interfaces, handler);
        return proxy;
    }
}

测试动态代理日志输出:

public class Main {

    public static void main(String[] args) {
        // 被代理对象,是一个接口实现类,即哪个实现类被代理
        Calculator calculator = new CalculatorImpl();
        // 返回代理对象
        calculator = new CalculatorProxy(calculator).getLoggingProxy();
        calculator.add(20, 10);
        calculator.sub(20, 10);
        /**
         * 执行结果:
         * 接口接收参数,x=20,y=10
         * 接口执行结果,z=30
         * 接口接收参数,x=20,y=10
         * 接口执行结果,z=10
         */
    }
}

四、AOP


 1.我们必须要清楚的几个概念:

①.切面(Aspect):  横切关注点被模块化的特殊对象,图示如下:
 
即前置日志、后置日志两个需求是横切关注点,而抽取出横切关注点形成的就是切面
②.通知(Advice):切面必须要完成的工作,比如日志切面要完成的工作是日志,所以通知在这里就是日志
③.目标(Target):被通知的对象,即真正的业务方法,比如我们在calculator.add方法执行前添加日志,calculator.add方法就是目标
④.代理(Proxy):向目标对象应用通知之后创建的对象
⑤.连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等,这就是连接点。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。
例如 calculator.add() 方法执行前的连接点,执行点为 calculator.add();方位为该方法执行前的位置
⑥.切点(pointcut):每个类都拥有多个连接点:例如 calculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。
2.使用AOP实现日志切面
前言:使用基于 AspectJ 注解(Java 社区里最完整最流行的 AOP 框架)
首先,引入相关jar包:
aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar

其次,定义一个日志切面,并且创建一个前置通知方法:

// 声明为一个切面
@Aspect
// 交给IOC容器管理
@Component
public class CalculatorAspectLogging {

    /**
     * 让这个方法知道该方法在哪些类的哪些方法开始之前执行
     * 声明该方法是一个前置通知
     * 表达式为:public 返回值 包名 接口名 参数类型,参数只需要类型
     * 参数:JoinPoint为连接点,可以获取参数
     */
    @Before("execution(public int com.spring.service.Calculator.add(int, int))")
    public void beforeMethod(JoinPoint joinPoint) {
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        //获取方法参数
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(String.format("接口方法:%s接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1)));
    }
}

表达式到对应的接口方法复制即可:

接着,在IOC容器中配置扫描表,以及开启AOP注解扫描:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
    
    <!-- 开启注解扫描包 -->
    <context:component-scan base-package="com.spring"></context:component-scan>
    
    <!-- 开启AOP注解扫描,让AOP注解生效 -->
    <!-- 使切面注解起作用 -->
    <!-- 当我们调用一个目标方法,而那个目标方法跟我们标注的通知注解相匹配的时候,aop框架自动的为目标方法所在的类生成一个代理对象 -->
    <!-- 在目标方法执行之前,先执行有前置通知注解标注的方法 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

最后,测试AOP前置通知:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        //执行结果:接口方法:add接收参数,x=20,y=10
    }
}

这样,我们前置通知就可以了,但是怎么实现方法调用前输出日志,调用后也输出日志呢?我们还需要添加一个后置通知

// 声明为一个切面
@Aspect
// 交给IOC容器管理
@Component
public class CalculatorAspectLogging {

    /**
     * 让这个方法知道该方法在哪些类的哪些方法开始之前执行
     * 声明该方法是一个前置通知
     * 表达式为:public 返回值 包名 接口名 参数类型,参数只需要类型
     * 参数:JoinPoint为连接点,可以获取参数
     */
    @Before("execution(public int com.spring.service.Calculator.add(int, int))")
    public void beforeMethod(JoinPoint joinPoint) {
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        //获取方法参数
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(String.format("接口方法:%s接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1)));
    }
    
    /**
     * 声明该方法是后置通知
     * @param joinPoint
     */
    @After("execution(public int com.spring.service.Calculator.add(int, int))")
    public void afterMethod(JoinPoint joinPoint) {
        int res = 0;
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        //获取方法参数
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        if("add".equals(methodName)) {
            res = Integer.parseInt(String.valueOf(args.get(0))) + Integer.parseInt(String.valueOf(args.get(1)));
        }else {
            res = Integer.parseInt(String.valueOf(args.get(0))) - Integer.parseInt(String.valueOf(args.get(1)));
        }
        System.out.println(String.format("接口方法:%s执行结果,z=%s", methodName,res));
    }
}

再次运行测试方法:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        /**
         * 执行结果:
         * 接口方法:add接收参数,x=20,y=10
         * 接口方法:add执行结果,z=30
         */
    }
}

这样,就大功告成 ! 但是我们不是要在所有方法调用之前和调用之后都织入通知么?只需要修改通知表达式,我们将表达式修改为:

@Before("execution(public int com.spring.service.Calculator.*(int, int))")

@After("execution(public int com.spring.service.Calculator.*(int, int))")
*表示在com.spring.service.Calculator包下的所有类都匹配的意思

再次执行测试程序:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);//记得该方法要添加调用
        /**
         * 执行结果:
         * 接口方法:add接收参数,x=20,y=10
         * 接口方法:add执行结果,z=30
         * 接口方法:sub接收参数,x=20,y=10
         * 接口方法:sub执行结果,z=10
         */
    }
}

3.AOP表达式的语法规则

方法签名 AspectJ 切入点表达式:
•最典型的切入点表达式时根据方法的签名来匹配各种方法:
–execution * com.spring.Calculator.*(..): 匹配 Calculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值,第二个 * 代表任意方法, 参数括号内的 .. 匹配任意数量的参数, 若目标类与接口与该切面在同一个包中, 可以省略包名.
–execution * com.spring.*.*(..):跟上方不同的是这里匹配的是spring包下的所有类以及所有方法
–execution public * Calculator.*(..): 匹配 Calculator 接口的所有公有方法.
–execution public double Calculator.*(..): 匹配 Calculator 中返回 double 类型数值的方法
–execution public double Calculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
–execution public double Calculator.*(double, double): 匹配参数类型为 double, double 类型的方法.

另外,还有比较复杂的合并表达式,在 AspectJ 切入点表达式可以通过操作符 &&, ||, ! 结合起来:

@Pointcut("execution(* *.add*(int, ..)) || execution(* *.sub(int, ..))")

4.通知类型

通过前面的案例,我们大概了解了下前置通知和后置通知,其实AspectJ为我们提供了5种类型的通知注解,分别是:前置通知、后置通知、返回通知、异常通知、环绕通知。

①.后置通知

是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候;

②.返回通知

无论连接点是正常返回还是抛出异常后置通知都会执行如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知

返回通知是在方法正常结束受执行的代码

返回通知是可以访问到方法返回值的

③.异常通知

只在连接点抛出异常时才执行异常通知

将 throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常,Throwable 是所有错误和异常类的超类,所以在异常通知方法可以捕获到任何错误和异常

如果只对某种特殊的异常类型进行处理, 可以将参数声明为其他异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行
注意:可以访问到异常对象,且可以指定在出现特定异常时再执行通知

④.环绕通知

环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点,甚至可以控制是否执行连接点

对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint,它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点

在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法,如果忘记这样做就会导致通知被执行了,但目标方法没有被执行

注意:

  • 环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed(); 的返回值,否则会出现空指针异常
  • 环绕通知类似于动态代理的全过程
  • ProceedingJoinPoint类型的参数可以决定是否执行目标方法,且环绕通知必须有返回值,返回值即为目标方法的返回值。

概念性的东西,看了这些都不太明白,通过下面的说明就能清楚了解上方5个通知分别的作用了 !

我们都知道Spring的事务机制(尚未整理,后面几个章节详细说明)是基于AOP实现的,在JDBC中,每次我们操作业务,都使用try...catch包围起来,当业务方法抛出异常,可以及时捕获异常并且手动回滚,所以我们大概能知道使用AOP实现事务的机制大概为:

try {
  System.out.println("这是前置通知");
  //执行真正的业务方法
  System.out.println("这是返回通知");
} catch (Exception e) {
  //抛出异常捕获并且回滚
  System.out.println("这是异常通知");
}
System.out.println("这是后置通知");

现在,不同的通知类型功能就清晰许多了。

5.通知类型案例

说明:前置通知和后置通知前面已经涉及,不再给出案例,以下直接给出切面和测试结果代码,接口等以上方为例 !

①.返回通知

/**
     * 返回通知
     * 在方法正常结束后执行的通知
     * 可以访问到方法的返回值
     * @param joinPoint
     */
    @AfterReturning(value = "execution(public int com.spring.service.Calculator.*(int, int))",returning="res")
    public void afterReturnningMethod(JoinPoint joinPoint,Object res) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println(String.format("接口方法:%s返回通知返回结果:%s", methodName,res));
    }

测试返回通知:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        /**
         * 执行结果:
         * 接口方法:add接收参数,x=20,y=10 // 前置通知
         * 接口方法:add执行结果,z=30 // 后置通知
         * 接口方法:add返回通知返回结果:30 // 返回通知
         * 接口方法:sub接收参数,x=20,y=10 // 前置通知
         * 接口方法:sub执行结果,z=10 // 后置通知
         * 接口方法:sub返回通知返回结果:10 // 返回通知
         */
    }
}

②.异常通知

首先,新增一个除法方法

public interface Calculator {

    int add(int x,int y);
    
    int sub(int x,int y);
    
    int div(int x,int y);
}

其次,实现这个接口方法

@Service
public class CalculatorImpl implements Calculator{

    @Override
    public int add(int x, int y) {
        //System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y));
        int z = x + y;
        //System.out.println(String.format("接口执行结果,z=%s", z));
        return z;
    }

    @Override
    public int sub(int x, int y) {
        //System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y));
        int z = x - y;
        //System.out.println(String.format("接口执行结果,z=%s", z));
        return z;
    }

    @Override
    public int div(int x, int y) {
        int z = x / y;
        return z;
    }

}

再者,添加异常通知

/**
     * 在目标方法出现异常时会执行的通知
     * 可以访问到异常对象且可以指定在出现特定异常时在执行通知
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "execution(public int com.spring.service.Calculator.*(int, int))",throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint,Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println(String.format("接口方法:%s返回通知返回结果:%s", methodName,e));
    }

最后,测试异常通知

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 0);
        /**
         *执行结果:
         *接口方法:add接收参数,x=20,y=10
         *接口方法:add执行结果,z=30
         *接口方法:add返回通知返回结果:30
         *接口方法:sub接收参数,x=20,y=10
         *接口方法:sub执行结果,z=10
         *接口方法:sub返回通知返回结果:10
         *接口方法:div接收参数,x=10,y=0
         *接口方法:div执行结果,z=10
         *接口方法:div异常通知返回结果:java.lang.ArithmeticException: / by zero
         */
    }
}

注意:在我们执行calculator.div(10,0)之后,出现了异常,而返回通知是正常执行才有返回结果,div这个方法并没有返回通知的日志。

同时,我们可以指定某个异常时才会打印异常日志:

/**
     * 在目标方法出现异常时会执行的通知
     * 可以访问到异常对象且可以指定在出现特定异常时在执行通知
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "execution(public int com.spring.service.Calculator.*(int, int))",throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint,NullPointerException e) { //这里指定了空指针才会执行该通知
        String methodName = joinPoint.getSignature().getName();
        System.out.println(String.format("接口方法:%s异常通知返回结果:%s", methodName,e));
    }

再次执行测试代码

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 0);
        /**
         *执行结果:
         *接口方法:add接收参数,x=20,y=10
         *接口方法:add执行结果,z=30
         *接口方法:add返回通知返回结果:30
         *接口方法:sub接收参数,x=20,y=10
         *接口方法:sub执行结果,z=10
         *接口方法:sub返回通知返回结果:10
         *接口方法:div接收参数,x=10,y=0
         *接口方法:div执行结果,z=10
         *Exception in thread "main" java.lang.ArithmeticException: / by zero   这里不是异常通知日志,现在的异常通知只有当NullPointExcetpion才会执行
         */
    }
}

③.环绕通知

为了区别,我们先把所有的通知去掉,只测试环绕通知:

首先,添加通知

/**
     * 环绕通知必须携带ProceedingJoinPoint类型的参数
     * 环绕通知相当于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
     * 环绕通知必须有返回值,返回值是被代理方法即目标方法的返回值
     * @param joinPoint
     */
    @Around("execution(public int com.spring.service.Calculator.*(int, int))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        System.out.println("这是环绕通知");
        return 1111;
    }

测试环绕通知

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 10);
        /**
         * 执行后输出:
         * 这是环绕通知
         * 这是环绕通知
         * 这是环绕通知
         */
    }
}

ok,在调用目标方法时环绕通知已经可以起作用了。那么难道这就是环绕通知?这只是环绕通知的简单测试,环绕通知可以整合所有的通知,如下:

// 声明为一个切面
@Aspect
// 交给IOC容器管理
@Component
public class CalculatorAspectLogging {

    /**
     * 环绕通知必须携带ProceedingJoinPoint类型的参数
     * 环绕通知相当于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
     * 环绕通知必须有返回值,返回值是被代理方法即目标方法的返回值
     * @param joinPoint
     */
    @Around("execution(public int com.spring.service.Calculator.*(int, int))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        
        Object res = null;
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        try {
            // 前置通知
            System.out.println(String.format("接口方法:%s[前置通知]接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1)));
            // 执行目标方法(即接口内的add、sub、div方法)
            res = joinPoint.proceed();
            // 返回通知
            System.out.println(String.format("接口方法:%s[返回通知]返回结果,z=%s", 
                    methodName,Integer.parseInt(String.valueOf(args.get(0)))+Integer.parseInt(String.valueOf(args.get(1)))));
            return res;
        } catch (Throwable e) {
            // 异常通知
            System.out.println(String.format("接口方法:%s[异常通知]返回异常结果:%s", methodName,e));
        }
        // 后置通知
        System.out.println(String.format("接口方法:%s[后置通知]返回结果,res=%s", methodName,res));
        return res;
    }
}

测试环绕通知

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 10);
        /**
         * 接口方法:add[前置通知]接收参数,x=20,y=10
         * 接口方法:add[返回通知]返回结果,z=30
         * 接口方法:sub[前置通知]接收参数,x=20,y=10
         * 接口方法:sub[返回通知]返回结果,z=30
         * 接口方法:div[前置通知]接收参数,x=10,y=10
         * 接口方法:div[返回通知]返回结果,z=20
         */
    }
}

这里结果没有异常通知和后置通知,所以我们把div除法方法设置为可以抛出异常:

calculator.div(10, 0);

再次执行测试方法:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 0);
        /**
         * 接口方法:add[前置通知]接收参数,x=20,y=10
         * 接口方法:add[返回通知]返回结果,z=30
         * 接口方法:sub[前置通知]接收参数,x=20,y=10
         * 接口方法:sub[返回通知]返回结果,z=30
         * 接口方法:div[前置通知]接收参数,x=10,y=0
         * 接口方法:div[异常通知]返回异常结果:java.lang.ArithmeticException: / by zero
         * 接口方法:div[后置通知]返回结果,res=null
         */
    }
}

6.切面的优先级

上面我们的Calculator接口定义了一个日志切面,假设我们又定义了一个XX切面,表达式同样配置着和日志切面一样,那么执行目标方法的时候,优先执行日志切面还是XX切面?

我们可以在切面添加@Order来解决执行目标方法有限执行的切面顺序:

@Order(1) //值越小,优先级越高
@Aspect
@Component
public class CalculatorAspectLogging {}
@order(2)
@Aspect
@Component
public class CalculatorAspectXX{}

这里就不详细举例。

7.重用切点表达式

我们先来看前置通知和后置通知的表达式:

@Before("execution(public int com.spring.service.Calculator.*(int, int))")
@After("execution(public int com.spring.service.Calculator.*(int, int))")

在上面前置通知和后置通知表达式中,我们注意到注解中的表达式都是一样的,所以我们应该能猜到表达式能重用,没错,我们可以把表达式提取出来,在一个自定义方法上面定义,然后前置通知方法和后置通知方法注解中引用这个自定义方法即可:

@Aspect
@Component
public class CalculatorAspectLogging {

    /**
     * 自定义一个方法,用于声明重用切入点表达式
     * 该方法都不再需要其他的代码
     * 添加@Pointcut注解,将以下通知的表达式提取到本处
     * 以下通知的注解内引用本方法名即可
     **/
    @Pointcut("execution(public int com.spring.service.Calculator.*(int, int))")
    public void commonExpression() {}
    
    @Before("commonExpression()")
    public void beforeMethod(JoinPoint joinPoint) {
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        //获取方法参数
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(String.format("接口方法:%s接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1)));
    }
    
    @After("commonExpression()")
    public void afterMethod(JoinPoint joinPoint) {
        int res = 0;
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        //获取方法参数
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        if("add".equals(methodName)) {
            res = Integer.parseInt(String.valueOf(args.get(0))) + Integer.parseInt(String.valueOf(args.get(1)));
        }else {
            res = Integer.parseInt(String.valueOf(args.get(0))) - Integer.parseInt(String.valueOf(args.get(1)));
        }
        System.out.println(String.format("接口方法:%s执行结果,z=%s", methodName,res));
    }
}

执行测试方法:

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calculator calculator = (Calculator) ctx.getBean(Calculator.class);
        calculator.add(20, 10);    
        calculator.sub(20, 10);
        calculator.div(10, 10);
        /**
         * 接口方法:add接收参数,x=20,y=10
         * 接口方法:add执行结果,z=30
         * 接口方法:sub接收参数,x=20,y=10
         * 接口方法:sub执行结果,z=10
         * 接口方法:div接收参数,x=10,y=10
         * 接口方法:div执行结果,z=0
         */
    }
}

效果是一样的 ! 

注意:在上面提到切面优先级时,我们说到一个切面时XX切面,如果XX切面一样引用了该日志切面的表达式怎么办(都是针对一样的方法的表达式),难道在XX切面内重新提取出一个重用表达式吗?

有两种情况:

1.两个切面在同一个包下,直接日志切面.表达式方法()即可

@Aspect
@Component
public class CalcultorAspectXX { @Before("CalculatorAspectLogging.commonExpression()") public void beforeMethod(JoinPoint joinPoint) {} @After("CalculatorAspectLogging.commonExpression()") public void afterMethod(JoinPoint joinPoint) {} }

2.两个切面在不同包下,得带上包名,即包名.切面名.表达式方法()即可

@Aspect
@Component
public class CalcultorAspectXX {
    @Before("com.spring.utils.CalculatorAspectLogging.commonExpression()")
    public void beforeMethod(JoinPoint joinPoint) {}
    
    @After("com.spring.utils.CalculatorAspectLogging.commonExpression()")
    public void afterMethod(JoinPoint joinPoint) {}
}  
原文地址:https://www.cnblogs.com/Json1208/p/8733652.html