Spring AOP 详解

Spring AOP 详解

目标:把 Spring AOP “是什么 / 怎么跑 / 怎么写 / 坑在哪” 一口气讲透。
你看完至少能做到:写得出常用切面、看得懂事务为什么生效/失效、排得了 AOP 的线上坑。


1. AOP 到底在解决什么问题?

AOP(Aspect Oriented Programming,面向切面编程)本质上是在做一件事:

把“横切关注点”从业务代码里抽出来,统一管理。

横切关注点 = 不是业务核心,但几乎每个业务点都会用到的东西,例如:

  • 日志埋点(入参/出参/耗时/traceId)
  • 监控指标(QPS、RT、错误率)
  • 权限校验(鉴权/验签/数据权限)
  • 事务(@Transactional)
  • 幂等、防重、限流、熔断
  • 审计(谁在什么时候改了什么)
  • 异常统一转换

如果不用 AOP,你会在每个方法里 copy 一堆 try/catch、日志、耗时统计 —— 业务很快变成“面条代码”。


2. Spring AOP 的关键概念(面试必问)

记住这一组词就够了:连接点、切点、通知、切面、织入、代理

2.1 Join Point(连接点)

可以被拦截的位置
在 Spring AOP 里基本就是:方法执行(method execution)。

2.2 Pointcut(切点)

你到底要拦截哪些连接点
也就是一条规则/表达式:execution(...)@annotation(...) 等。

2.3 Advice(通知)

拦截到之后要干什么

  • @Before:方法执行前
  • @After:方法执行后(无论是否异常)
  • @AfterReturning:正常返回后
  • @AfterThrowing:抛异常后
  • @Around:最强(可控制是否执行目标方法、可改返回值、可吞异常)

2.4 Aspect(切面)

切点 + 通知 + 其它配置(如 order),组合成一个“模块”。

2.5 Weaving(织入)

把切面应用到目标对象上,形成最终执行链。
Spring AOP 是在 运行时 通过代理织入(不是编译期)。

2.6 Proxy(代理)

Spring AOP 的实现方式:用代理对象替代原对象,外部调用进来时先走代理,再走目标方法。


3. Spring AOP vs AspectJ(别混了)

  • Spring AOP
    • 运行时代理(JDK 动态代理 / CGLIB)
    • 只支持方法级别(Method Execution)切入
    • 轻量,够用,和 Spring 容器集成非常好
  • AspectJ
    • 编译期/类加载期织入(更“强”)
    • 能拦截字段、构造器、静态块等更多 join point
    • 需要额外 weaving 配置,复杂度更高

现实里:大多数业务系统 Spring AOP 足够;需要“拦截非方法执行”才考虑 AspectJ weaving。


4. Spring AOP 是怎么跑起来的(核心原理)

4.1 代理创建发生在什么时候?

Bean 创建完成后,Spring 会用一堆 BeanPostProcessor 进行后置处理。
AOP 的关键是:AutoProxyCreator(一类 BeanPostProcessor)。

流程大概是:

  1. Spring 创建 bean(实例化、属性注入、初始化)
  2. 进入 BeanPostProcessor:
    • postProcessBeforeInitialization
    • 初始化(init-method / @PostConstruct)
    • postProcessAfterInitialization
  3. AOP 的 AutoProxyCreator 在 AfterInitialization 阶段判断:
    • 这个 bean 是否命中某个 Advisor(切点 + 通知)
    • 命中就创建代理对象(Proxy)
    • 用代理替换原 bean 放进容器

所以你最终从容器拿到的很可能不是原类,而是代理类。

4.2 Advisor / Interceptor / MethodInvocation(执行链)

Spring 会把通知包装成一串拦截器(Interceptor),形成调用链:

代理入口 -> 拦截器链(通知) -> 目标方法

@Around 本质就是拦截器链里的一环,并且能决定是否调用 proceed() 继续链条。


5. JDK 动态代理 vs CGLIB(怎么选?)

5.1 JDK 动态代理

  • 基于接口:目标类必须实现接口
  • 生成一个实现相同接口的代理类
  • 优点:不需要子类化,较轻
  • 缺点:没接口就用不了(除非强制用 CGLIB)

5.2 CGLIB

  • 基于继承:给目标类生成子类代理
  • 优点:不需要接口
  • 缺点:
    • final 类 / final 方法不能被代理(不能重写)
    • 代理类生成相对更重(但现在 JVM 里通常没啥大问题)

5.3 Spring 默认策略(常见结论)

  • 有接口 → 默认 JDK 代理
  • 没接口 → 用 CGLIB
  • 你也可以强制 CGLIB:
    • @EnableAspectJAutoProxy(proxyTargetClass = true)
    • spring.aop.proxy-target-class=true

6. 注解式 AOP(@AspectJ 风格)怎么写

最推荐用法:@Aspect + @Around/@Before

6.1 最常用切面模板(日志 + 耗时)

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@Order(10) // 数字越小,优先级越高(越先执行)
public class LogAspect {

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

    @Around("serviceMethods()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        String method = pjp.getSignature().toShortString();
        try {
            Object result = pjp.proceed();
            long cost = System.currentTimeMillis() - start;
            log.info("[AOP] {} ok, cost={}ms", method, cost);
            return result;
        } catch (Throwable ex) {
            long cost = System.currentTimeMillis() - start;
            log.warn("[AOP] {} fail, cost={}ms, ex={}", method, cost, ex.toString());
            throw ex;
        }
    }
}

6.2 让切点更精准:按注解拦截(推荐)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String value() default "";
}
@Aspect
@Component
public class AuditAspect {

    @Pointcut("@annotation(auditLog)")
    public void auditPointcut(AuditLog auditLog) {}

    @Around(value = "auditPointcut(auditLog)", argNames = "pjp,auditLog")
    public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
        // auditLog.value() 就是注解参数
        return pjp.proceed();
    }
}

这种方式比 execution(..) 更稳(改包名、改方法名不容易误伤/漏拦)。


7. Pointcut 表达式速查(能背就背)

最常用:execution + @annotation,其它知道即可。

7.1 execution(方法签名匹配)

格式(别死背,理解就行):
execution(访问修饰符 返回值 包名.类名.方法名(参数))

例子:

execution(* com.example.service.UserService.*(..))        // UserService 所有方法
execution(public * com.example..service..*(..))           // service 包及子包所有 public 方法
execution(* *..*Service.save*(..))                        // 所有以 save 开头的方法
execution(* *(java.lang.String, ..))                      // 第一个参数是 String 的方法

7.2 within(按类/包匹配)

within(com.example..service..*)                           // 某个包下所有类的方法
within(com.example.service.UserService)                   // 某个类

7.3 this / target(按代理类型/目标类型)

  • this(xxx):代理对象类型匹配
  • target(xxx):目标对象类型匹配

7.4 args(按参数类型)

args(java.lang.Long, ..)                                  // 第一个参数是 Long

7.5 @annotation / @within / @target

  • @annotation(注解):方法上有这个注解
  • @within(注解):类上有这个注解
  • @target(注解):目标类上有这个注解(和 @within 类似但语义略有区别)

实战建议:能用 @annotation 就优先用它。


8. 通知执行顺序(常被问)

以一个 @Around 和其它通知同时存在为例,典型顺序是:

  1. @Around(进入)
  2. @Before
  3. 目标方法执行
  4. @AfterReturning(若正常返回)或 @AfterThrowing(若抛异常)
  5. @After
  6. @Around(退出)

多个切面之间通过 @Order 控制优先级:数字越小越靠外层(越先进入、越后退出)。


9. Spring AOP 的经典坑(尤其事务)

9.1 自调用(self-invocation)导致 AOP 失效(第一大坑)

@Service
public class OrderService {

    public void outer() {
        innerTx(); // 直接 this.innerTx(),不会走代理
    }

    @Transactional
    public void innerTx() { ... }
}

原因:outer()innerTx() 在同一个对象里,调用是 this 调用,绕过代理。

解决套路(按推荐程度):

  1. 把 innerTx 放到另一个 Bean,通过注入调用(最干净)
  2. 开启暴露代理:@EnableAspectJAutoProxy(exposeProxy = true) 然后:
    ((OrderService) AopContext.currentProxy()).innerTx();
    
    (能用但不优雅)
  3. 用 AspectJ weaving(重,但能拦截内部调用)

9.2 final / private 方法拦不到

  • Spring AOP 主要拦截 public 方法(尤其事务)
  • CGLIB 也无法覆盖 final 方法
  • private 方法本来就不是外部调用点,代理也很难切进去

9.3 没被 Spring 管理的对象,AOP 不生效

你自己 new 出来的对象,容器没机会给你换成代理。

9.4 切点写太宽导致“全站被拦截”,性能/日志爆炸

execution(* com..*(..)) 这种一不小心就炸。
建议:

  • 优先按注解
  • 或者限定在 service 包 + public + 业务前缀

10. 怎么确认一个 Bean 到底有没有被代理?(排障必备)

10.1 看类名

Object bean = ctx.getBean(UserService.class);
System.out.println(bean.getClass());
  • JDK 代理:com.sun.proxy.$Proxyxx
  • CGLIB:UserService$$EnhancerBySpringCGLIB$$...

10.2 用 AopUtils

import org.springframework.aop.support.AopUtils;

boolean isAop = AopUtils.isAopProxy(bean);
boolean isCglib = AopUtils.isCglibProxy(bean);
boolean isJdk = AopUtils.isJdkDynamicProxy(bean);

10.3 开日志(快速看到 Advisor 匹配)

把这些包的日志级别调到 DEBUG/TRACE:

  • org.springframework.aop
  • org.springframework.aop.framework
  • org.springframework.aop.aspectj

11. AOP 在真实项目里最常见的 5 个落地

  1. 统一日志/链路追踪:接口入参/出参 + traceId + 耗时
  2. 统一异常转换:把底层异常转成业务错误码
  3. 权限/数据权限:基于注解 + SpEL 做规则表达
  4. 幂等/防重:@Idempotent(Redis/DB 唯一键)配合 AOP
  5. 事务:@Transactional 本质也是 AOP(Advisor + 拦截器)

12. 一句话总结(背这句就够了)

Spring AOP 用 BeanPostProcessor 在 Bean 初始化后创建 代理对象,通过 切点 匹配目标方法,把 通知 组装成拦截器链,实现对方法调用的增强;默认优先 JDK 动态代理(有接口),否则用 CGLIB,最常见的坑是 自调用绕过代理


13. 额外补充:面试高频问答快闪

  • Q:@Transactional 为什么是 AOP?
    A:事务拦截器本质就是一个 MethodInterceptor,匹配到方法就开启/提交/回滚事务。

  • Q:为什么同类内部调用事务不生效?
    A:绕过代理(this 调用),没走拦截器链。

  • Q:AOP 能拦截 static 方法吗?
    A:Spring AOP 基本不行(代理是对象级别),AspectJ weaving 才可能做到更多 join point。

  • Q:@Order 数字越大越先执行?
    A:不是。数字越小越优先,越靠外层。


Spring AOP(面向切面编程)是Spring框架中的一个模块,用于提供横切关注点(Cross-Cutting Concerns)的支持。横切关注点是与应用程序的核心业务逻辑无关的功能,例如日志记录、性能统计、事务管理等。 在Spring AOP中,通过定义切面(Aspect)来捕获横切关注点,并将其应用到目标对象的方法中。切面由切点(Pointcut)和通知(Advice)组成。切点定义了在何处应用通知,通知则定义了在切点处执行的操作。 Spring AOP支持以下几种类型的通知: 1. 前置通知(Before Advice):在目标方法执行之前执行的通知。 2. 后置通知(After Advice):在目标方法执行之后执行的通知,不管方法是否抛出异常。 3. 返回通知(After Returning Advice):在目标方法成功执行并返回结果后执行的通知。 4. 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。 5. 环绕通知(Around Advice):围绕目标方法执行的通知,可以在方法调用前后执行自定义操作。 除了通知,Spring AOP还支持引入(Introduction)和切点表达式(Pointcut Expression)等功能。引入允许为目标对象添加新的接口和实现,而切点表达式则允许开发人员定义切点的匹配规则。 要在Spring应用程序中使用AOP,需要进行以下步骤: 1. 引入Spring AOP的依赖。 2. 配置AOP代理。 3. 定义切面和通知。 4. 配置切点和通知之间的关系。 总之,Spring AOP提供了一种便捷的方式来处理横切关注点,使得开发人员可以将关注点与核心业务逻辑分离,提高代码的可维护性和可重用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值