likes
comments
collection
share

Spring AOP:从实践到原理

作者站长头像
站长
· 阅读数 14

AOP简介:Spring AOP,AspectJ

AOP(Aspect-Oriented Programming,面向切面编程)是 Spring Boot 中的一个重要概念, AOP 通过将横切关注点(如日志、安全性和事务管理等)从业务逻辑中分离出来,使得代码更加模块化和易于维护。

Spring AOP和AspectJ都是Java中的AOP框架。AspectJ是一个独立的AOP框架,Spring AOP是Spring框架的一部分,Spring使用AspectJ提供的库,用于切入点解析和匹配,但是并不依赖AspectJ的编译器或织入.简单来说,在Spring框架中,使用AspectJ注解来定义切面,使用Spring AOP来织入切面。

Spring AOP支持切点表达式(pointcut expression),支持五种通知类型(Before、After、AfterReturning、AfterThrowing和Around)。

AOP CASE

要在 Spring Boot 项目中使用 AOP,你需要执行以下步骤:

  1. 添加依赖:在 build.gradle 文件中添加 Spring Boot AOP 的依赖:

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-aop'
    }
    
  2. 创建切面:新建一个类,使用 @Aspect 注解标注这个类是一个切面。在这个类中,你可以定义通知方法,并使用相应的注解(如 @Before@After 等)标注这些方法。

  3. 定义切点:使用 @Pointcut 注解定义一个切点表达式,指定通知应用的位置。

  4. 配置 AOP:在 Spring Boot 的配置类中,使用 @EnableAspectJAutoProxy 注解启用 AOP 功能。

下面是一个简单的例子,展示了如何在 Spring Boot 项目中使用 AOP:

// 定义切面
@Aspect
@Component
public class LoggingAspect {
    // 定义切点
    @Pointcut("execution(* com.example.myapp.service.*.*(..))")
    public void serviceMethods() {}

    // 定义前置通知
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before: " + joinPoint.getSignature());
    }

    // 定义后置通知
    @After("serviceMethods()")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After: " + joinPoint.getSignature());
    }
}

以上代码中,我们定义了一个名为 LoggingAspect 的切面,其中包含两个通知方法:logBeforelogAfter。这两个通知分别在 com.example.myapp.service 包中的所有方法执行前后进行日志记录。切点表达式 execution(* com.example.myapp.service.*.*(..)) 表示匹配此包下的所有方法。

AOP核心概念

  1. 切面(Aspect):将横切关注点模块化的类。一个切面可以包含多个通知(Advice)和切点(Pointcut)。
  2. 通知(Advice):切面中的方法,用于在特定的连接点(Joinpoint)执行操作。通知有以下五种类型:
    • 前置通知(Before):在连接点之前执行的方法。
    • 后置通知(After):在连接点之后执行的方法,无论连接点执行是否成功。
    • 返回通知(AfterReturning):在连接点成功执行之后执行的方法。
    • 异常通知(AfterThrowing):在连接点抛出异常时执行的方法。
    • 环绕通知(Around):在连接点之前和之后执行的方法,可以控制连接点的执行。
  3. 切点(Pointcut):用于定义通知应该在何处应用的表达式。切点表达式可以指定方法、类或包等。
  4. 连接点(Joinpoint):程序执行过程中的某个特定点,如方法调用或异常抛出。通知会应用于这些连接点。
  5. 织入(Weaving):将切面与目标对象(Target Object)结合的过程。织入可以在编译期、类加载期或运行期完成。

通知(Advice)

  • 前置通知(Before)
  • 后置通知(AfterReturning)
  • 异常通知(AfterThrowing)
  • 最终通知(After)
  • 环绕通知(Around) 环绕通知是一个取代原有目标对象方法的通知,也提供了回调原有目标对象的方法

执行顺序

try {
    // @Before 执行前通知
    // 执行目标方法
    // @Around
} finally {
    // @After 执行后置通知
    // @AfterReturning 执行返回后通知
} catch(e) {
    // @AfterThrowing 抛出异常通知
}

  • 环绕通知是取代了原有目标的方法,记得把返回值return,参见下方示例
  • 如果是抛出了异常 并不会执行 AfterReturing

织入时机

  1. 编译时织入:在代码编译时,把切面代码融合进来,生成完整功能的Java字节码,这就需要特殊的Java编译器了,AspectJ属于这一类。

  2. 类加载时织入:在Java字节码加载时,把切面的字节码融合进来,这就需要特殊的类加载器,AspectJ和AspectWerkz实现了类加载时织入

  3. 运行时织入:在运行时,通过动态代理的方式,调用切面代码增强业务功能,Spring采用的正是这种方式。动态代理会有性能上的开销,好处是不需要特殊的编译器和类加载器。

切点表达式

execution

表达式模式

execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)

表达式解释:

  • modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符
  • ret-type:匹配返回类型,使用 * 匹配任意类型
  • declaring-type:匹配目标类,省略时匹配任意类型
    • .. 匹配包及其子包的所有类
  • name-pattern:匹配方法名称,使用 * 表示通配符
    • 匹配任意方法
    • set* 匹配名称以 set 开头的方法
  • param-pattern:匹配参数类型和数量
    • () 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
  • throws-pattern:匹配抛出异常类型,省略时匹配任意类型
execution(* com.example.myapp.service..*(..))

这个切点表达式表示匹配com.example.myapp.service包及其子包中所有类的所有方法。其中,*表示匹配任意返回类型和方法名,..表示匹配任意子包,(..)表示匹配任意参数列表。

execution(* com.example.myapp.service.*.*(..))

这个切点表达式表示匹配com.example.myapp.service包中所有类的所有方法。其中,*表示匹配任意返回类型和方法名,(..)表示匹配任意参数列表。

注意,这个表达式不会包含service的子包

@annotation,within

@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) &&" +
"within(com.example.controller..*)")

这个切点表达式表示匹配带有@RequestMapping注解的所有方法,并且这些方法所在的类位于com.example.controller包及其子包中

ProceedingJoinPoint的使用

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AddDataCheck {
    String module();
}
package com.example.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {
    private static final Logger logger = LoggerFactory.getLogger(MyAspect.class);

    @Pointcut("@annotation(com.example.service.AddDataCheck")
    public void serviceMethods() {}

    @Around("serviceMethods()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取切点签名
        Signature sig = joinPoint.getSignature();
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }

        // 获取注解上的参数
        MethodSignature msig = (MethodSignature) sig;
        Object target = joinPoint.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        AddDataCheck check = currentMethod.getAnnotation(AddDataCheck.class);
        String modelName = StringUtils.isBlank(check.module()) ? currentMethod.getName() : check.module();

        // 获取方法参数
        Object[] params = joinPoint.getArgs();

        // 调用目标方法
        Object result = null;
        if (Objects.isNull(params) || params.length == 0) {
            result =  joinPoint.proceed();
        } else {
            result =  joinPoint.proceed(params);
        }
        return res;
    }
}


AOP原理:动态代理

Spring AOP基于动态代理实现,动态代理有两种方式:JDK Proxy 和 Cglib

Spring AOP:从实践到原理

JDK动态代理

JDK 动态代理是 Java 提供的一种原生代理机制。它主要利用 Java 的反射机制和接口来实现动态代理。以下是 JDK 动态代理的核心原理:

  1. 接口与实现:JDK 动态代理要求目标类必须实现至少一个接口。代理类也会实现这些接口,这样可以确保代理类和目标类具有相同的方法签名。

  2. InvocationHandler:在 JDK 动态代理中,需要实现 java.lang.reflect.InvocationHandler 接口。InvocationHandler 是一个处理器接口,代理类会把具体的方法调用转发给它。它包含一个方法 Object invoke(Object proxy, Method method, Object[] args),其中:

    • proxy 是代理对象;
    • method 是目标类中需要被调用的方法;
    • args 是调用目标方法所需的参数。 你需要在 invoke 方法中编写代理逻辑,例如在目标方法执行前后添加日志、事务处理等。
  3. Proxy 类:JDK 提供了一个 java.lang.reflect.Proxy 类,它包含一个重要的静态方法 newProxyInstance,用于创建代理对象:

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
    
    其中:
    loader 是目标类的类加载器;
    interfaces 是目标类实现的接口列表;
    h 是一个实现了 `InvocationHandler` 接口的处理器对象。
    
    newProxyInstance 方法会动态地生成一个代理类,该类实现了目标类的接口,并将方法调用转发给 `InvocationHandler。最后,它返回一个代理对象,你可以像使用目标对象一样使用这个代理对象。
    

以下是一个简单的 JDK 动态代理示例:

假设有一个接口和实现类:

public interface Hello {
    void sayHello();
}

public class HelloImpl implements Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello, JDK Dynamic Proxy!");
    }
}

创建一个 InvocationHandler 实现类:

public class HelloInvocationHandler implements InvocationHandler {
    private final Object target;

    public HelloInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method call");
        Object result = method.invoke(target, args);
        System.out.println("After method call");
        return result;
    }
}

使用 Proxy.newProxyInstance 创建代理对象并调用方法:

public class Main {
    public static void main(String[] args) {
        Hello hello = new HelloImpl();
        InvocationHandler handler = new HelloInvocationHandler(hello);
        Hello proxy = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, handler);
        proxy.sayHello();
    }
}

输出:

Before method call
Hello, JDK Dynamic Proxy!
After method call

从示例中可以看到,在调用 sayHello 方法时,代理对象将方法调用转发给 InvocationHandler,在目标方法执行前后添加了日志。这就是 JDK 动态代理的基本原理。

CGLIB动态代理

CGLIB(Code Generation Library)是一个第三方代码生成类库,CGLIB 动态代理通过在运行时在内存中动态生成一个子类对象从而实现对被代理对象功能的扩展,底层依靠ASM(开源的java字节码编辑类库)操作字节码实现的。

由于CGLIB是通过继承来实现动态代理,因此被代理类不能是final,同时目标类的方法不能是final,否则代理类就会直接调用目标类的方法。

1、生成代理类的二进制字节码文件

2、加载二进制字节码,生成Class对象( 例如使用Class.forName()方法 )

3、通过反射机制获得实例构造,并创建代理类对象

demo

// 被代理对象
public class AirCraft {
    public void doSomething() {
        System.out.println("上天");
    }
}
// 拦截器 与测试用例
public class AirCraftInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("before:"+method.getName());
        Object object = proxy.invokeSuper(obj, args);
        System.out.println("after:"+method.getName());
        return object;
    }

    // 测试用例
    public static void main(String[] args) {
				System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/chenrunkai/Desktop/proxyInfo");
        Enhancer enhancer = new Enhancer();
        // 继承被代理类
        enhancer.setSuperclass(AirCraft.class);
        // 设置回调
        enhancer.setCallback(new AirCraftInterceptor());
        // 生成代理了对象
        AirCraft airCraft =  (AirCraft)enhancer.create();
        // 调用代理类的方法会被我们实现的方法拦截器进行拦截
        airCraft.doSomething();

    }
}

运行结果:

Spring AOP:从实践到原理

before:doSomething
上天
after:doSomething

Process finished with exit code 0

cglib动态代理生成的文件解析

public class AirCraft$$EnhancerByCGLIB$$13502f9c extends AirCraft implements Factory {
    private boolean CGLIB$BOUND;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static final Method CGLIB$doSomething$0$Method;
    private static final MethodProxy CGLIB$doSomething$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$finalize$1$Method;
    private static final MethodProxy CGLIB$finalize$1$Proxy;
    private static final Method CGLIB$equals$2$Method;
    private static final MethodProxy CGLIB$equals$2$Proxy;
    private static final Method CGLIB$toString$3$Method;
    private static final MethodProxy CGLIB$toString$3$Proxy;
    private static final Method CGLIB$hashCode$4$Method;
    private static final MethodProxy CGLIB$hashCode$4$Proxy;
    private static final Method CGLIB$clone$5$Method;
    private static final MethodProxy CGLIB$clone$5$Proxy;
..............
// 重写父类方法
public final void doSomething() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$doSomething$0$Method, CGLIB$emptyArgs, CGLIB$doSomething$0$Proxy);
        } else {
            super.doSomething();
        }
    }
...............
}

观察代理类的class文件,可以得到以下信息:

  1. 代理类继承了被代理类,重写了被代理类的方法
  2. 在重写的方法中,会先判断是否实现了MethodInterceptor的intercept方法,也就是是否实现了拦截器
  3. 如果实现了拦截器,则会调用我们实现的intercept方法。
  4. 没有实现拦截器,直接调用父类的方法

关于代理方式的选择

  • 根据 Spring Framework 5.x 文档。Spring AOP 默认使用 JDK 动态代理,如果对象没有实现接口,则使用 CGLIB 代理。
  • SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
  • 在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。

参考

第15章-Spring AOP切点表达式(Pointcut)详解

动态代理+注解(DynamicProxyAndAnnotations)

Spring 5 AOP 默认改用 CGLIB 了?从现象到源码的深度分析

【干货】JDK动态代理的实现原理以及如何手写一个JDK动态代理

CGLIB 动态代理 原理分析

Java代理-动态字节码生成代理的5种方式_思考、总结、专注-CSDN博客