
Spring AOP 系统性知识体系
一、AOP 基础
1.1 AOP 定义与核心思想
面向切面编程(Aspect-Oriented Programming) 是一种编程范式,它通过横切关注点的分离,解决了面向对象编程(OOP)中代码分散和代码纠缠的问题。
- 核心思想:将业务逻辑中与核心功能无关的通用功能(如日志、事务、权限、监控等)提取出来,形成独立的切面,在运行时动态地将这些切面织入到业务代码的指定位置
- 解决的问题:
- 代码冗余:相同逻辑重复出现在多个业务方法中
- 耦合度高:通用逻辑与业务逻辑紧密耦合,难以维护
- 扩展性差:新增通用功能需要修改所有相关业务代码
1.2 AOP 与 OOP 的关系
- 互补关系:AOP 不是 OOP 的替代品,而是 OOP 的补充
- OOP:擅长处理纵向的业务逻辑分层(Controller→Service→DAO)
- AOP:擅长处理横向的通用功能抽取(跨越多个业务模块的相同逻辑)
1.3 AOP在Spring全家桶中的地位
AOP是Spring Framework的两大核心支柱之一(另一个是IOC),它为Spring生态中的众多组件提供了基础支撑:
- Spring声明式事务管理
- Spring Cache缓存抽象
- Spring Security安全框架
- Spring Boot的自动配置与监控
- 各种企业级应用的非功能性需求实现
二、代理模式:AOP 的底层基石
AOP的核心实现机制是代理模式。代理模式通过引入一个代理对象来控制对目标对象的访问,在不修改目标对象代码的前提下,为其添加额外功能。
2.1 静态代理
定义
静态代理是在编译期就已经确定代理类与目标类的关系,代理类由程序员手动编写或工具生成。
实现步骤
- 定义业务接口
- 编写目标类实现业务接口
- 编写代理类实现相同的业务接口
- 在代理类中持有目标对象的引用
- 在代理方法中调用目标方法并添加额外逻辑
代码示例
// 1. 业务接口
public interface UserService {
void addUser(String username);
}
// 2. 目标类
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username) {
System.out.println("添加用户:" + username);
}
}
// 3. 静态代理类
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String username) {
// 前置增强
System.out.println("记录日志:开始执行addUser方法");
// 调用目标方法
target.addUser(username);
// 后置增强
System.out.println("记录日志:结束执行addUser方法");
}
}
// 4. 使用
public class Main {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("张三");
}
}
优缺点
- 优点:
- 简单直观,易于理解
- 编译期生成,运行时性能好
- 可以对特定方法进行精细控制
- 缺点:
- 代码冗余:每个接口都需要编写对应的代理类
- 维护成本高:接口方法变更时,代理类也需要同步修改
- 灵活性差:无法在运行时动态创建代理
2.2 动态代理
定义
动态代理是在运行时根据需要动态生成代理类和代理对象,无需手动编写代理类。
分类
Spring AOP 提供了两种动态代理实现:JDK动态代理和CGLIB动态代理。
2.3 JDK动态代理
JDK动态代理是Java原生提供的动态代理实现,它基于接口实现,只能代理实现了至少一个接口的类。
核心原理
- 基于接口实现,要求目标类必须实现至少一个接口
- 由JDK内置的
java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现 - 运行时动态生成一个实现了目标类所有接口的代理类
- 代理类会将所有方法调用转发给
InvocationHandler的invoke()方法处理
实现步骤
- 定义业务接口
- 编写目标类实现业务接口
- 编写调用处理器实现
InvocationHandler接口 - 在
invoke()方法中添加额外逻辑并调用目标方法 - 使用
Proxy.newProxyInstance()方法创建代理对象
代码示例
// 1. 业务接口和目标类同上
// 2. 调用处理器
public class LogInvocationHandler implements InvocationHandler {
private Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("JDK动态代理:记录日志,方法名:" + method.getName());
// 调用目标方法
Object result = method.invoke(target, args);
// 后置增强
System.out.println("JDK动态代理:方法执行完成");
return result;
}
}
// 3. 创建代理对象
public class Main {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 目标类实现的接口
new LogInvocationHandler(target) // 调用处理器
);
proxy.addUser("李四");
}
}
优缺点
- 优点:
- JDK原生支持,无需引入第三方依赖
- 实现简单,性能较好(JDK 8+有显著优化)
- 可以代理任意实现了接口的类
- 缺点:
- 只能代理实现了接口的类,无法代理没有实现接口的类
- 代理类会继承
Proxy类,由于Java单继承限制,无法再继承其他类
2.4 CGLIB动态代理
CGLIB(Code Generation Library)是一个强大的高性能代码生成库,它基于继承实现,可以代理没有实现接口的类。
核心原理
CGLIB通过ASM字节码框架在运行时动态生成目标类的子类,并重写目标类的非final方法。当调用代理对象的方法时,会先调用MethodInterceptor的intercept方法,然后再调用目标对象的方法。
- 基于继承实现,不要求目标类实现接口
- 使用第三方CGLIB(Code Generation Library)库实现
- 运行时动态生成目标类的子类作为代理类
- 代理类会重写目标类的所有非final方法
- 通过方法拦截器
MethodInterceptor来处理方法调用
实现步骤
- 编写目标类(无需实现接口)
- 编写方法拦截器实现
MethodInterceptor接口 - 在
intercept()方法中添加额外逻辑并调用目标方法 - 使用
Enhancer类创建代理对象
代码示例
// 1. 目标类(无需实现接口)
public class UserService {
public void addUser(String username) {
System.out.println("添加用户:" + username);
}
}
// 2. 方法拦截器
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 前置增强
System.out.println("CGLIB动态代理:记录日志,方法名:" + method.getName());
// 调用目标方法(使用MethodProxy避免反射)
Object result = proxy.invokeSuper(obj, args);
// 后置增强
System.out.println("CGLIB动态代理:方法执行完成");
return result;
}
}
// 3. 创建代理对象
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 设置父类(目标类)
enhancer.setCallback(new LogMethodInterceptor()); // 设置回调
UserService proxy = (UserService) enhancer.create();
proxy.addUser("王五");
}
}
优缺点
- 优点:
- 可以代理没有实现接口的类,适用范围更广
- 性能较好(Spring 3.2+集成了CGLIB,无需额外依赖)
- 生成的代理类是目标类的子类,类型转换更自然
- 缺点:
- 不能代理final类和final方法(因为无法继承和重写)
- 生成代理类的速度比JDK动态代理慢
- 需要依赖CGLIB库(Spring 3.2+已内置)
2.5 JDK动态代理 vs CGLIB动态代理 全面对比
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 底层原理 | 基于接口实现 | 基于继承实现 |
| 代理限制 | 必须实现至少一个接口 | 不能是final类,方法不能是final |
| 核心类 | Proxy、InvocationHandler |
Enhancer、MethodInterceptor |
| 方法调用方式 | 反射调用 | 直接调用(MethodProxy) |
| 依赖 | JDK原生,无需第三方依赖 | Spring 3.2+内置,早期版本需引入cglib依赖 |
| 生成速度 | 较快,生成实现了目标接口的代理类 | 较慢,生成目标类的子类 |
| 运行性能 | JDK 8+性能优秀,与CGLIB相当 | 早期版本性能优于JDK,JDK 8+差距缩小 |
| 代理范围 | 只能代理接口中定义的方法 | 可以代理所有非final方法 |
| Spring默认选择 | 当目标类实现接口时 | 当目标类没有实现接口时 |
2.6 Spring AOP 代理选择策略
Spring AOP 会根据目标类是否实现接口自动选择代理方式:
- 默认策略:
- 如果目标类实现了至少一个接口,使用JDK动态代理
- 如果目标类没有实现任何接口,使用CGLIB动态代理
- 强制使用CGLIB:
- 在Spring配置中设置
proxy-target-class="true" - 在Spring Boot中设置
spring.aop.proxy-target-class=true
- 在Spring配置中设置
- 注意事项:
- 当目标类实现了接口但强制使用CGLIB时,Spring会同时生成JDK代理和CGLIB代理
- 代理对象只能赋值给接口类型(JDK代理)或目标类类型(CGLIB代理)
三、Spring AOP 核心概念
Spring AOP是基于代理模式实现的AOP框架,它提供了一套完整的AOP解决方案。以下是Spring AOP的核心概念:
3.1 核心术语概念
- 切面(Aspect):横切关注点的模块化,它包含了通知和切入点。例如,日志切面、事务切面。
- 连接点(Join Point):程序执行过程中的某个点,如方法调用、异常抛出等。Spring AOP只支持方法级别的连接点。
- 通知(Advice):切面在特定连接点执行的动作。通知有五种类型:
- 前置通知(@Before):在目标方法执行前执行
- 后置通知(@After):在目标方法执行后执行(无论是否抛出异常)
- 返回通知(@AfterReturning):在目标方法正常返回后执行
- 异常通知(@AfterThrowing):在目标方法抛出异常后执行
- 环绕通知(@Around):在目标方法执行前后都执行,可以控制目标方法的执行
- 切入点(Pointcut):匹配连接点的表达式。它定义了哪些连接点需要被增强。
- 目标对象(Target Object):被代理的对象,也就是包含核心业务逻辑的对象。
- 代理对象(Proxy Object):AOP框架生成的对象,它包含了目标对象的所有方法和切面的增强逻辑。
- 织入(Weaving):将切面应用到目标对象并创建代理对象的过程。织入有三种时机:
- 编译期织入:在编译时将切面代码编译到目标类中(如AspectJ)
- 类加载期织入:在类加载时将切面代码织入到目标类中(如AspectJ 5+)
- 运行期织入:在运行时动态生成代理对象(Spring AOP默认方式)
3.2 核心术语图解
┌─────────────────────────────────────────────────┐ │ Aspect │ │ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │ │ Pointcut │ │ Advice │ │ Introduction │ │ │ └─────────┘ └─────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Join Point │ │ (方法执行、异常抛出等程序执行过程中的点) │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Weaving │ │ (将切面织入到目标对象的过程) │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Proxy │ │ (生成的代理对象) │ └─────────────────────────────────────────────────┘
3.2 核心术语详解
1. 切面(Aspect)
- 定义:横切关注点的模块化,包含了切点和通知的组合
- 通俗理解:一个切面就是一个包含了"在什么地方"和"做什么"的类
- 示例:日志切面、事务切面、权限切面
2. 连接点(Join Point)
- 定义:程序执行过程中的某个特定点,如方法执行、异常抛出、字段访问等
- Spring AOP支持的连接点:仅支持方法执行类型的连接点
- 通俗理解:所有可以被切面拦截的方法都是连接点
3. 切点(Pointcut)
- 定义:用于匹配连接点的表达式,定义了"在什么地方"应用通知
- 作用:从所有连接点中筛选出需要增强的特定方法
- 表达式类型:
- execution:匹配方法执行(最常用)
- within:匹配指定类型内的方法
- this:匹配代理对象类型的方法
- target:匹配目标对象类型的方法
- args:匹配参数类型符合要求的方法
- @annotation:匹配带有指定注解的方法
- 示例:
execution(* com.example.service.*.*(..))匹配service包下所有类的所有方法
4. 通知(Advice)
- 定义:切面在特定连接点上执行的动作,定义了"做什么"
- 通知类型:
| 通知类型 | 执行时机 | 适用场景 |
|---|---|---|
| 前置通知(Before) | 目标方法执行前 | 权限检查、日志记录 |
| 后置返回通知(AfterReturning) | 目标方法正常返回后 | 结果处理、日志记录 |
| 后置异常通知(AfterThrowing) | 目标方法抛出异常后 | 异常处理、日志记录 |
| 后置最终通知(After) | 目标方法执行后(无论是否异常) | 资源释放 |
| 环绕通知(Around) | 目标方法执行前后 | 性能监控、事务管理 |
5. 引入(Introduction)
- 定义:允许在不修改现有类的情况下,为其添加新的方法或字段
- 作用:可以让代理对象实现额外的接口
- 示例:为所有Service类添加一个
getVersion()方法
6. 织入(Weaving)
- 定义:将切面应用到目标对象并创建代理对象的过程
- 织入时机:
| 织入时机 | 说明 | 实现方式 |
|---|---|---|
| 编译期织入 | 在编译时将切面织入到目标类的字节码中 | AspectJ编译器 |
| 类加载期织入 | 在类加载到JVM时将切面织入 | AspectJ 5+的加载时织入(LTW) |
| 运行期织入 | 在运行时动态生成代理对象 | Spring AOP(JDK/CGLIB) |
7. 目标对象(Target Object)
- 定义:被一个或多个切面所通知的对象
- 注意:目标对象永远是被代理的对象,而不是代理对象本身
8. 代理对象(Proxy Object)
- 定义:AOP框架创建的对象,包含了目标对象的所有方法和切面的增强逻辑
- 注意:在Spring容器中,我们获取到的Bean实际上是代理对象
3.4 切入点表达式语法
Spring AOP使用AspectJ的切入点表达式语言来定义切入点。最常用的是execution表达式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
modifiers-pattern:方法修饰符(可选)ret-type-pattern:返回值类型(必选)declaring-type-pattern:类名(可选)name-pattern:方法名(必选)param-pattern:参数类型(必选)throws-pattern:异常类型(可选)
常用示例
- 匹配所有public方法:
execution(public * *(..)) - 匹配所有以"add"开头的方法:
execution(* add*(..)) - 匹配com.example.service包下所有类的所有方法:
execution(* com.example.service.*.*(..)) - 匹配com.example.service包及其子包下所有类的所有方法:
execution(* com.example.service..*.*(..))四、Spring AOP 实现机制与方式
4.1 Spring AOP的代理选择规则
Spring AOP会根据目标对象是否实现了接口来自动选择使用JDK动态代理还是CGLIB动态代理:
- 如果目标对象实现了至少一个接口,Spring会默认使用JDK动态代理
- 如果目标对象没有实现任何接口,Spring会使用CGLIB动态代理
- 可以通过配置强制使用CGLIB动态代理:
或在Spring Boot中:<aop:config proxy-target-class="true"/>spring.aop.proxy-target-class=true
4.2 Spring AOP的自调用问题
自调用问题是Spring AOP中最常见的问题之一。当一个类中的方法调用同一个类中的另一个方法时,被调用的方法不会被AOP增强。
原因分析
Spring AOP是基于代理的,只有通过代理对象调用的方法才会被增强。当使用this关键字调用同一个类中的方法时,实际上是调用了目标对象本身的方法,而不是代理对象的方法。
解决方案
使用ApplicationContext获取代理对象:
@Service public class UserService { @Autowired private ApplicationContext context; public void methodA() { // 获取代理对象 UserService proxy = context.getBean(UserService.class); proxy.methodB(); // 这样methodB会被增强 } @Transactional public void methodB() { // ... } }使用AspectJ编译期织入:
AspectJ通过编译期织入直接修改目标类的字节码,不存在自调用问题。但需要引入AspectJ编译器,配置相对复杂。
4.3 基于XML配置的AOP
<!-- 配置目标对象 -->
<bean id="userService" class="com.example.service.UserServiceImpl"/>
<!-- 配置切面 -->
<bean id="logAspect" class="com.example.aspect.LogAspect"/>
<!-- 配置AOP -->
<aop:config>
<!-- 定义切点 -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<!-- 定义切面 -->
<aop:aspect ref="logAspect">
<!-- 前置通知 -->
<aop:before pointcut-ref="servicePointcut" method="before"/>
<!-- 后置返回通知 -->
<aop:after-returning pointcut-ref="servicePointcut" method="afterReturning" returning="result"/>
<!-- 环绕通知 -->
<aop:around pointcut-ref="servicePointcut" method="around"/>
</aop:aspect>
</aop:config>
4.4 基于注解的AOP(推荐)
// 1. 开启AOP注解支持
@Configuration
@EnableAspectJAutoProxy // 等价于<aop:aspectj-autoproxy/>
public class AopConfig {
}
// 2. 定义切面
@Aspect
@Component
public class LogAspect {
// 定义切点
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {
}
// 前置通知
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint.getSignature().getName());
}
// 后置返回通知
@AfterReturning(pointcut = "servicePointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("后置返回通知:结果=" + result);
}
// 环绕通知
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long end = System.currentTimeMillis();
System.out.println("方法执行时间:" + (end - start) + "ms");
return result;
}
}
五、Spring AOP 应用场景
5.1 核心应用场景
1. 日志记录
- 记录方法调用信息(方法名、参数、返回值、执行时间)
- 记录系统操作日志(谁在什么时间做了什么操作)
- 记录异常日志(异常信息、堆栈跟踪)
2. 事务管理
- Spring声明式事务的底层就是AOP
- 在方法执行前开启事务,执行后提交事务,异常时回滚事务
- 无需在业务代码中编写事务管理代码
3. 权限控制
- 在方法执行前检查用户是否具有相应的权限
- 统一的权限验证逻辑,避免在每个业务方法中重复编写
- 支持细粒度的权限控制(方法级别的权限)
4. 性能监控
- 统计方法的执行时间
- 监控系统的性能瓶颈
- 生成性能报告
5. 异常处理
- 统一处理系统中的异常
- 将异常转换为统一的响应格式
- 记录异常信息并发送告警
6. 缓存管理
- 在方法执行前检查缓存中是否有结果
- 如果有则直接返回缓存结果,否则执行方法并将结果存入缓存
- 统一的缓存逻辑,简化业务代码
5.2 其他应用场景
- 数据校验
- 分布式锁
- 接口限流
- 接口幂等性保证
- 审计日志
- 资源池管理
六、Spring AOP 常见问题与最佳实践
6.1 常见问题
1. 内部方法调用无法被拦截
- 问题原因:当目标对象内部的方法A调用方法B时,方法B的调用是通过this引用进行的,而不是通过代理对象
- 解决方案:
- 方法1:使用
AopContext.currentProxy()获取代理对象 - 方法2:将方法B移到另一个类中
- 方法3:使用AspectJ编译期织入
- 方法1:使用
2. final方法无法被代理
- 问题原因:CGLIB无法重写final方法,JDK动态代理也无法代理接口中没有的final方法
- 解决方案:避免在需要被代理的类中使用final方法
3. 代理对象无法注入到目标对象中
- 问题原因:循环依赖导致代理对象创建失败
- 解决方案:
- 使用
@Lazy注解延迟加载 - 重构代码避免循环依赖
- 使用
6.2 最佳实践
- 优先使用注解方式:基于注解的AOP配置更加简洁、易读、易维护
- 切面职责单一:一个切面只负责一个横切关注点
- 切点表达式精确:避免使用过于宽泛的切点表达式,减少不必要的拦截
- 优先使用JDK动态代理:当目标类实现接口时,优先使用JDK动态代理
- 避免在通知中修改参数或返回值:除非必要,否则不要修改方法的参数和返回值
- 使用环绕通知时必须调用proceed():否则目标方法不会被执行
- 处理异常:在通知中妥善处理异常,避免影响业务逻辑的正常执行
- 优先使用注解方式配置AOP:注解方式比XML方式更简洁,易于维护
- 使用@Order注解指定切面执行顺序:当有多个切面时,使用
@Order注解可以明确指定它们的执行顺序
七、Spring AOP 与 AspectJ 的对比
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 动态代理(JDK/CGLIB) | 字节码操作(编译期/类加载期) |
| 连接点支持 | 仅支持方法执行 | 支持所有连接点(方法、构造器、字段、异常等) |
| 性能 | 运行时生成代理,性能略低 | 编译期织入,性能更高 |
| 复杂度 | 简单易用,学习成本低 | 功能强大,学习成本高 |
| 集成Spring | 原生集成 | 需要额外配置 |
| 适用场景 | 简单的横切关注点 | 复杂的横切关注点 |
八、总结
Spring AOP 是 Spring Framework 的核心组件之一,它基于代理模式实现了面向切面编程的思想。通过将通用功能从业务逻辑中分离出来,AOP 极大地提高了代码的可维护性和扩展性。
Spring AOP 提供了两种动态代理实现:JDK动态代理(基于接口)和CGLIB动态代理(基于继承),并会根据目标类的情况自动选择合适的代理方式。
理解 AOP 的核心概念(切面、连接点、切点、通知、织入)是掌握 Spring AOP 的关键。在实际开发中,AOP 广泛应用于日志记录、事务管理、权限控制、性能监控等场景,是构建企业级应用不可或缺的技术。
Spring AOP 面试高频问答卡片(背诵版)
基础概念篇
Q1:什么是AOP?它的核心思想是什么?解决了什么问题?
A:
- 定义:面向切面编程(Aspect-Oriented Programming)是一种编程范式,通过横切关注点的分离解决OOP中的代码分散和纠缠问题
- 核心思想:将业务逻辑中与核心功能无关的通用功能(日志、事务、权限、监控等)提取为独立切面,在运行时动态织入到业务代码的指定位置
- 解决的问题:
- 代码冗余:相同逻辑重复出现在多个业务方法中
- 耦合度高:通用逻辑与业务逻辑紧密耦合,难以维护
- 扩展性差:新增通用功能需要修改所有相关业务代码
Q2:AOP与OOP是什么关系?各自擅长处理什么问题?
A:
- 关系:AOP不是OOP的替代品,而是OOP的互补技术
- OOP:擅长处理纵向的业务逻辑分层(Controller→Service→DAO)
- AOP:擅长处理横向的通用功能抽取(跨越多个业务模块的相同逻辑)
Q3:AOP在Spring全家桶中的地位是什么?
A:AOP是Spring Framework的两大核心支柱之一(另一个是IOC),为Spring生态众多组件提供基础支撑:
- Spring声明式事务管理
- Spring Cache缓存抽象
- Spring Security安全框架
- Spring Boot的自动配置与监控
- 各种企业级应用的非功能性需求实现
代理模式篇(面试核心)
Q4:静态代理和动态代理有什么区别?
A:
| 对比维度 | 静态代理 | 动态代理 |
|---|---|---|
| 代理类生成时机 | 编译期确定,手动编写或工具生成 | 运行时动态生成,无需手动编写 |
| 代码量 | 每个接口都需要编写代理类,代码冗余 | 一个处理器可代理多个类,代码简洁 |
| 维护成本 | 接口变更时代理类需同步修改 | 接口变更不影响代理逻辑 |
| 灵活性 | 无法在运行时动态创建代理 | 可以在运行时动态创建任意代理 |
| 性能 | 编译期生成,运行时性能好 | 运行时生成代理类,性能略低 |
Q5:JDK动态代理的实现原理是什么?有什么优缺点?
A:
- 核心原理:
- 基于接口实现,要求目标类必须实现至少一个接口
- 由JDK内置的
Proxy类和InvocationHandler接口实现 - 运行时动态生成一个实现了目标类所有接口的代理类
- 所有方法调用都会转发给
InvocationHandler.invoke()方法处理
- 优点:
- JDK原生支持,无需第三方依赖
- 实现简单,JDK 8+性能优秀
- 缺点:
- 只能代理实现了接口的类
- 代理类继承了
Proxy类,受Java单继承限制
Q6:CGLIB动态代理的实现原理是什么?有什么优缺点?
A:
- 核心原理:
- 基于继承实现,不要求目标类实现接口
- 使用ASM字节码框架在运行时动态生成目标类的子类
- 重写目标类的所有非final方法
- 通过
MethodInterceptor.intercept()方法拦截方法调用
- 优点:
- 可以代理没有实现接口的类,适用范围更广
- Spring 3.2+已内置,无需额外依赖
- 缺点:
- 不能代理final类和final方法(无法继承和重写)
- 生成代理类的速度比JDK动态代理慢
Q7:JDK动态代理和CGLIB动态代理的核心区别是什么?
A:
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 底层原理 | 基于接口实现 | 基于继承实现 |
| 代理限制 | 必须实现至少一个接口 | 不能是final类,方法不能是final |
| 核心类 | Proxy、InvocationHandler |
Enhancer、MethodInterceptor |
| 方法调用 | 反射调用 | MethodProxy直接调用 |
| 生成速度 | 较快 | 较慢 |
| 运行性能 | JDK 8+与CGLIB相当 | 早期版本优于JDK |
Q8:Spring AOP默认的代理选择策略是什么?如何强制使用CGLIB代理?
A:
- 默认策略:
- 如果目标类实现了至少一个接口,使用JDK动态代理
- 如果目标类没有实现任何接口,使用CGLIB动态代理
- 强制使用CGLIB:
- XML配置:
<aop:config proxy-target-class="true"/> - Spring Boot配置:
spring.aop.proxy-target-class=true - 注解配置:
@EnableAspectJAutoProxy(proxyTargetClass=true)
- XML配置:
核心概念篇
Q9:解释Spring AOP的7个核心概念
A:
- 切面(Aspect):横切关注点的模块化,包含切点和通知的组合(如日志切面)
- 连接点(Join Point):程序执行过程中的点,Spring AOP仅支持方法执行类型
- 切点(Pointcut):匹配连接点的表达式,定义了"在什么地方"应用通知
- 通知(Advice):切面在特定连接点执行的动作,定义了"做什么"
- 目标对象(Target):被代理的对象,包含核心业务逻辑
- 代理对象(Proxy):AOP框架生成的对象,包含目标方法和增强逻辑
- 织入(Weaving):将切面应用到目标对象并创建代理对象的过程
Q10:Spring AOP支持哪五种通知类型?分别在什么时机执行?
A:
| 通知类型 | 执行时机 | 适用场景 |
|---|---|---|
| @Before | 目标方法执行前 | 权限检查、参数校验 |
| @AfterReturning | 目标方法正常返回后 | 结果处理、日志记录 |
| @AfterThrowing | 目标方法抛出异常后 | 异常处理、告警通知 |
| @After | 目标方法执行后(无论是否异常) | 资源释放 |
| @Around | 目标方法执行前后 | 性能监控、事务管理 |
Q11:织入有哪三种时机?Spring AOP默认使用哪种?
A:
- 编译期织入:编译时将切面代码编译到目标类中(如AspectJ编译器)
- 类加载期织入:类加载到JVM时织入(如AspectJ LTW)
- 运行期织入:运行时动态生成代理对象
- Spring AOP默认:运行期织入(JDK/CGLIB动态代理)
Q12:最常用的execution切入点表达式语法是什么?举几个常用例子
A:
- 语法:
execution(修饰符? 返回值类型 类名? 方法名(参数) 异常?) - 常用示例:
- 匹配所有public方法:
execution(public * *(..)) - 匹配所有以"add"开头的方法:
execution(* add*(..)) - 匹配service包下所有类的所有方法:
execution(* com.example.service.*.*(..)) - 匹配service包及其子包下所有类的所有方法:
execution(* com.example.service..*.*(..)) - 匹配带有@Transactional注解的方法:
@annotation(org.springframework.transaction.annotation.Transactional)
- 匹配所有public方法:
实现机制与问题篇
Q13:Spring AOP的自调用问题是什么?产生的原因是什么?有哪些解决方案?
A:
- 问题描述:当一个类中的方法A调用同一个类中的方法B时,方法B不会被AOP增强
- 根本原因:Spring AOP基于代理,只有通过代理对象调用的方法才会被增强。内部调用使用
this关键字,指向的是目标对象本身而非代理对象 - 解决方案:
- 使用AopContext获取代理对象:
((UserService)AopContext.currentProxy()).methodB() - 拆分方法到另一个类:将需要增强的方法移到独立的Bean中
- 使用AspectJ编译期织入:直接修改目标类字节码,不存在代理问题
- 使用AopContext获取代理对象:
Q14:为什么final方法无法被Spring AOP代理?
A:
- JDK动态代理:只能代理接口中定义的方法,而接口中不能有final方法
- CGLIB动态代理:通过生成目标类的子类并重写方法实现代理,而final方法无法被重写
- 因此,Spring AOP无法代理任何final方法
Q15:如何基于注解实现Spring AOP?请写出关键步骤
A:
- 开启AOP注解支持:在配置类上添加
@EnableAspectJAutoProxy - 定义切面类:创建普通类,添加
@Aspect和@Component注解 - 定义切点:在方法上添加
@Pointcut注解,指定切入点表达式 - 定义通知:在方法上添加对应的通知注解(@Before、@After等)
- 编写增强逻辑:在通知方法中实现具体的增强功能
核心代码示例:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {
}
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("方法执行前:" + joinPoint.getSignature().getName());
}
}
应用与对比篇
Q16:Spring AOP有哪些核心应用场景?
A:
- 日志记录:方法调用信息、操作日志、异常日志
- 事务管理:Spring声明式事务的底层实现
- 权限控制:方法级别的权限验证
- 性能监控:统计方法执行时间,定位性能瓶颈
- 异常处理:统一异常处理和转换
- 缓存管理:Spring Cache的底层实现
- 其他:数据校验、分布式锁、接口限流、幂等性保证
Q17:Spring AOP和AspectJ有什么区别?
A:
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 动态代理(JDK/CGLIB) | 字节码操作(编译期/类加载期) |
| 连接点支持 | 仅支持方法执行 | 支持所有连接点(方法、构造器、字段、异常等) |
| 性能 | 运行时生成代理,性能略低 | 编译期织入,性能更高 |
| 复杂度 | 简单易用,学习成本低 | 功能强大,学习成本高 |
| 集成Spring | 原生集成 | 需要额外配置 |
| 适用场景 | 简单的横切关注点 | 复杂的横切关注点 |
Q18:Spring AOP的最佳实践有哪些?
A:
- 优先使用注解方式配置AOP,简洁易维护
- 保持切面职责单一,一个切面只负责一个横切关注点
- 切点表达式尽量精确,避免使用过于宽泛的表达式
- 当目标类实现接口时,优先使用JDK动态代理
- 除非必要,不要在通知中修改方法参数或返回值
- 使用环绕通知时必须调用
proceed()方法,否则目标方法不会执行 - 使用
@Order注解明确指定多个切面的执行顺序 - 妥善处理通知中的异常,避免影响业务逻辑