谈谈 Spring 的 AOP?

#Java#面试八股

谈谈 Spring 的 AOP?

结论先行:AOP(Aspect Oriented Programming,面向切面编程)把日志、事务、权限、缓存这类"横切关注点"从业务代码里抽出来,集中声明、按需织入。Spring AOP 是运行时基于动态代理的、方法级的、半自动 AOP:容器为被切中的 Bean 生成代理,外部调用先进入通知链(advice chain),再落到目标方法。它不修改字节码、不支持字段级连接点,这是与 AspectJ 最本质的差别。

一、概念对齐

概念含义Spring 中的落地
切面 Aspect横切逻辑的模块化单元@Aspect 标注的类
连接点 JoinPoint程序执行中可以插入逻辑的点只能是方法调用/执行
切点 Pointcut匹配连接点的表达式@Pointcut("execution(* com.demo.service.*.*(..))")
通知 Advice在连接点处执行的动作@Before、@Around 等
织入 Weaving把切面接到目标上的过程运行时生成代理(非编译期)
目标 Target被代理的业务对象原始 Bean

二、调用链

外部调用 ──▶ 代理对象(Proxy)
                │
                ├─ @Before 前置通知
                ├─ 目标方法执行(ProceedingJoinPoint.proceed)
                ├─ @AfterReturning / @AfterThrowing
                └─ @After 最终通知
                │
            返回结果 / 抛出异常

通知执行顺序(同一连接点、Spring 5.3+ 统一语义):

通知时机能否改变返回值能否吞异常
@Around包裹全程能(可改返回值)能
@Before目标方法前否否
@AfterReturning正常返回后能(returning 参数绑定,不建议改)否
@AfterThrowing抛异常后否能(不再抛即吞掉)
@After无论成败(类似 finally)否否

三、切点表达式与代码

@Aspect
@Component
@Order(1)   // 多个切面时决定顺序,数字越小越外层
public class CostAspect {

    // 切点:service 包下所有 public 方法,任意参数、任意返回值
    @Pointcut("execution(public * com.demo.service..*.*(..))")
    public void serviceMethods() {}

    // 切点:所有带 @Loggable 注解的方法
    @Pointcut("@annotation(com.demo.common.Loggable)")
    public void annotated() {}

    @Around("serviceMethods() || annotated()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();                       // 必须调用,否则业务不执行
        } finally {
            long cost = (System.nanoTime() - start) / 1_000_000;
            System.out.printf("%s 耗时 %d ms%n", pjp.getSignature().toShortString(), cost);
        }
    }
}
// 注解式切面:业务侧只加注解,切面统一处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

execution 表达式的结构:execution(修饰符? 返回值 包.类.方法(参数) throws?),其中 .. 表示"任意层级包/任意个参数",* 表示"任意",务必写对包名层级,否则切点悄悄不匹配。

四、Spring AOP 与 AspectJ

维度Spring AOPAspectJ
织入时机运行时(创建 Bean 时生成代理)编译期/编译后/加载期
实现JDK 动态代理或 CGLIB修改字节码
连接点仅方法执行方法、构造器、字段、静态块等
是否需容器需要(Bean 才被代理)不需要,普通 Java 对象也可
性能每次调用多一层代理,可忽略织入后接近原生调用
使用成本零配置(starter 已启用)需要编译器/agent 配合

五、常见追问

  • 问:AOP 与代理的关系? Spring AOP 的实现就是代理模式 + 拦截器链;没有代理就没有 AOP 效果,所以"自调用失效"实质是"没经过代理"。
  • 问:this 和 target 的区别? this 匹配代理对象类型,target 匹配目标对象类型;JDK 代理下两者可能不同,写切点时用 target 更稳。
  • 问:多个切面怎么排序? @Order 或实现 Ordered;@Around 的顺序是"外层先进入、后退出",像洋葱。
  • 问:AOP 能拦 Controller 吗? 能,但 Controller 通常不被代理(没切点),且 @RequestMapping 方法参数绑定发生在代理之内,切面拿不到原始 HttpServletRequest 之外的信息。

小结

回答"谈谈 AOP"分四层:是什么(横切关注点解耦)、怎么实现(动态代理 + 通知链)、怎么写(切面 + 切点表达式 + 通知类型)、边界在哪(仅方法级、需容器、自调用失效)。能把"代理是运行时织入的唯一手段"讲透,就已经超过背概念的水平。

笔记加载中…