AOP详解

一、先用 5W 把 AOP 捋顺

1. What:AOP 是什么

AOP 全称 Aspect Oriented Programming,面向切面编程。

它的核心思想是:
不去改原业务代码本身,而是在某些“时机点”把通用逻辑织进去。

比如你有很多 service 方法:

1
2
3
public void transfer() { ... }
public void createOrder() { ... }
public void pay() { ... }

你发现每个方法都想做这些事:

  1. 打日志

  2. 统计耗时

  3. 开事务

  4. 捕获异常

如果全写在方法里,业务代码会变脏:

1
2
3
public void transfer() {
long start = System.currentTimeMillis();
System.out.println("开始执行 transfer");
try {  
    // 业务逻辑  
1
2
3
4
5
6
7
    } catch (Exception e) {
System.out.println("异常:" + e.getMessage());
throw e;
} finally {
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}

AOP 想做的事就是:
这些通用逻辑我不想在每个业务方法里重复写,我想“统一切进去”。


2. Why:为什么要有 AOP

因为它解决了两个很现实的问题:

第一,减少重复代码

日志、事务、权限这种逻辑,很多方法都会用。
不抽出来的话会复制粘贴一大片。

第二,让业务代码更纯粹

业务方法只做业务本身。
像“记录日志”“开启事务”“鉴权”这些不属于核心业务的东西交给 AOP。

所以面试里你可以说:

AOP 的价值就是实现关注点分离,降低耦合,提高代码复用性和可维护性。

这句很稳。


3. Where:AOP 用在哪

最常见场景就是这些:

日志

方法执行前后打印日志,记录参数、返回值、耗时。

事务

Spring 里的 @Transactional 本质上就是 AOP 的经典应用。

权限校验

调用接口前判断当前用户有没有权限。

参数校验 / 幂等校验

比如下单接口要先检查 token 是否重复提交。

监控统计

统计某些方法调用次数、执行时间、异常率。

异常统一处理

虽然全局异常处理更常用 @ControllerAdvice,但有些业务层异常增强也会配合 AOP。


4. When:什么时候适合用 AOP

当你发现一段逻辑:

  1. 不是核心业务

  2. 多个地方都要用

  3. 执行位置比较固定,比如方法前、方法后、异常时

  4. 适合统一增强

这时候就适合 AOP。

比如事务就非常典型:
你根本不想每个方法都手动写:

1
2
beginTransaction();
try {
// 业务  
1
2
3
4
    commit();
} catch (Exception e) {
rollback();
}

所以 Spring 帮你用 AOP 接管了。


5. Who / How:谁来实现,怎么实现

在 Spring 里,AOP 一般是通过 代理模式 实现的。

也就是说:

你拿到的对象,很多时候不是原始对象,而是代理对象。
代理对象会在调用目标方法前后,加上额外逻辑。

Spring AOP 常见底层实现有两个:

JDK 动态代理

要求目标类 实现接口

CGLIB 动态代理

目标类 没有接口 也能代理,它是通过 继承目标类 生成子类代理。

你可以记一句:

有接口优先 JDK 动态代理,没有接口就走 CGLIB。


二、AOP 最核心的几个概念

这个部分是面试高频区,你一定要吃透。


1. Aspect:切面

切面就是你写的那一整套增强逻辑。

比如“日志切面”:

1
2
3
@Aspect
@Component
public class LogAspect {
...  

}

这个类整体就叫一个切面。


2. JoinPoint:连接点

连接点指的是:程序执行过程中可以被插入增强逻辑的点。

在 Spring AOP 里,主要指方法执行点

你可以简单理解:

哪个方法可以被拦截,那个位置就是连接点。


3. Pointcut:切点

切点是从很多连接点里,筛选出你真正要拦的那些点

比如:

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

意思就是:
com.example.service 包下所有类的所有方法,都是我要拦截的目标。


4. Advice:通知 / 增强

通知就是:你到底要在那个时机做什么事。

常见 5 种:

Before

方法执行前

After

方法执行后,不管成功还是异常都会执行

AfterReturning

方法正常返回后执行

AfterThrowing

方法抛异常后执行

Around

环绕通知,功能最强,前后都能控制,甚至可以决定方法执不执行

面试里通常会问:最常用哪个?

答案是:

环绕通知 Around 最强大,实际开发很常用。事务底层也很依赖类似环绕控制。


5. Target:目标对象

被代理、被增强的原始业务对象。

比如:

1
2
3
4
@Service
public class OrderService {
public void createOrder() { ... }
}

OrderService 就是目标对象。


6. Proxy:代理对象

Spring 实际返回给你的对象,通常不是原始的 OrderService,而是它的代理对象。

你调用的是代理对象的方法,代理对象再去调用目标方法,并织入增强逻辑。


三、AOP 执行流程,你要能脑补出来

假设你有一个订单方法:

orderService.createOrder();

如果这个方法被 AOP 增强了,执行流程大概是:

  1. 容器里拿到的是代理对象,不是原对象

  2. 你调用 createOrder()

  3. 代理对象先匹配这个方法是否命中切点

  4. 命中后先执行通知逻辑

  5. 再调用目标方法

  6. 方法执行后继续执行后置通知、返回通知、异常通知等

所以你可以把 AOP 想象成:

业务方法外面套了一层壳。


四、最经典的一套代码,先给你打通

下面这套是 Spring Boot 里最经典、最适合面试和项目复习的 AOP 日志案例。


1. 目标业务类

package com.example.service;

import org.springframework.stereotype.Service;

1
2
@Service
public class UserService {
1
2
3
4
5
6
7
8
    public String login(String username, String password) {
System.out.println("执行业务方法:login");
if ("admin".equals(username)) {
return "登录成功";
}
throw new RuntimeException("用户名错误");
}
}

2. 切面类

package com.example.aop;

1
2
3
4
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

1
2
3
@Aspect
@Component
public class LogAspect {
// 定义切点:拦截 service 包下所有类的所有方法  
1
2
@Pointcut("execution(* com.example.service..*.*(..))")
public void servicePointcut() {}
// 前置通知:方法执行前  
1
2
3
4
5
@Before("servicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("【前置通知】方法名:" + joinPoint.getSignature().getName());
System.out.println("【前置通知】参数:" + Arrays.toString(joinPoint.getArgs()));
}
// 后置返回通知:方法正常返回后  
1
2
3
4
5
@AfterReturning(value = "servicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("【返回通知】方法名:" + joinPoint.getSignature().getName());
System.out.println("【返回通知】返回值:" + result);
}
// 异常通知:方法抛异常后  
1
2
3
4
5
@AfterThrowing(value = "servicePointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
System.out.println("【异常通知】方法名:" + joinPoint.getSignature().getName());
System.out.println("【异常通知】异常信息:" + e.getMessage());
}
// 后置通知:方法结束后,不管成功还是异常  
1
2
3
4
@After("servicePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("【后置通知】方法执行结束:" + joinPoint.getSignature().getName());
}
// 环绕通知:最强  
1
2
3
4
@Around("servicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("【环绕前】开始执行:" + joinPoint.getSignature().getName());
1
2
3
4
5
6
7
8
9
10
11
12
13
        try {
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("【环绕后】方法执行成功");
return result;
} catch (Throwable e) {
System.out.println("【环绕异常】方法执行异常:" + e.getMessage());
throw e;
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println("【环绕最终】耗时:" + cost + "ms");
}
}
}

3. 启动类开启 AOP

Spring Boot 一般引入 starter 后就能用,很多场景自动开启。
你要是想写得明确一点,也可以加:

package com.example;

1
2
3
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
1
2
3
4
5
6
7
@EnableAspectJAutoProxy
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

4. 测试调用

package com.example;

1
2
3
4
import com.example.service.UserService;
import org.junit.jupiter.API.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
1
2
@SpringBootTest
public class AopTest {
1
2
@Autowired
private UserService userService;
1
2
3
4
5
@Test
void testLoginSuccess() {
String result = userService.login("admin", "123456");
System.out.println("测试结果:" + result);
}
1
2
3
4
5
    @Test
void testLoginFail() {
userService.login("tom", "123456");
}
}

五、你要真正看懂 execution 表达式

这个是切点表达式的核心。

最常见写法:

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

我们拆开看:

execution(...)

表示匹配方法执行

第一个 *

表示任意返回值类型

com.example.service..*

表示 service 包及其子包下任意类

第二个 *

表示任意方法名

(..)

表示任意参数列表

所以整体意思就是:

拦截 service 包及其子包下任意类的任意方法。


再看几个经典例子。

1. 拦截某个类所有方法

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

2. 拦截某个方法

execution(* com.example.service.UserService.login(..))

3. 拦截所有 public 方法

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

4. 拦截返回值是 String 的方法

execution(String com.example.service...(..))


六、五种通知怎么区分,面试常考

这个你要能非常清楚地说出来。

1. @Before

在目标方法执行前执行。
适合做参数检查、打印开始日志。

2. @After

在目标方法结束后执行。
无论是否异常都会执行。
有点像 finally

3. @AfterReturning

目标方法正常返回后执行。
适合记录返回值。

4. @AfterThrowing

目标方法抛出异常后执行。
适合记录异常日志。

5. @Around

最强大。
能在目标方法前后都做事,还能决定是否执行目标方法。
适合做权限控制、事务、性能统计、缓存控制等。


一个非常重要的点:

如果能用 Around,很多时候它一个就能覆盖前后逻辑。

比如统计耗时,Around 最适合:

1
2
3
4
5
6
7
8
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
return result;
}

七、Spring AOP 和 AspectJ 的区别

这个很容易被问到。

Spring AOP

Spring 自己提供的 AOP 实现。
它是 基于代理 的。
主要拦截方法级别

AspectJ

更完整更强大的 AOP 框架。
它不是单纯代理,它能在 编译期、类加载期、运行期 织入。
能力更强,不只限于方法执行点。

面试回答可以这样说:

Spring AOP 是运行时基于动态代理实现的,使用简单,适合大多数 Spring 项目。AspectJ 功能更强,织入能力更全面,但相对更重。


八、Spring AOP 的限制,超高频坑点

这个部分很值钱,很多人只会背概念,不会讲坑。


1. Spring AOP 主要拦截 public 方法

尤其常规代理场景下,你要增强的方法一般得是 public
私有方法、final 方法这类就不太适合普通 Spring AOP 代理。


2. final 类 / final 方法不适合 CGLIB 增强

因为 CGLIB 靠继承生成子类代理。
final 了,人家没法继承,也就没法增强。


3. 自调用失效,这是面试王炸坑点

看代码:

1
2
@Service
public class OrderService {
1
2
3
4
public void methodA() {
System.out.println("methodA");
methodB(); // 自调用
}
1
2
3
4
5
    @Transactional
public void methodB() {
System.out.println("methodB");
}
}

你以为 methodA()methodB() 时,@Transactional 会生效。
其实很可能不生效

为什么?

因为 AOP 是通过代理对象增强的。
你从外部调用:

orderService.methodB();

这是 代理对象 -> 目标对象,增强生效。

但在类内部:

methodB();

这是 this.methodB(),属于对象内部直接调用,没经过代理对象。
所以增强失效。

面试标准解释

Spring AOP 基于代理实现,只有通过代理对象调用目标方法,切面逻辑才会生效。类内部自调用没有经过代理对象,因此 AOP 失效。

解决思路

  1. methodB() 拆到另一个 bean 中

  2. 从 Spring 容器中拿代理对象再调用

  3. 开启暴露代理,用 AopContext.currentProxy() 获取代理对象调用

比如:

@EnableAspectJAutoProxy(exposeProxy = true)

1
2
@Service
public class OrderService {
1
2
3
4
public void methodA() {
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.methodB();
}
1
2
3
4
5
    @Transactional
public void methodB() {
System.out.println("methodB");
}
}

但实际项目里,更推荐拆 bean,可读性更好。


九、AOP 和代理模式是什么关系

这个要会串起来。

AOP 是思想,代理模式是实现手段之一。

在 Spring 里,AOP 很多时候就是用动态代理实现的。
代理对象帮你在调用目标方法前后织入逻辑。

所以可以说:

Spring AOP 的底层核心就是动态代理。


十、AOP 和 IOC 的关系

也常被顺手问。

IOC

负责创建对象、管理对象。

AOP

负责给这些对象加增强逻辑。

你可以理解为:

IOC 提供对象,AOP 在对象外面套代理。

Spring 先通过 IOC 把 bean 管起来,然后发现某个 bean 需要 AOP 增强,就为它创建代理对象并放进容器。

所以最后你注入到的,很多时候其实是代理对象。


十一、最经典应用:@Transactional 为什么是 AOP

这个一定要会说。

1
2
@Transactional
public void transfer() {
// 扣钱  
// 加钱  

}

你没写事务开启、提交、回滚代码,但事务照样生效。
本质就是 Spring 在这个方法外层织了一层事务逻辑。

伪代码大概像这样:

1
2
3
4
5
6
7
8
9
10
11
public Object invoke() {
beginTransaction();
try {
Object result = targetMethod();
commit();
return result;
} catch (Exception e) {
rollback();
throw e;
}
}

这是不是特别像环绕通知?

对,事务本质上就是一个很典型的 AOP 增强。


十二、再给你一个“权限校验”经典代码

这个比日志更像真实项目。

自定义注解

package com.example.annotation;

import java.lang.annotation.*;

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckPermission {
String value();
}

切面实现权限校验

package com.example.aop;

1
2
3
4
5
import com.example.annotation.CheckPermission;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
1
2
3
@Aspect
@Component
public class PermissionAspect {
1
2
3
@Before("@annotation(checkPermission)")
public void check(JoinPoint joinPoint, CheckPermission checkPermission) {
String permission = checkPermission.value();
    // 模拟当前用户权限  
    String currentUserPermission = "user:add";  
1
2
3
if (!permission.equals(currentUserPermission)) {
throw new RuntimeException("没有权限访问该方法");
}
1
2
3
        System.out.println("权限校验通过,权限为:" + permission);
}
}

业务方法使用

package com.example.service;

1
2
import com.example.annotation.CheckPermission;
import org.springframework.stereotype.Service;
1
2
@Service
public class AdminService {
1
2
3
4
5
    @CheckPermission("user:add")
public void addUser() {
System.out.println("执行新增用户操作");
}
}

十三、AOP 常见面试题的标准回答模板

这部分你可以直接背思路。


1. 什么是 AOP?

AOP 是面向切面编程,核心思想是把日志、事务、权限校验这类横切关注点从业务代码中抽离出来,通过切面统一织入到目标方法中,从而实现关注点分离,降低耦合,提高复用性和可维护性。


2. AOP 有哪些核心概念?

核心概念包括:

  1. Aspect 切面

  2. JoinPoint 连接点

  3. Pointcut 切点

  4. Advice 通知

  5. Target 目标对象

  6. Proxy 代理对象


3. Spring AOP 底层怎么实现?

Spring AOP 主要基于动态代理实现。
如果目标类实现了接口,通常使用 JDK 动态代理。
如果没有实现接口,通常使用 CGLIB 动态代理。


4. JDK 动态代理和 CGLIB 区别?

JDK 动态代理要求目标类实现接口,通过生成接口实现类做代理。
CGLIB 不要求接口,它通过继承目标类生成子类代理。
final 类和 final 方法不适合 CGLIB 增强。


5. @Transactional 为什么会失效?

一个重要原因是 类内部自调用
因为 Spring AOP 基于代理实现,只有通过代理对象调用方法时事务增强才会生效。类内部直接调用不会经过代理对象,因此事务可能失效。


6. AOP 适合哪些场景?

适合日志、事务、权限校验、性能统计、缓存、参数校验、异常增强等通用横切逻辑。


十四、你最后要形成的脑图

我帮你压缩成一条主线:

AOP 是为了把横切逻辑抽离出来,通过切面在目标方法的特定位置织入增强。Spring AOP 底层主要靠动态代理实现,常见应用有日志、事务、权限控制。核心概念是切面、切点、通知、连接点。重点坑点是代理机制带来的自调用失效。

这条主线你一旦记住,AOP 就不会再散。


十五、给你一个超短复习口诀

你临面试前可以默念这个:

AOP 干什么:抽横切
怎么做:代理织入
织到哪:切点
什么时候织:通知
谁被织:目标对象
最经典场景:事务、日志、权限
最大坑:自调用失效


十六、最后我帮你做一个“面试版 1 分钟回答”

你可以直接说:

“AOP 就是面向切面编程,主要用来把日志、事务、权限校验这类横切关注点从业务代码里抽离出来,统一增强到目标方法上。Spring AOP 底层主要是基于动态代理实现的,有接口一般用 JDK 动态代理,没有接口一般用 CGLIB。AOP 的核心概念包括切面、切点、通知、连接点、目标对象和代理对象。实际开发里最典型的应用就是 @Transactional。另外 Spring AOP 有一个常见坑,就是类内部自调用不会经过代理对象,所以切面可能失效。”