跳转至

Spring AOP

约 2633 个字 157 行代码 1 张图片 预计阅读时间 11 分钟

AOP介绍

AOP(Aspect Oriented Programming)即面向切面编程,是Spring框架的第二大核心(第一大核心是IoC)。它是一种编程思想,旨在将横切关注点(如日志记录、权限校验、事务管理等)与业务逻辑代码分离,实现对某一类特定问题的集中处理。在程序运行期间,AOP可以在不修改源代码的基础上对已有方法进行功能增强,具有无侵入性、解耦的特点。

对于AOP的实现,常见的有:

  • Spring AOP
  • AspectJ
  • CGLIB

Spring AOP引入

引入依赖:

XML
1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建一个自定义的时间记录的切面类:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect // 使用AspectJ注解,但是具体实现是Spring AOP
@Component
public class TimeRecord {
    @Around("execution(* org.epsda.springaop.controller.*.*(..))") // 定义切点表达式
    public Object record(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        // 执行目标方法
        Object proceed = proceedingJoinPoint.proceed();
        log.info("目标方法:" + proceedingJoinPoint.getSignature() + "执行了:" +
                (System.currentTimeMillis() - start) + "ms");

        return proceed;
    }
}

Spring AOP相关概念

核心概念

  • 连接点:满足切点表达式规则、可以被AOP控制的方法
  • 切点:用于定义一组规则,告诉程序需要对哪些方法进行功能增强,一般使用AspectJ切点表达式语言来描述
  • 通知:具体要执行的重复共性功能,最终以方法形式体现,是抽取出来的公共增强逻辑
  • 切面:由切点、连接点、通知组成,既包含了需要增强的范围规则,也包含了增强逻辑的定义,一般是被@Aspect注解标识的类。通过切面可以描述需要对哪些方法,在什么时候执行什么样的增强操作

通知类型

  • @Around:环绕通知,此注解标注的通知方法会在目标方法执行前后都执行
  • @Before:前置通知,此注解标注的通知方法会在目标方法执行前执行
  • @After:后置通知,此注解标注的通知方法会在目标方法执行后执行,无论目标方法是否发生异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法会在目标方法正常返回后执行,发生异常时不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法会在目标方法发生异常后执行

不同的通知类型的执行顺序:

当程序正常执行,未发生异常时,通知的执行顺序为:

  • @Around前置逻辑 → @Before前置通知 → 目标方法执行 → @AfterReturning返回后通知 → @After后置通知 → @Around后置逻辑

当目标方法执行过程中发生异常时,通知的执行顺序为:

  • @Around前置逻辑 → @Before前置通知 → 目标方法执行抛出异常 → @AfterThrowing异常后通知 → @After后置通知→@Around后置逻辑

Note

整个执行顺序有点像栈结构,优先级越高的切面,其前置逻辑越先执行,后置逻辑越晚执行,符合"先进后出"的特点

对于环绕通知有两个需要注意的点:

  • 必须调用ProceedingJoinPoint.proceed()来触发原始目标方法执行,其他通知不需要手动触发目标方法
  • @Around修饰的目标方法如果有返回值,则对应的切面也可以有返回值,用来接收原始方法的返回值,否则调用方无法获取到目标方法的返回结果

ProceedingJoinPoint常用方法

ProceedingJoinPoint环绕通知(@Around 的专属接口,它继承了 JoinPoint,额外提供了 proceed() 方法用来"放行"执行目标方法。

proceed() —— 执行目标方法

大白话:拦住请求,做完你的前置逻辑后,调用这个方法让原方法继续执行。

Java
1
2
3
4
5
6
7
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("前置逻辑");
    Object result = pjp.proceed();  // 执行原方法
    System.out.println("后置逻辑");
    return result;
}

Warning

注意proceed() 必须调用,且通常只调用一次。如果调用了两次,原方法会执行两次(幂等性操作慎用)

proceed(Object[] args) —— 修改参数后执行

大白话:把方法的参数偷偷换掉,再用新参数去执行原方法。

Java
1
2
3
4
5
6
@Around("execution(* updateUser(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    args[0] = ((User) args[0]).setName("被AOP修改了");  // 改参数
    return pjp.proceed(args);  // 用新参数执行
}

getArgs() —— 获取方法参数

大白话:拿到调用方法时传进来的所有实参。

Java
1
2
3
4
Object[] args = pjp.getArgs();
for (Object arg : args) {
    System.out.println("参数值: " + arg);
}

getSignature() —— 获取方法签名

大白话:拿到方法的名字、返回类型、参数类型等元信息。

Java
1
2
3
4
5
MethodSignature signature = (MethodSignature) pjp.getSignature();
String methodName = signature.getName();           // 方法名
Class<?> returnType = signature.getReturnType();   // 返回类型
Class<?>[] paramTypes = signature.getParameterTypes(); // 参数类型
Method method = signature.getMethod();             // 反射的Method对象

Tip

常用于日志记录:UserService.updateUser() 这种格式。

getTarget() —— 获取被代理的原始对象

大白话:拿到真正干活的那个对象(不是Spring生成的代理对象)。

Java
1
2
Object target = pjp.getTarget();
System.out.println("目标类: " + target.getClass().getName());

getThis() —— 获取代理对象本身

大白话:拿到Spring给你生成的代理对象(通常是CGLIB或JDK动态代理的实例)。

Java
1
2
3
Object proxy = pjp.getThis();
System.out.println("代理类: " + proxy.getClass().getName()); 
// 输出可能是: com.example.service.UserService$$EnhancerBySpringCGLIB$$...

getKind() —— 获取连接点类型

大白话:告诉你当前拦截的是什么类型的连接点(方法执行、字段访问等)。

Java
1
String kind = pjp.getKind();  // 通常返回 "method-execution"

getStaticPart() —— 获取静态连接点信息

大白话:获取不包含运行时参数的方法签名信息。

Java
1
Signature staticPart = pjp.getStaticPart();

切点规则

Spring提供了专门的@Pointcut注解,用来定义和提取公共的切点表达式,避免在多个通知中重复编写相同的匹配规则。

如果不同类之间的切点调用,则需要带上切点定义方法的全限定类名,格式为:全限定类名.方法名()。但是被调用的切点定义方法,必须将访问修饰符从private修改为public,否则其他类无法正常访问该切点

相同切面的优先级

当多个切面同时匹配同一个目标方法时,默认会按照切面类的类名字母排序,字母排名靠前的切面优先级更高。可以通过@Order注解来指定切面的优先级,数值越小优先级越高。

切点表达式

常见的切点表达式有两种写法,分别用来应对不同的匹配场景:

  1. execution():根据方法的签名信息匹配目标方法,是最常用的切点表达式
  2. @annotation():根据方法上的自定义注解匹配目标方法,适合匹配无规则的指定方法

方法的签名信息匹配

execution表达式的基础语法格式为:

Java
1
execution(<访问修饰符> <返回类型> <包名.类名>.<方法名>(<方法参数> <异常>)

其中方法参数和异常可以省略,表达式支持两个通配符来简化匹配规则:

  • *:匹配任意字符,只匹配一个元素(可以是返回类型、包名、类名、方法名或单个方法参数)
  • ..:匹配多个连续的任意符号,可以通配任意层级的包,或任意个数、任意类型的方法参数

访问修饰符如果省略,则不匹配private方法,但是会匹配publicprotected以及默认包访问权限的方法

以下是常见的execution切点表达式示例:

TestController 下的 public修饰,返回类型为String 方法名为t1,无参方法:

Java
1
execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符:

Java
1
execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型:

Java
1
execution(* com.example.demo.controller.TestController.t1())

匹配TestController下的所有无参方法:

Java
1
execution(* com.example.demo.controller.TestController.*())

匹配TestController下的所有方法:

Java
1
execution(* com.example.demo.controller.TestController.*(..))

匹配controller包下所有的类的所有方法:

Java
1
execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的TestController

Java
1
execution(* com..TestController.*(..))

匹配com.example.demo包下,子孙包下的所有类的所有方法:

Java
1
execution(* com.example.demo..*(..))

自定义注解匹配

如果需要对多个无规则的指定方法进行增强,使用execution表达式会比较繁琐,此时可以使用@annotation()切点表达式,通过匹配方法上的自定义注解来定位需要增强的方法

使用方式如下:

  1. 编写自定义注解
  2. 使用 @annotation 表达式(注解全权限定名称)定义切点
  3. 在需要增强的目标方法上添加自定义注解

Spring AOP实现原理

代理模式

代理模式,也叫委托模式。它的定义是:为其他对象提供一种代理以控制对这个对象的访问。通过代理类,我们在调用目标方法时不再直接调用,而是通过代理对象间接调用,从而可以在调用前后添加额外的逻辑

代理根据创建时期分为两种:

  1. 静态代理:在程序运行前,代理类的 .class 文件就已经存在了,由程序员创建代理类或特定工具自动生成
  2. 动态代理:在程序运行时,运用反射机制动态创建代理对象

静态代理

以房屋租赁为例,定义一个租房接口和房东实现类,然后创建中介代理类,在代理类中调用房东的租房方法前后添加额外的服务逻辑。例如下面的代码:

房东和中介都要进行一个共同的操作(出租和售卖):

Java
1
2
3
4
5
6
7
public interface HouseOperation {
    /**
     * 房子的具体行为,需要房东和中介共同实现
     */
    void rent();
    void sale();
}

房东实现自己的出租和售卖操作,只有简单的行为:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class HouseOwnerSubject implements HouseOperation {
    /**
     * 某一个房东,要将房子通过中介这个代理商把房子卖出去或者租出去
     */
    @Override
    public void rent() {
        System.out.println("我的房子出租了");
    }

    @Override
    public void sale() {
        System.out.println("我的房子售卖了");
    }
}

中介需要在售卖或者出租之前进行一些额外的操作,然后再售卖或者出租:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HouseProxySubject implements HouseOperation{
    /**
     * 中介要帮房东出租房子
     * 在房子的售卖和出租流程的前后进行补充操作
     */
    private HouseOwnerSubject houseOwnerSubject;

    public HouseProxySubject(HouseOwnerSubject houseOwnerSubject) {
        this.houseOwnerSubject = houseOwnerSubject;
    }

    @Override
    public void rent() {
        System.out.println("中介帮忙出租");
        houseOwnerSubject.rent();
    }

    @Override
    public void sale() {
        System.out.println("中介帮忙售卖");
        houseOwnerSubject.sale();
    }
}

中介自己没房子,需要房东先提供给中介房源。当有租客或者买家来看房时,租客找到中介进行后续步骤:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
    public static void main(String[] args) {
        /**
         * 来了一个房东,想要中介帮他出租房
         */
        HouseOwnerSubject houseOwnerSubject = new HouseOwnerSubject();
        HouseProxySubject houseProxySubject = new HouseProxySubject(houseOwnerSubject);

        /**
         * 来了一个租客,想要租房
         * 中介帮房东出租
         */
        houseProxySubject.rent();

        /**
         * 来了一个买家,想要买房
         * 中介帮房东售卖
         */
        houseProxySubject.sale();
    }
}

这种方式虽然实现了功能增强,但每个被代理对象(例如HouseOwnerSubject)都需要单独编写一个代理类(例如HouseProxySubject),当需要代理的类较多时,会导致代码冗余和维护困难。

动态代理

相比于静态代理,动态代理不需要为每个目标对象单独编写代理类,而是在程序运行时动态创建代理对象。Java提供了两种常见的动态代理实现方式:JDK动态代理和CGLIB动态代理。下面以JDK动态代理演示:

先创建一个JDK代理器:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JDKInvocationHandler implements InvocationHandler {

    private HouseOwnerSubject houseOwnerSubject;

    public JDKInvocationHandler(HouseOwnerSubject houseOwnerSubject) {
        this.houseOwnerSubject = houseOwnerSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("中介开始售卖或者出租房子");
        // 执行被代理对象的方法
        return method.invoke(houseOwnerSubject, args);
    }
}

然后让JDK帮忙生成代理对象:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        /**
         * 使用JDK动态代理,不手动创建中介
         */
        HouseOperation operation = (HouseOperation) Proxy.newProxyInstance(
                HouseOwnerSubject.class.getClassLoader(),
                new Class[]{HouseOperation.class},
                new JDKInvocationHandler(houseOwnerSubject));
        operation.rent();
        operation.sale();
    }
}

除了使用JDK进行动态代理,还可以使用CGLIB进行动态代理,二者最大的区别就是JDK动态代理不能代理没有实现接口的类,而CGLIB则没有这种限制

Spring AOP的实现原理

Spring AOP 是基于动态代理实现的。Spring 在运行时通过 ProxyFactory 创建代理对象,DefaultAopProxyFactory 中的 createAopProxy 方法会根据目标对象的类型决定使用哪种代理方式:如果目标类实现了接口,则使用 JDK 动态代理;否则使用 CGLIB 动态代理。此外,还可以通过 proxyTargetClass 属性强制使用 CGLIB 代理,Spring Boot 2.X 开始默认使用 CGLIB 代理。

对于proxyTargetClass,有下面几种情况:

  • proxyTargetClassfalse 且目标类实现了接口时,使用 JDK 动态代理
  • proxyTargetClassfalse 且目标类未实现接口时,使用 CGLIB 动态代理
  • proxyTargetClasstrue 时,无论目标类是否实现接口,都使用 CGLIB 动态代理。

常见面试题

  1. 如何理解Spring AOP
  2. AOP的实现方式有哪些
    1. 代理
    2. 注解
    3. 配置
  3. AOP原理(动态代理)