Spring AOP核心原理与实战:从代理机制到生产级切面设计
2026/8/11 5:53:56 网站建设 项目流程

1. 项目概述:为什么我们绕不开AOP?

干了这么多年开发,我发现一个挺有意思的现象:很多朋友对Spring框架里的IoC(控制反转)和DI(依赖注入)说得头头是道,觉得这就是Spring的核心。这没错,但在我看来,真正让Spring从一众框架里脱颖而出,展现出那种“优雅”和“强大”气质的,其实是另一个特性——AOP(面向切面编程)。你可能每天都在用@Transactional来管理事务,用@Cacheable来做缓存,或者自己写过一些日志切面,但有没有停下来想过,这背后到底是怎么一回事?为什么我们写的业务代码可以如此干净,而那些横跨多个模块的“琐事”却能被悄无声息地处理好?

AOP解决的,正是这种“交叉关注点”的难题。想象一下,你正在开发一个用户服务模块,里面有登录、注册、查询信息等方法。按照传统OOP(面向对象编程)的思路,你会在每个方法的开头和结尾,手动加上记录日志的代码;在每个需要更新数据库的方法前后,手动处理事务的开启、提交或回滚;在调用某些耗时的外部接口前,手动检查权限。很快你会发现,这些非核心的、却又必不可少的代码(日志、事务、安全),像藤蔓一样缠绕在你的核心业务逻辑上,导致代码重复、臃肿,且难以维护。今天要改一下日志格式,你得翻遍几十个方法;明天要调整事务的隔离级别,又是一个大工程。

AOP的思想,就是把这些散布在各处的“交叉关注点”从业务逻辑中剥离出来,集中到一个地方去管理和实现。这个“地方”,在AOP的术语里就叫“切面”(Aspect)。然后,通过一种神奇的机制,在程序运行的时候,把这些切面里定义的行为,“织入”到需要它们的业务方法中去。这样,你的业务类就只需要关心“用户登录时要验证密码和状态”这件事本身,变得清晰而纯粹。这种能力,不是简单的工具类封装能比拟的,它是一种编程范式的提升。

所以,无论你是刚接触Spring的新手,想弄明白@Transactional注解为何如此神奇;还是已经用过一些AOP功能的中级开发者,希望更深入地理解其原理并能自定义强大的切面;甚至是正在面试,需要清晰阐述JDK动态代理和CGLIB的区别,这篇文章都将为你提供一个从入门到精通的详细路径。我会结合我多年踩坑填坑的经验,不仅告诉你AOP怎么用,更会重点剖析它为什么这么设计,以及在实际项目中如何用得稳、避得坑。

2. AOP核心概念与运行机制深度拆解

在动手写代码之前,我们必须先建立起一套准确的AOP“世界观”。很多初学者觉得AOP概念抽象,正是因为对这些术语的理解停留在表面。让我们把它们一个个拆开,看看在Spring的语境下,它们具体指代什么,又是如何协作的。

2.1 核心术语的具象化理解

  1. 切面 (Aspect): 这是AOP的核心模块。它不是一个类,而是一个“模块化”的抽象。你可以把它想象成一个工具箱,这个工具箱里专门存放处理“日志记录”的所有工具和说明书。一个切面通常会包含两类东西:通知 (Advice)切点 (Pointcut)。在Spring中,切面通常用一个用@Aspect注解标注的普通Java类来实现。

  2. 连接点 (Join Point): 这是程序执行过程中一个非常明确的“点”。在Spring AOP中,这个“点”特指方法的执行。比如,UserService.login()方法的调用,就是一个具体的连接点。你可以把它理解为所有可以被“插入”额外逻辑的候选位置。

  3. 通知 (Advice): 这是切面在特定连接点执行的动作。也就是“做什么”。它定义了切面的具体逻辑,比如“记录日志”。通知有多种类型,决定了这个动作在连接点的“什么时候”执行:

    • 前置通知 (@Before): 在目标方法执行之前执行。常用于权限校验、参数校验、日志记录。
    • 后置通知 (@After): 在目标方法执行之后执行(无论成功还是异常)。常用于资源清理、后续通知。
    • 返回通知 (@AfterReturning): 在目标方法成功执行并返回结果后执行。可以访问到方法的返回值,常用于记录成功日志、处理返回结果。
    • 异常通知 (@AfterThrowing): 在目标方法抛出异常后执行。可以访问到抛出的异常对象,常用于异常处理、错误日志记录。
    • 环绕通知 (@Around): 这是功能最强大的通知。它包围了目标方法的执行,可以在方法调用前后执行自定义行为,甚至决定是否继续执行目标方法。它是实现事务、缓存等复杂功能的基石。
  4. 切点 (Pointcut): 这是一个表达式,用于匹配连接点。如果说连接点是所有可能的“位置”,那么切点就是一个“筛选器”,告诉AOP框架:“我的通知只应用于这些匹配上的连接点”。Spring使用AspectJ的切点表达式语言,功能非常强大。例如,表达式execution(* com.example.service.*.*(..))会匹配com.example.service包下所有类的所有方法。

  5. 引入 (Introduction): 允许我们向现有的类添加新的方法或属性。这是一个相对高级的特性,可以理解为动态地为类实现新的接口。比如,在不修改原有类代码的情况下,让某个服务类额外实现一个监控接口。在实际开发中不如前几种通知常用。

  6. 目标对象 (Target Object): 被一个或多个切面所通知的对象。也就是我们包含核心业务逻辑的那个对象,例如UserServiceImpl

  7. AOP代理 (AOP Proxy): 这是Spring AOP实现的关键。为了实现切面的织入,Spring会为目标对象创建一个代理对象。我们调用的不再是原始的目标对象,而是这个代理对象。代理对象在调用目标方法的前后,会执行切面中定义的拦截逻辑。Spring默认使用JDK动态代理(基于接口)或CGLIB(基于子类)来创建这个代理。

  8. 织入 (Weaving): 将切面应用到目标对象,从而创建代理对象的过程。Spring AOP是在运行时完成织入的,这不同于一些在编译期或类加载期织入的AOP框架(如AspectJ)。

注意: 务必区分Spring AOP和AspectJ。Spring AOP是Spring框架自身提供的一套基于代理的AOP实现,它只支持方法级别的连接点,功能足够应对绝大多数企业应用场景。而AspectJ是一个更强大、更完整的AOP框架,它支持编译期、类加载期织入,并能拦截字段访问、构造器调用等更多类型的连接点。Spring可以集成AspectJ,但通常我们说的“Spring AOP”指的是其自带的代理机制。

2.2 代理机制:JDK动态代理与CGLIB的抉择

这是面试高频考点,也是理解AOP底层原理的关键。Spring到底是如何创建那个“代理对象”的?

1. JDK动态代理

  • 原理: 基于Java原生的java.lang.reflect.ProxyInvocationHandler接口实现。它要求目标对象必须实现至少一个接口。代理对象会实现这个接口,并将所有方法调用委托给一个InvocationHandler。在InvocationHandlerinvoke方法中,我们可以插入切面逻辑,并决定是否以及如何调用原始目标方法。
  • 创建过程: Spring检查目标类,如果它实现了接口,则默认使用JDK动态代理。
  • 特点
    • 代理对象与目标对象是兄弟关系(都实现同一接口),而非父子关系。
    • 因为基于接口,所以只能代理接口中声明的方法。
    • 性能在早期版本中通常被认为优于CGLIB,但随着JVM和CGLIB的优化,差距已不明显。

2. CGLIB (Code Generation Library) 代理

  • 原理: 通过动态生成目标类的子类来创建代理。它通过操作字节码,在子类中重写父类的方法,并在重写的方法中加入拦截逻辑。
  • 创建过程: 如果目标类没有实现任何接口,Spring会自动使用CGLIB。你也可以通过配置强制Spring对所有情况都使用CGLIB(spring.aop.proxy-target-class=true)。
  • 特点
    • 代理对象与目标对象是父子关系(代理类是目标类的子类)。
    • 可以代理没有实现接口的类。
    • 因为是继承,所以无法代理final类或final方法。
    • 早期版本在生成代理类时有一定开销,但创建完成后运行效率很高。

如何选择与常见误区

  • 默认行为: Spring Boot 2.x 之后,默认配置下,如果目标对象实现了接口,则使用JDK动态代理;否则使用CGLIB。但在Spring Boot中,由于通常使用@Transactional等注解,而事务管理需要基于代理,所以为了保持一致性,Spring Boot默认强制使用了CGLIB代理(即proxy-target-class默认为true)。这是一个非常重要的实践细节!
  • 性能考量: 在现代JVM上,两者性能差异对于大多数应用来说可以忽略不计。不应将性能作为首要选择依据。
  • 设计考量
    • 如果你坚持“面向接口编程”,希望服务层通过接口暴露,那么JDK动态代理是自然的选择。
    • 如果你使用了类库中的第三方类(没有接口),或者就是喜欢写具体的类,那么CGLIB是必需的。
    • 强制使用CGLIB可以避免一个潜在的坑:如果你有一个类实现了接口,但你只想代理其中某些方法(通过切点表达式),使用JDK代理时,通过接口引用调用方法会被代理,而直接通过具体类引用调用方法则不会被代理(因为JDK代理生成的是接口实现类,而非目标类的子类)。而CGLIB由于是子类,无论通过哪种引用调用,都会被拦截。
// 一个展示代理差异的简单示例 public interface UserService { void doSomething(); } @Component public class UserServiceImpl implements UserService { @Override public void doSomething() { System.out.println("Doing real work..."); } } @Aspect @Component public class MyAspect { @Before("execution(* com.example.UserService.*(..))") public void beforeAdvice() { System.out.println("Before advice executed."); } } // 在另一个Bean中注入 @Autowired private UserService userService; // 这是一个JDK代理对象(如果未强制CGLIB) @Autowired private UserServiceImpl userServiceImpl; // 这是原始对象,NOT a proxy! 通知无效! // 调用 userService.doSomething() 会打印 “Before advice executed.” 和 “Doing real work...” // 调用 userServiceImpl.doSomething() 只会打印 “Doing real work...”,切面失效!

理解这两种代理方式的区别,不仅能帮助你在面试中应对自如,更能让你在遇到诡异的“切面不生效”问题时,快速定位到是否是代理类型导致的。

3. Spring AOP实战:从零编写一个功能完整的切面

理论说得再多,不如动手写一遍。让我们来设计并实现一个相对复杂的切面,它要完成以下功能:

  1. 记录所有Service层方法的入参、出参和执行时间。
  2. 当方法执行时间超过特定阈值时,发出警告日志。
  3. 统一处理特定类型的业务异常,并转换返回格式。

我们将这个切面命名为ServiceMonitorAspect

3.1 环境准备与基础配置

首先,在一个Spring Boot项目中,你需要确保引入了AOP的依赖。如果你使用Maven,在pom.xml中添加:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

这个starter已经包含了Spring AOP和AspectJ相关的库。Spring Boot会自动配置AOP,无需额外@EnableAspectJAutoProxy注解(在非Boot的Spring项目中需要此注解)。

3.2 定义切面与切点

我们创建一个类,并用@Aspect@Component注解标记它,让Spring能将其识别为一个切面并纳入容器管理。

package com.example.demo.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.core.annotation.Order; import java.util.Arrays; @Aspect @Component @Slf4j @Order(1) // 定义切面执行顺序,数字越小优先级越高 public class ServiceMonitorAspect { /** * 定义一个可重用的切点(Pointcut),匹配service包下所有类的所有方法。 * 这里使用 `execution` 表达式,它是功能最强大的切点指示器。 * 表达式解读:execution(返回类型 包名.类名.方法名(参数列表)) * - `*`: 匹配任意返回类型 * - `com.example.demo.service..*`: 匹配 `com.example.demo.service` 包及其所有子包下的所有类 * - `.*(..)`: 匹配任意方法名,以及任意参数列表(0个或多个参数) */ @Pointcut("execution(* com.example.demo.service..*.*(..))") public void serviceLayer() { // 方法体通常为空,它只是一个标记,用于承载@Pointcut注解 } /** * 另一个切点,用于匹配带有@CustomAnnotation注解的方法。 * 这展示了如何基于注解进行拦截,非常灵活。 */ @Pointcut("@annotation(com.example.demo.annotation.CustomAnnotation)") public void annotatedMethod() {} }

实操心得: 将通用的切点表达式用@Pointcut定义成空方法,是一种最佳实践。它提高了代码的复用性和可读性。后续的通知注解(@Around,@Before等)可以直接引用这个方法名(如serviceLayer()),而不是重复书写复杂的表达式。

3.3 实现环绕通知:方法监控的核心

环绕通知功能最强大,也最常用。我们将主要逻辑放在这里。

/** * 环绕通知:用于监控方法执行性能,记录日志。 * @param joinPoint 提供了访问目标方法信息的能力 * @return 目标方法的执行结果 * @throws Throwable 可能抛出的异常 */ @Around("serviceLayer()") public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 获取方法签名信息 String className = joinPoint.getTarget().getClass().getSimpleName(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); // 2. 记录方法开始执行,打印入参 log.info("[监控开始] {}.{}() 入参: {}", className, methodName, Arrays.toString(args)); long startTime = System.currentTimeMillis(); Object result = null; try { // 3. 执行目标方法 result = joinPoint.proceed(); // 这行代码是关键,它调用了原始的业务方法 // 4. 计算耗时 long elapsedTime = System.currentTimeMillis() - startTime; log.info("[监控结束] {}.{}() 执行耗时: {} ms, 返回: {}", className, methodName, elapsedTime, result); // 5. 慢方法警告 long slowThreshold = 1000L; // 阈值设为1秒 if (elapsedTime > slowThreshold) { log.warn("[性能警告] {}.{}() 执行过慢,耗时 {} ms, 超过阈值 {} ms", className, methodName, elapsedTime, slowThreshold); } } catch (Exception e) { // 6. 异常处理:记录异常日志,并可以选择重新抛出或转换异常 long elapsedTime = System.currentTimeMillis() - startTime; log.error("[监控异常] {}.{}() 执行耗时: {} ms 后发生异常: {}", className, methodName, elapsedTime, e.getMessage(), e); // 这里可以选择抛出一个自定义的、对前端更友好的异常 // throw new BusinessException("服务调用失败", e); throw e; // 默认重新抛出原异常 } return result; }

关键点解析

  • ProceedingJoinPoint: 这是JoinPoint的子接口,专用于环绕通知。最重要的方法是proceed(),它用于触发目标方法的执行。在proceed()前后,就是我们可以插入自定义逻辑的地方。
  • joinPoint.proceed()必须调用,否则目标方法永远不会执行。你可以调用一次,也可以不调用(实现权限拦截),甚至可以调用多次(但通常不这么做)。
  • 异常处理: 在catch块中,我们记录了异常日志。这里是一个关键决策点:是直接抛出原始异常,还是包装成业务异常?这取决于你的全局异常处理策略。如果直接抛出,异常会被Spring的全局异常处理器或调用者捕获。
  • 性能: 在切面中执行的操作(如日志IO、序列化参数等)本身也有开销。在高并发场景下,需要评估切面逻辑的性能影响,避免成为瓶颈。可以考虑对日志级别进行控制(如debug级别不记录参数),或使用异步日志。

3.4 使用其他类型通知进行补充

环绕通知虽然强大,但有时使用更具体的通知类型可以让意图更清晰。

/** * 前置通知:在目标方法执行前运行。适用于权限校验、参数预检查等。 * 注意:它无法阻止方法执行(除非抛异常),若要拦截,应用@Around。 */ @Before("serviceLayer() && args(userId, ..)") // 通过args绑定参数 public void checkPermission(Long userId) { log.debug("前置检查: 正在验证用户 {} 的权限", userId); // 模拟权限检查,失败可抛出 AuthorizationException if (userId == null || userId < 1L) { throw new IllegalArgumentException("无效的用户ID"); } } /** * 返回通知:在目标方法成功返回后执行。可以访问返回值。 * returning 属性用于将返回值绑定到通知方法的参数上。 */ @AfterReturning(pointcut = "serviceLayer()", returning = "returnValue") public void logSuccessfulReturn(Object returnValue) { log.debug("方法成功执行,返回值: {}", returnValue); // 可以在这里对返回值进行二次处理,例如缓存结果 } /** * 异常通知:在目标方法抛出特定异常后执行。 * throwing 属性用于将异常对象绑定到通知方法的参数上。 * 这里我们只捕获 IllegalArgumentException。 */ @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex") public void handleIllegalArgument(IllegalArgumentException ex) { log.error("业务方法抛出了非法参数异常: {}", ex.getMessage()); // 可以在这里进行特定的异常处理,如发送告警、记录详细错误等到特定存储 // 注意:异常通知执行后,异常依然会向上传播,除非你在这里处理掉它(不推荐轻易吞掉异常)。 } /** * 后置通知:在目标方法执行后执行(无论成功或失败)。类似于finally块。 * 适用于资源清理等操作。 */ @After("serviceLayer()") public void doCleanUp() { log.debug("执行后置清理工作..."); // 例如,清理ThreadLocal变量 }

3.5 切点表达式高级用法与组合

AspectJ切点表达式非常强大,掌握其语法能让你精确控制切面的作用范围。

// 1. 组合切点:使用 && (与), || (或), ! (非) @Pointcut("serviceLayer() && !annotatedMethod()") public void serviceLayerButNotAnnotated() {} // 这个切点匹配所有Service层方法,但排除那些带有@CustomAnnotation注解的方法。 // 2. 基于注解的切点 @Pointcut("@within(org.springframework.stereotype.Service)") // 匹配带有@Service注解的类中的所有方法 public void withinServiceAnnotation() {} @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)") // 匹配带有@Transactional注解的方法 public void transactionalMethod() {} // 3. 基于Bean名称的切点 (Spring特有) @Pointcut("bean(*Service)") // 匹配所有Bean名称以"Service"结尾的Bean中的所有方法 public void beanNamePointcut() {} // 4. 基于参数类型的切点 @Pointcut("execution(* *..*Service.*(java.lang.String, ..))") // 匹配第一个参数是String类型的方法 public void firstArgIsString() {} // 5. 在通知中组合使用 @Around("serviceLayer() && transactionalMethod()") public Object monitorTransactionalMethods(ProceedingJoinPoint pjp) throws Throwable { // 只监控那些既是Service层方法,又带有@Transactional注解的方法 log.info("监控一个事务方法..."); return pjp.proceed(); }

4. 生产环境中的进阶应用与最佳实践

掌握了基础用法后,我们来看看如何在复杂的生产环境中用好AOP,并避开那些常见的“坑”。

4.1 典型应用场景剖析

  1. 日志记录: 如前所述,这是AOP最经典的应用。但生产环境的日志切面需要考虑更多:

    • 脱敏: 在记录参数时,对密码、手机号、身份证号等敏感信息进行掩码处理。
    • 异步记录: 将日志记录操作放入独立的线程池中执行,避免阻塞主业务线程。可以使用@Async注解或自行管理线程池。
    • 条件记录: 根据方法执行结果(成功/失败)、耗时阈值等条件,决定是否记录详细日志,避免日志泛滥。
  2. 声明式事务管理 (@Transactional): Spring的事务管理是AOP应用的典范。理解其原理至关重要:

    • @Transactional注解本身就是基于AOP实现的。Spring会为被注解的类或方法创建代理,在方法开始前开启事务,在方法结束后提交或回滚事务。
    • 失效场景: 事务失效是常见问题。例如,自调用(同一个类中一个非事务方法调用另一个事务方法)会导致事务不生效,因为自调用绕过了代理对象。解决方法是将事务方法放到另一个Bean中,或使用AopContext.currentProxy()获取当前代理(不推荐,侵入性强)。
  3. 缓存抽象 (@Cacheable,@CacheEvict): Spring Cache也是基于AOP。它拦截方法调用,检查缓存中是否有结果,有则直接返回,无则执行方法并缓存结果。自定义Key生成器、缓存解析器是高级用法。

  4. 权限校验与安全: 在方法执行前,通过前置通知或环绕通知,检查当前用户的角色或权限。可以结合自定义注解(如@PreAuthorize("hasRole('ADMIN')"))实现非常灵活的权限控制。Spring Security就大量使用了AOP。

  5. 性能监控与度量: 集成Micrometer等度量库,在切面中统一收集方法的调用次数、成功失败率、耗时分布(直方图)等指标,并上报到Prometheus、InfluxDB等监控系统。

  6. 接口限流与熔断: 在环绕通知中,集成Resilience4j或Sentinel的API,轻松为任何方法添加限流、熔断、舱壁隔离等弹性能力。

  7. 数据绑定与验证: 在方法执行前,对参数进行统一格式校验或转换。

4.2 必须绕开的陷阱与解决方案

  1. 陷阱一:代理失效(自调用问题)

    • 现象: 在同一个类中,方法A(无事务)调用了方法B(有@Transactional),方法B的事务不生效。
    • 根因: 调用发生在目标对象内部,而非代理对象内部。this.methodB()this指的是目标对象本身,不是Spring创建的代理对象,因此事务拦截逻辑不会被执行。
    • 解决方案
      • (推荐)重构设计: 将方法B抽取到另一个Service Bean中,然后通过依赖注入调用。
      • (不推荐)使用AopContext: 在方法A中,通过((YourService) AopContext.currentProxy()).methodB()调用。这需要先暴露代理(@EnableAspectJAutoProxy(exposeProxy = true)),且代码有侵入性。
  2. 陷阱二:final方法与private方法

    • 现象: 为finalprivate方法添加的AOP通知不生效。
    • 根因
      • CGLIB通过生成子类来代理,无法重写final方法。
      • Spring AOP(无论是JDK还是CGLIB)基于代理,而private方法无法被代理对象访问到(因为代理对象调用的是继承或实现的方法)。
    • 解决方案: 不要试图代理finalprivate方法。将需要代理的方法改为publicprotected(非final)。
  3. 陷阱三:切面执行顺序混乱

    • 现象: 定义了多个切面(如日志切面、事务切面、缓存切面),它们的执行顺序不符合预期,可能导致逻辑错误(例如先缓存了数据,但事务还没提交)。
    • 解决方案
      • 使用@Order注解在切面类上指定顺序,数字越小优先级越高。注意@Order控制的是切面的顺序,而同一个切面内不同通知类型的执行顺序是固定的(环绕通知的proceed()之前部分最先,之后部分最后)。
      • 更细粒度的控制,可以实现Ordered接口。
      • 理解Spring内置切面(如事务)的默认Order值(通常是最高的,例如Ordered.LOWEST_PRECEDENCE - 1),确保你的自定义切面在它之前或之后正确执行。
  4. 陷阱四:异常处理不当导致“吃”掉异常

    • 现象: 在环绕通知或异常通知中捕获了异常并处理(如记录日志),但没有重新抛出,导致调用方认为方法执行成功。
    • 解决方案: 除非你有明确的理由(如进行降级处理并返回默认值),否则在记录完异常日志后,应该将原异常或包装后的异常重新抛出(throw e;)。
  5. 陷阱五:切点表达式过于宽泛或性能问题

    • 现象: 使用execution(* *..*.*(..))这样的表达式匹配所有方法,会给系统带来不必要的性能开销,且可能拦截到Spring内部Bean的方法,引发意外行为。
    • 解决方案: 尽可能精确地定义切点,限定到具体的包、类或注解。例如,execution(* com.yourcompany.yourproject.service..*.*(..))

4.3 性能优化与调试技巧

  1. 选择性织入: 在非生产环境(如开发、测试)开启详细的日志和监控切面,在生产环境通过配置(如@Profile("!prod"))或条件注解(@ConditionalOnProperty)关闭非核心的、耗性能的切面(如记录完整参数的日志切面)。

  2. 避免在切面中进行耗时/阻塞操作: 如网络IO、复杂计算、同步锁等。考虑异步化。

  3. 调试代理对象: 当切面不生效时,第一件事是确认Spring是否为目标Bean创建了代理。在调试器中,查看注入的Bean变量的实际类型。如果是com.sun.proxy.$ProxyXX(JDK代理)或YourService$$EnhancerBySpringCGLIB$$...(CGLIB代理),则说明代理已创建。如果是原始类名,则代理未生效,需要检查切点表达式是否正确、目标方法是否是public、是否在同一个Bean内自调用等。

  4. 使用AspectJ的编译时织入(CTW): 对于性能极其苛刻的场景,可以考虑使用AspectJ的编译时织入。它会在编译阶段就将切面代码直接织入到目标类字节码中,运行时没有代理开销,且可以拦截更多类型的连接点(如字段访问、构造器调用)。但这需要额外的构建配置(如AspectJ编译器ajc),增加了复杂度,一般Spring AOP的运行时代理已完全够用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询