本文介绍 Spring 中 AOP 的原理及使用方式。

引入

在介绍 AOP 之前,假设我们有一个需求:在实现加减乘除功能的同时,满足:

  • 在程序执行期间追踪正在发生的活动;
  • 希望计算器只能处理正数运算。

有如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface ArithmeticCalculator {

    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    public static final Logger LOGGER = LoggerFactory.getLogger(ArithmeticCalculator.class);

    @Override
    public int add(int i, int j) {
        LOGGER.info("The method add begins with [{}, {}]", i, j);
        int result = i + j;
        LOGGER.info("The method add ends with {}", result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        LOGGER.info("The method sub begins with [{}, {}]", i, j);
        int result = i - j;
        LOGGER.info("The method sub ends with {}", result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        LOGGER.info("The method mul begins with [{}, {}]", i, j);
        int result = i * j;
        LOGGER.info("The method mul ends with {}", result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        LOGGER.info("The method div begins with [{}, {}]", i, j);
        int result = i / j;
        LOGGER.info("The method div ends with {}", result);
        return result;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args) {
        ArithmeticCalculator arithmeticCalculator = new ArithmeticCalculatorImpl();
        int result = arithmeticCalculator.add(1, 2);
        System.out.println(result);

        result = arithmeticCalculator.sub(4, 2);
        System.out.println(result);
    }
}

输出结果如下:

1
2
3
4
5
6
23:01:49.551 [main] INFO com.example.springdemo.aop.helloworld.ArithmeticCalculator - The method add begins with [1, 2]
23:01:49.553 [main] INFO com.example.springdemo.aop.helloworld.ArithmeticCalculator - The method add ends with 3
3
23:01:49.553 [main] INFO com.example.springdemo.aop.helloworld.ArithmeticCalculator - The method sub begins with [4, 2]
23:01:49.553 [main] INFO com.example.springdemo.aop.helloworld.ArithmeticCalculator - The method sub ends with 2
2

可以看到,在上面的实现类中,「打印日志」的代码是十分相似的,并且当「打印日志」的代码与具体的「业务代码」放到一起时,「业务代码」的功能就显得不那么清晰了。不仅冗余的日志代码很多,而且上面的方法不是一种很好的实现方式。即:

  • 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
  • 代码分散:以日志需求为例,只是为了满足这个单一的需求,就不得不在多个模块(方法)中多次重复写相同的日志代码。如果日志需求发生变化,必须修改所有模块。

针对以上问题,可以使用基于动态代理的方式,完成 AOP 的功能。代理设计模式的原理是:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都需要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

使用动态代理实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    public static final Logger LOGGER = LoggerFactory.getLogger(ArithmeticCalculator.class);

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

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

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

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ArithmeticCalculatorLoggingProxy {

    // 要代理的对象
    private ArithmeticCalculator target;

    public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target) {
        this.target = target;
    }

    public ArithmeticCalculator getLoggingProxy() {
        ArithmeticCalculator proxy = null;

        // 代理对象与普通对象不同,普通对象直接 new 出来即可,此时 JVM 有默认的类加载器,
        // 而现在的对象是自己代理出来的,需要确定代理对象由哪个类加载器进行加载
        ClassLoader loader = target.getClass().getClassLoader();
        // 代理对象的类型,即其中有哪些方法
        Class[] interfaces = new Class[]{ArithmeticCalculator.class};
        // 当调用代理对象的方法时,所执行的代码就在 InvocationHandler 中
        InvocationHandler handler = new InvocationHandler() {
            /**
             * @param proxy 正在返回的那个代理对象,一般情况下在 invoke 方法中都不使用该对象
             * @param method 正在被调用的方法
             * @param args 调用方法时传入的参数
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                // 日志
                System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
                // 执行方法
                Object result = method.invoke(target, args);
                // 日志
                System.out.println("The method " + methodName + " ends with " + result);
                return result;
            }
        };
        proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, handler);

        return proxy;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        ArithmeticCalculator target = new ArithmeticCalculatorImpl();

        ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();

        int result = proxy.add(1, 2);
        System.out.println(result);

        proxy.sub(4, 2);
        System.out.println(result);
    }
}

输出结果如下所示:

1
2
3
4
5
6
The method add begins with [1, 2]
The method add ends with 3
3
The method sub begins with [4, 2]
The method sub ends with 2
3

从结果中可以看到,「业务代码」是简洁的,我们使用动态代理的方式,实现了打印日志的功能。但是这种方式在实际开发中较为繁琐,需要写较多代码。而我们用得最多的就是通过 AOP 的方式进行实现。

AOP

AOP(Aspect-Oriented Programming, 面向切面编程)是一种新的方法论,是对传统 OOP(Object-Oriented Programming, 面向对象编程)的补充。

AOP 的主要编程对象是切面,而切面模块化横切关注点

在使用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不需要修改受影响的类。这样的话,横切关注点就被模块化到特殊的对象(切面)里了。其好处在于:

  • 每个事物的逻辑位于一个位置,代码不分散,便于维护和升级。
  • 业务模块更简洁,只包含核心业务代码。

与 AOP 相关的的术语如下:

  • 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice):切面必须要完成的工作
  • 目标(Target):被通知的对象
  • 代理(Proxy):向目标对象应用通知之后所创建的对象
  • 连接点(JoinPoint):程序执行的某个特定位置。例如,类中某个方法调用前、调用后、方法抛出异常后等。
    • 连接点由两个信息确定:方法表示的程序执行点和相对点表示的方位。例如,ArithmeticCalculator#add()方法执行前的连接点,执行点为 ArithmeticCalculator#add();方位为该方法执行前的位置。
  • 切点(PointCut):每个类都有多个连接点。例如,ArithmeticCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。
    • 例如,连接点相当于数据库中的记录,切点相当于查询条件。
    • 切点和连接点不是一对一的关系,一个切点匹配多个连接点。切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface ArithmeticCalculator {

    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

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

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

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

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * 把该类声明为切面
 * 1. 需要把该类放入到 IoC 容器中;
 * 2. 然后添加一个切面注解
 *
 * @date 2021/07/28
 */
@Aspect
@Component
public class LoggingAspect {

    // 前置通知:在目标方法开始之前执行
    @Before("execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.add(int, int))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("The method " + methodName + "begins with " + args);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.example.springdemo.aop.impl"/>

    <!-- 使 AspectJ 注解起作用:自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy/>
</beans>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        // 创建 Spring 的 IoC 容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContextAop.xml");
        // 从容器中获取 bean 的实例
        ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator");
        // 使用 bean
        int result = arithmeticCalculator.add(1, 2);
        System.out.println("add result: " + result);

        result = arithmeticCalculator.div(9 , 3);
        System.out.println("div result: " + result);
    }
}

输出结果如下:

1
2
3
The method addbegins with [1, 2]
add result: 3
div result: 3

如果想要让 ArithmeticCalculator 接口中的所有方法都设置前置通知的话,则可以将@Before注解中的add方法用通配符*替换,如下所示:

1
@Before("execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))")

输出结果如下所示:

1
2
3
4
The method addbegins with [1, 2]
add result: 3
The method divbegins with [9, 3]
div result: 3

其中,AspectJ 支持 5 中类型的通知注解:

  • @Before:前置通知,在目标方法执行前执行。
  • @After:后置通知,在目标方法执行后(无论是否发生异常)执行。
    • 在后置通知中,还不能访问目标方法执行的结果。
  • @AfterReturning:返回通知,在目标方法返回结果之后执行。
  • @AfterThrowing:异常通知,在目标方法抛出异常之后执行。
  • @Around:环绕通知,围绕着目标方法执行。

针对切入点表达式,如果将其修改为@Before("execution(* ArithmeticCalculator.add(..))"),则其中*代表匹配任意修饰符及任意返回值,参数列表中的..表示匹配任意数量的参数。也就是说,切入点表达式是根据方法的签名来匹配各种方法的。

此外,可以在通知方法中声明一个类型为 JoinPoint 的参数,然后就可以访问方法的名称或者参数值了。

下面将剩余的其他通知类型进行说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Aspect
@Component
public class LoggingAspect {

    // 前置通知:在目标方法开始之前执行
    @Before("execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("beforeMethod: The method " + methodName + " begins with " + args);
    }

    // 在后置通知中,还不能访问目标方法执行的结果
    @After("execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("afterMethod: The method " + methodName + " ends with " + args);
    }

    // 返回通知:在目标方法返回结果之后执行
    // 返回通知是可以访问到方法的返回值的
    @AfterReturning(value = "execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterReturningMethod: The method " + methodName + " ends with " + result);
    }


    // 异常通知
    @AfterThrowing(value = "execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))", throwing = "exception")
    public void afterThrowingMethod(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterThrowingMethod: The method " + methodName + " occurs exception: " + exception);
    }

    // 环绕通知需要携带 ProceedingJoinPoint 类型的参数
    // 环绕通知类似于动态代理的全过程:ProceedingJoinPoint 类型的参数可以决定是否执行目标方法
    // 且环绕通知必须有返回值,返回值即为目标方法的返回值
    @Around(value = "execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {

        Object result = null;
        String methodName = proceedingJoinPoint.getSignature().getName();

        // 执行目标方法
        try {
            // 前置通知
            System.out.println("The method " + methodName + " begins with" + Arrays.asList(proceedingJoinPoint.getArgs()));
            result = proceedingJoinPoint.proceed();
            // 返回通知
            System.out.println("The method " + methodName + " ends with result: " + result);
        } catch (Throwable throwable) {
            // 异常通知
            System.out.println("The method " + methodName + " occurs exception: " + throwable);
            throw new RuntimeException(throwable);
        }
        // 后置通知
        System.out.println("The method " + methodName + " ends");

        return result;
    }
}

对于环绕通知,虽然它的功能是最强的,即类似于动态代理的方式,但是并不代表是经常使用的。

切面的优先级

可以在切面类上添加一个@Order()注解,值越小,则优先级越高。

此外,还可以复用切入点表达式,直接新建一个方法,然后使用@Pointcut()注解即可,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Order(1)
@Aspect
@Component
public class LoggingAspect {

    // 定义一个方法,用于声明切入点表达式
    // 一般情况下,该方法中不需要再加入其他代码
    @Pointcut("execution(public int com.example.springdemo.aop.impl.ArithmeticCalculator.*(int, int))")
    public void declareJoinPointExpression() {

    }

    // 前置通知:在目标方法开始之前执行
    @Before("declareJoinPointExpression()")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("beforeMethod: The method " + methodName + " begins with " + args);
    }

    // 在后置通知中,还不能访问目标方法执行的结果
    @After("declareJoinPointExpression()")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("afterMethod: The method " + methodName + " ends with " + args);
    }

    // 返回通知:在目标方法返回结果之后执行
    // 返回通知是可以访问到方法的返回值的
    @AfterReturning(value = "declareJoinPointExpression()", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterReturningMethod: The method " + methodName + " ends with " + result);
    }


    // 异常通知
    @AfterThrowing(value = "declareJoinPointExpression()", throwing = "exception")
    public void afterThrowingMethod(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterThrowingMethod: The method " + methodName + " occurs exception: " + exception);
    }

    // 环绕通知需要携带 ProceedingJoinPoint 类型的参数
    // 环绕通知类似于动态代理的全过程:ProceedingJoinPoint 类型的参数可以决定是否执行目标方法
    // 且环绕通知必须有返回值,返回值即为目标方法的返回值
    @Around("declareJoinPointExpression()")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {

        Object result = null;
        String methodName = proceedingJoinPoint.getSignature().getName();

        // 执行目标方法
        try {
            // 前置通知
            System.out.println("The method " + methodName + " begins with" + Arrays.asList(proceedingJoinPoint.getArgs()));
            result = proceedingJoinPoint.proceed();
            // 返回通知
            System.out.println("The method " + methodName + " ends with result: " + result);
        } catch (Throwable throwable) {
            // 异常通知
            System.out.println("The method " + methodName + " occurs exception: " + throwable);
            throw new RuntimeException(throwable);
        }
        // 后置通知
        System.out.println("The method " + methodName + " ends");

        return result;
    }
}