9K字详解:Spring AOP从实践到原理

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 的依赖:

     arduino 

    复制代码

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

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

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

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

 

java

复制代码

// 定义切面 @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) 环绕通知是一个取代原有目标对象方法的通知,也提供了回调原有目标对象的方法

执行顺序

 

csharp

复制代码

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

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

织入时机

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

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

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

切点表达式

execution

表达式模式

 

java

复制代码

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:匹配抛出异常类型,省略时匹配任意类型
 

java

复制代码

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

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

 

java

复制代码

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

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

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

@annotation,within

 

java

复制代码

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

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

ProceedingJoinPoint的使用

 

java

复制代码

import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AddDataCheck { String module(); }

 

java

复制代码

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

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,用于创建代理对象:

     java 

    复制代码

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

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

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

 

csharp

复制代码

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

创建一个 InvocationHandler 实现类:

 

typescript

复制代码

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 创建代理对象并调用方法:

 

arduino

复制代码

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(); } }

输出:

 

sql

复制代码

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

 

java

复制代码

// 被代理对象 public class AirCraft { public void doSomething() { System.out.println("上天"); } }

 

java

复制代码

// 拦截器 与测试用例 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(); } }

运行结果:

 

java

复制代码

before:doSomething 上天 after:doSomething Process finished with exit code 0

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

 

java

复制代码

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配置已无效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值