导读:本期聚焦于下班再修创作的《Spring中基于AOP的@AspectJ如何正确使用?核心要点与常见问题详解》,敬请观看详情。在Spring里实现日志、权限、事务这类横切逻辑,如果全部硬编码进业务方法,不仅重复代码多,后期调整还会牵一发动全身。@AspectJ风格通过注解声明切面和通知,配合切入点表达式,把增强逻辑从核心业务中剥离,是目前Spring AOP最常用的落地方式。但实际项目中,通知不生效、自调用失效、切点匹配错误等问题经常出现,多半与代理机制和表达式语法有关。本文会从@Aspect、@Before、@Around等核心注解讲起,厘清五种通知的执行时机与适用场景,再说明切点表达式的匹配规则,最后集中梳理自调用、final方法、多切面顺序、异常处理等高频注意事项。掌握这些内容,能让横切功能更稳定,也便于排错。

Spring AOP 的 @AspectJ 体系并不是一套独立框架,而是借助 AspectJ 提供的注解和表达式语法,在 Spring 容器中声明切面逻辑,底层仍然运行在 Spring AOP 的代理机制之上。它的价值在于把权限校验、日志打印、缓存处理等横切关注点集中管理,避免在几十个 Service 方法里重复编写相同代码。不过要用好这套方案,必须先分清注解边界、切点匹配规则以及代理限制,否则很容易出现配置看似正确但通知就是不触发的情况。

Spring中基于AOP的@AspectJ如何正确使用?核心要点与常见问题详解

一、核心注解与通知执行时机

@AspectJ 切面编程的第一步是声明一个切面类。这个类要用 @Aspect 标记,同时还要交给 Spring 容器管理,通常配合 @Component 或在配置类中以 @Bean 方式注册。只写 @Aspect 不交给容器,切面不会被加载,通知自然也不会触发。Spring Boot 项目只要引入 spring-boot-starter-aop,自动配置会开启 AOP 代理支持,但如果需要暴露当前代理对象,后面会提到要额外开启 exposeProxy 配置。

通知类型决定了增强逻辑在目标方法生命周期的哪个位置执行。@Before 在方法执行之前运行,适合记录入参、检查权限或初始化上下文;@After 在方法结束后执行,无论正常返回还是抛异常都会进入,作用类似 finally 块;@AfterReturning 只在方法正常返回后执行,并且可以通过参数拿到返回值;@AfterThrowing 只在抛出异常后执行,常用于异常告警或清理资源;@Around 则完全包裹目标方法,可以控制是否继续执行、修改入参、修改返回值甚至吞掉异常,能力最强但使用成本也最高。

下面是一个最简单的日志切面,它在 Service 层所有方法执行前打印一行提示。这个切面声明了切入点表达式,用来圈定需要增强的方法范围。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceLogAspect {

    @Before("execution(* com.example.service..*(..))")
    public void recordBefore() {
        System.out.println("Service method is about to execute");
    }
}

如果需要计算方法执行耗时,@Around 是更合适的选择。它通过 ProceedingJoinPoint 对象控制目标方法的调用,并且可以在调用前后分别记录时间,最终返回目标方法的原始返回值。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TimingAspect {

    @Around("execution(* com.example.service.UserService.findById(..))")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println("cost: " + cost + "ms");
        }
    }
}

二、切点表达式如何精确匹配目标方法

切点表达式是 @AspectJ 体系里最容易出错的部分。最常用的 execution 表达式完整格式为:修饰符、返回值、声明类型、方法名、参数列表以及异常类型。日常使用中通常会省略修饰符和异常类型,写成 execution(* com.example.service..*(..)) 这种形式。第一个星号表示任意返回值,双点表示当前包及其子包,方法名位置的星号表示所有方法,参数列表中的双点表示任意个数、任意类型的参数。

除了 execution,还有几个高频表达式。within 按类或包限定范围,例如 within(com.example.service.*) 只匹配该包下的类;@annotation 按方法上的注解匹配,适合自定义审计或权限注解;@within 按类上的注解匹配;args 按方法参数类型匹配;bean 按 Spring Bean 名称匹配。表达式之间可以用 &&、||、! 组合,从而圈定更精确的范围。

多个切入点表达式组合时,通常会先用 @Pointcut 定义可复用的切点方法,再在通知注解中引用。这样做既能减少重复,也能让切面逻辑更容易维护。下面演示了将 Service 层切点与自定义注解切点组合使用的方式。

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.springframework.stereotype.Component;

@Aspect
@Component
public class AuditAspect {

    @Pointcut("execution(* com.example.service..*(..))")
    public void serviceLayer() {}

    @Pointcut("@annotation(com.example.annotation.AuditLog)")
    public void auditLog() {}

    @Around("serviceLayer() && auditLog()")
    public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

需要注意的是,@annotation 只匹配方法上的注解,而 @within 才匹配类级注解。很多人在类级别打了注解,却用 @annotation 去匹配,结果自然不会生效。参数类型匹配时还要考虑继承关系,例如 args(java.io.Serializable) 可以匹配参数为 String 的方法,因为 String 实现了 Serializable。

三、常见问题与注意事项

第一个高频问题是自调用失效。Spring AOP 依赖代理对象实现增强,当在同一个类内部通过 this 调用另一个方法时,调用的是原始对象而不是代理对象,因此通知不会触发。常见的场景是 createOrder 方法内部调用 pay 方法,而 pay 方法上声明了切面。此时外部调用 createOrder 时,内部的 pay 调用并不会经过代理。

@Service
public class OrderService {

    public void createOrder() {
        this.pay();
    }

    public void pay() {
        // 切面不会生效
    }
}

要解决这个问题,可以通过 AopContext.currentProxy() 拿到当前代理对象,然后用代理对象发起内部调用。这需要在配置类上开启 exposeProxy,并保证调用方式改为代理调用。除此之外,也可以将相关方法拆分到不同 Bean 中,或者注入自身代理对象。本质都是让调用经过 Spring 代理。

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AopConfig {
}
@Service
public class OrderService {

    public void createOrder() {
        ((OrderService) AopContext.currentProxy()).pay();
    }

    public void pay() {
        // 通过代理调用后通知生效
    }
}

第二个问题是代理机制对方法可见性和 final 的限制。Spring AOP 使用 JDK 动态代理或 CGLIB 代理。JDK 动态代理要求目标类实现接口,CGLIB 通过继承目标类生成子类。因此 private、static、final 方法都无法被增强,final 类也无法通过 CGLIB 代理。Spring Boot 2.x 默认使用 CGLIB,即使有接口也会走类代理,但 final 问题依然存在。如果业务中大量使用 final 方法,需要提前考虑是否改用 AspectJ 编译期织入。

第三个容易忽略的问题是通知执行顺序与异常传播。@After 无论是否异常都会执行,@AfterThrowing 只在异常时执行,@AfterReturning 只在正常返回时执行。如果 @Around 捕获了异常且没有继续抛出,那么全局异常处理器可能无法感知原始异常。多个切面同时作用于同一个方法时,可以用 @Order 注解或实现 Ordered 接口控制优先级。顺序值越小,越先进入目标方法,越晚离开,类似同心圆结构。

第四个问题是切点表达式看起来没问题,但实际没有匹配到任何方法。这时需要检查包路径是否写错、方法是否为 public、类是否被 Spring 管理、调用方是否通过代理调用。如果在切面类自身的方法上加切点,也可能因为切面类也被代理而出现诡异行为,建议把核心业务切面与切面类本身分开。

四、实战:日志与权限切面的完整实现

权限校验是 @AspectJ 的典型应用。通过自定义注解标记需要控制权限的方法,再配合 @Around 通知统一处理,可以把权限判断从业务代码中完全抽离。下面先定义一个 RequiresPermission 注解,用来描述方法所需的权限值。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    String value();
}

接着编写权限切面,通过 @annotation 表达式拦截所有标注了 RequiresPermission 的方法,并在通知方法参数中直接获取注解实例。这种方式不需要手动解析方法上的注解,代码更加清晰。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PermissionAspect {

    @Around("@annotation(requiresPermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable {
        String permission = requiresPermission.value();
        if (!hasPermission(permission)) {
            throw new SecurityException("缺少权限: " + permission);
        }
        return joinPoint.proceed();
    }

    private boolean hasPermission(String permission) {
        // 实际项目接入权限上下文
        return true;
    }
}

使用时只需在业务方法上添加注解,例如 @RequiresPermission("order:create")。当该方法被外部调用时,权限切面会先检查当前用户是否具备对应权限,不具备则直接抛出异常,业务方法不会继续执行。这样的设计让权限规则集中管理,新增权限控制点时无需修改业务逻辑,只需要给方法加上注解即可。

综合来看,@AspectJ 方案能够让 Spring AOP 的横切逻辑更加声明化,但它的稳定性高度依赖开发者对代理机制和切点语法的理解。日志、权限、事务等场景都可以借助这套机制实现统一治理。遇到通知不生效时,优先排查自调用、方法可见性、切点表达式以及 Bean 管理状态,往往能快速定位根因。

Spring AOP@AspectJ切点表达式修改时间:2026-09-20 01:38:25

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0920/59452.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。