摘要
代理模式是Java开发中最核心的设计模式之一,也是Spring AOP的底层基石。但很多同学写了三年Java,对静态代理、JDK动态代理、CGLIB代理的区别还是一知半解。这篇文章从源码级别拆解三种代理的实现原理,对比它们的性能差异和适用场景,最后结合Spring AOP的源码,看看Spring到底是怎么选代理的。看完这篇,面试问代理模式,你可以直接和他聊源码。
一、为什么会写这篇
最近在review项目代码时,发现组里一位同学在写一个日志切面,用的是Spring AOP的@Around注解。一切看起来很正常,直到他问了我一个问题:
"门主,为什么我加了@Transactional的Service方法,事务有时候生效有时候不生效?"
我让他把代码发过来看了一眼,发现他Service实现了一个接口,但是在@Transactional注解加在了实现类的方法上,而不是接口方法上。深入了解后才发现,他对Spring AOP底层用的哪种代理方式完全不清楚。
这其实不是个例。很多Java开发用了几年Spring,每天在用@Transactional、@Cacheable,但对底层的代理机制一知半解。一旦遇到代理失效、AOP不生效的问题,就开始玄学排错。
所以今天干脆把代理模式这件事彻底讲清楚。
二、什么是代理模式
2.1 定义
代理模式(Proxy Pattern)属于结构型设计模式。它的核心思想是:不直接访问目标对象,而是通过一个代理对象来间接访问。
听起来很简单对吧?但就是这个简单的思想,撑起了Spring AOP、MyBatis Mapper接口、RPC远程调用等一大片技术体系。
2.2 核心角色
代理模式中有三个核心角色:
| 角色 | 说明 | 类比 |
|---|---|---|
| Subject(抽象主题) | 定义真实对象和代理对象的公共接口 | 合同 |
| RealSubject(真实主题) | 真正执行业务逻辑的对象 | 房东 |
| Proxy(代理) | 持有RealSubject的引用,在调用前后做增强处理 | 中介 |
2.3 UML类图

2.4 为什么需要代理?
举个例子。假设你有一个UserService,里面有个saveUser()方法。现在上线要求:所有Service方法调用都要记录日志。
最简单的做法是什么?在每个方法里手动加日志:
c
public void saveUser(User user) {
log.info("开始保存用户: {}", user.getId());
// 业务逻辑...
log.info("保存用户完成");
}
一个方法还行,但如果有100个方法呢?这就是典型的横切关注点问题。日志、事务、权限这些逻辑和业务逻辑混在一起,代码越来越臃肿。
代理模式就是来解决这个问题的:把增强逻辑抽离出来,通过代理对象统一织入。
三、静态代理
3.1 实现方式
静态代理是最原始的代理方式。代理类和目标类实现同一个接口,在代理类中持有目标类的引用,在调用目标方法前后做增强处理。
3.2 代码示例
先定义一个接口:
arduino
/**
* 用户服务接口
* 不管是真实对象还是代理对象,都实现这个接口
*/
public interface UserService {
/**
* 保存用户
*/
void saveUser(String userId, String userName);
/**
* 查询用户
*/
String getUser(String userId);
}
真实业务类:
typescript
/**
* 真实的用户服务实现类
* 这里只关心业务逻辑,不关心日志、事务等横切关注点
*/
public class UserServiceImpl implements UserService {
@Override
public void saveUser(String userId, String userName) {
// 模拟业务处理
System.out.println("【业务逻辑】保存用户: id=" + userId + ", name=" + userName);
}
@Override
public String getUser(String userId) {
// 模拟业务处理
System.out.println("【业务逻辑】查询用户: id=" + userId);
return "User{id='" + userId + "', name='张三'}";
}
}
静态代理类:
typescript
import java.time.LocalDateTime;
/**
* 静态代理类
*
* 核心思路:
* 1. 实现和目标类相同的接口
* 2. 持有目标类的引用
* 3. 在调用目标方法前后做增强处理
*/
public class UserServiceStaticProxy implements UserService {
// 持有目标对象的引用
private final UserService target;
/**
* 通过构造方法传入目标对象
*/
public UserServiceStaticProxy(UserService target) {
this.target = target;
}
@Override
public void saveUser(String userId, String userName) {
// 前置增强:记录日志
System.out.println("【代理】前置日志 - 开始执行 saveUser,时间: " + LocalDateTime.now());
// 调用目标对象的实际方法
target.saveUser(userId, userName);
// 后置增强:记录日志
System.out.println("【代理】后置日志 - saveUser 执行完成,时间: " + LocalDateTime.now());
}
@Override
public String getUser(String userId) {
// 前置增强
System.out.println("【代理】前置日志 - 开始执行 getUser,时间: " + LocalDateTime.now());
// 调用目标方法
String result = target.getUser(userId);
// 后置增强
System.out.println("【代理】后置日志 - getUser 执行完成,时间: " + LocalDateTime.now());
return result;
}
}
客户端调用:
arduino
/**
* 客户端调用
* 对调用方来说,完全感知不到代理的存在
* 它只知道自己拿到的是一个 UserService 接口
*/
public class Client {
public static void main(String[] args) {
// 1. 创建真实对象
UserService realService = new UserServiceImpl();
// 2. 创建代理对象,把真实对象传进去
UserService proxy = new UserServiceStaticProxy(realService);
// 3. 调用代理对象的方法(对调用方透明)
proxy.saveUser("U001", "张三");
System.out.println("--- 分割线 ---");
String user = proxy.getUser("U001");
System.out.println("查询结果: " + user);
}
}
运行结果:
ini
【代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:30:15.123
【业务逻辑】保存用户: id=U001, name=张三
【代理】后置日志 - saveUser 执行完成,时间: 2025-06-15T10:30:15.125
--- 分割线 ---
【代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:30:15.125
【业务逻辑】查询用户: id=U001
【代理】后置日志 - getUser 执行完成,时间: 2025-06-15T10:30:15.125
查询结果: User{id='U001', name='张三'}
3.3 静态代理的优缺点
优点:
- 实现简单,容易理解
- 代码直观,编译期就能发现类型错误
缺点:
- 代码冗余严重:每个方法都要写一遍增强逻辑,如果有100个方法,代理类就有100份重复代码
- 类爆炸:每个目标类都需要一个对应的代理类。如果有10个Service,就需要10个代理类
- 维护成本高:新增方法时,接口、目标类、代理类都要改
- 只能代理特定接口:代理类是为特定接口服务的,不具备通用性
总结一句话:静态代理只适合接口方法极少、代理类数量极少的场景。企业项目中基本不会用。
四、JDK动态代理
4.1 为什么需要动态代理?
静态代理的问题在于:代理类是写死的,每个目标类都要手写一个代理类。能不能在运行时动态生成代理类?
答案是:JDK动态代理。
4.2 核心原理
JDK动态代理的核心是两个类:
java.lang.reflect.Proxy:负责生成代理对象java.lang.reflect.InvocationHandler:负责处理代理逻辑
核心流程:
- 调用
Proxy.newProxyInstance()方法 - JDK在运行时动态生成一个代理类(字节码),这个类实现了你指定的接口
- 所有接口方法的调用,都会被转发到
InvocationHandler.invoke()方法 - 在
invoke()方法中,你可以自由决定是否调用目标方法,以及在调用前后做什么
4.3 代码示例
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDateTime;
/**
* JDK动态代理的调用处理器
*
* 核心思想:
* 不再为每个接口手写代理类,而是通过一个通用的 InvocationHandler
* 来处理所有接口方法的调用
*/
public class LogInvocationHandler implements InvocationHandler {
// 持有目标对象
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
/**
* 代理对象的所有方法调用都会走到这里
*
* @param proxy 代理对象本身(注意:不要在这个方法里调用proxy的方法,会死循环)
* @param method 当前被调用的方法
* @param args 方法参数
* @return 方法执行结果
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法名,方便日志输出
String methodName = method.getName();
// 前置增强
System.out.println("【JDK动态代理】前置日志 - 开始执行 " + methodName
+ ",时间: " + LocalDateTime.now());
Object result = null;
try {
// 调用目标对象的实际方法
// 这里用了反射,method.invoke(target, args)
result = method.invoke(target, args);
// 后置增强
System.out.println("【JDK动态代理】后置日志 - " + methodName
+ " 执行成功,时间: " + LocalDateTime.now());
} catch (Exception e) {
// 异常增强
System.out.println("【JDK动态代理】异常日志 - " + methodName
+ " 执行失败,异常: " + e.getCause().getMessage());
throw e;
}
return result;
}
/**
* 创建代理对象的工厂方法
*
* @param target 目标对象
* @return 代理对象
*/
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target) {
// 三个参数:
// 1. 类加载器:用来加载动态生成的代理类
// 2. 接口数组:代理类需要实现的接口
// 3. InvocationHandler:方法调用处理器
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInvocationHandler(target)
);
}
}
客户端调用:
csharp
/**
* 客户端调用JDK动态代理
*/
public class JdkProxyClient {
public static void main(String[] args) {
// 1. 创建真实对象
UserService realService = new UserServiceImpl();
// 2. 通过工厂方法创建代理对象
UserService proxy = LogInvocationHandler.createProxy(realService);
// 3. 调用代理对象的方法
proxy.saveUser("U002", "李四");
System.out.println("--- 分割线 ---");
String user = proxy.getUser("U002");
System.out.println("查询结果: " + user);
// 验证代理对象的类型
System.out.println("--- 类型信息 ---");
System.out.println("代理对象类型: " + proxy.getClass().getName());
System.out.println("是否是代理对象: " + Proxy.isProxyClass(proxy.getClass()));
}
}
运行结果:
ini
【JDK动态代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:35:22.456
【业务逻辑】保存用户: id=U002, name=李四
【JDK动态代理】后置日志 - saveUser 执行成功,时间: 2025-06-15T10:35:22.458
--- 分割线 ---
【JDK动态代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:35:22.458
【业务逻辑】查询用户: id=U002
【JDK动态代理】后置日志 - getUser 执行成功,时间: 2025-06-15T10:35:22.458
查询结果: User{id='U002', name='李四'}
--- 类型信息 ---
代理对象类型: com.sun.proxy.$Proxy0
是否是代理对象: true
4.4 源码级分析:JDK动态代理到底做了什么?
关键问题来了:Proxy.newProxyInstance() 到底生成了什么?
我们可以通过设置系统属性来保存生成的代理类字节码,然后反编译看看:
erlang
// 在main方法开头设置,保存JDK动态代理生成的类到磁盘
System
.
setProperty
(
"sun.misc.ProxyGenerator.saveGeneratedFiles"
,
"true"
)
;
// JDK 8+ 使用:
// System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
反编译后看到的代理类大致长这样(简化版):
typescript
/**
* 这是JDK在运行时动态生成的代理类
* 注意:这个类在编译期不存在,是在运行时由ProxyGenerator生成的字节码
*/
public final class $Proxy0 extends Proxy implements UserService {
// 缓存Method对象,避免每次调用都通过反射获取
private static Method m1; // equals
private static Method m2; // toString
private static Method m3; // saveUser
private static Method m4; // getUser
private static Method m0; // hashCode
// 构造方法:接收InvocationHandler
public $Proxy0(InvocationHandler h) {
super(h); // 调用Proxy的构造方法,保存h
}
@Override
public final void saveUser(String userId, String userName) {
try {
// 核心逻辑:调用InvocationHandler的invoke方法
// 把当前方法(m3)和参数传进去
super.h.invoke(this, m3, new Object[]{userId, userName});
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
@Override
public final String getUser(String userId) {
try {
return (String) super.h.invoke(this, m4, new Object[]{userId});
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}
关键点就一个: 代理类$Proxy0继承自java.lang.reflect.Proxy,实现了UserService接口。所有接口方法的实现,都是调用super.h.invoke(),也就是InvocationHandler.invoke()。
4.5 JDK动态代理的优缺点
优点:
- 无需手写代理类 :运行时动态生成,一个
InvocationHandler可以代理任意接口 - 代码复用性高 :增强逻辑集中在
invoke()方法中 - 接口驱动:符合面向接口编程的思想
缺点:
- 必须实现接口:JDK动态代理要求目标类必须实现至少一个接口。如果目标类没有实现任何接口,就无法使用
- 性能开销:反射调用有一定的性能损耗(虽然JDK 8+已经做了大量优化)
五、CGLIB代理
5.1 为什么需要CGLIB?
JDK动态代理有一个硬伤:目标类必须实现接口。
但在实际项目中,并不是所有类都实现了接口。比如:
typescript
// 没有实现任何接口的类
public class UserService {
public void saveUser(String userId, String userName) {
System.out.println("保存用户: " + userId);
}
}
这种类怎么办?用不了JDK动态代理。
这时候CGLIB就派上用场了。
5.2 核心原理
CGLIB(Code Generation Library)的代理原理和JDK动态代理完全不同:
- JDK动态代理:基于接口,生成一个实现了该接口的代理类
- CGLIB代理:基于继承,生成目标类的子类作为代理类
CGLIB的核心机制:
- 通过ASM字节码框架,动态生成目标类的子类
- 子类重写所有非final的方法
- 在方法中通过
MethodInterceptor来拦截调用 - 调用父类(目标类)的方法,并在前后做增强
5.3 代码示例
首先添加Maven依赖:
xml
<!-- CGLIB 依赖 -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
目标类(没有实现接口):
typescript
/**
* 目标类:没有实现任何接口
* 这就是JDK动态代理无法处理的情况
*/
public class UserServiceNoInterface {
public void saveUser(String userId, String userName) {
System.out.println("【业务逻辑】保存用户: id=" + userId + ", name=" + userName);
}
public String getUser(String userId) {
System.out.println("【业务逻辑】查询用户: id=" + userId);
return "User{id='" + userId + "', name='王五'}";
}
// final方法:CGLIB无法重写,也就无法代理
public final void cannotProxy() {
System.out.println("这个方法是final的,CGLIB无法代理");
}
}
CGLIB代理实现:
java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* CGLIB代理实现
*
* 核心思路:
* 1. 通过Enhancer创建目标类的子类
* 2. 子类重写目标类的所有非final方法
* 3. 通过MethodInterceptor拦截方法调用
* 4. 调用父类方法(目标类方法)前后做增强
*/
public class LogMethodInterceptor implements MethodInterceptor {
/**
* 拦截目标类的方法调用
*
* @param obj 代理对象本身(目标类的子类)
* @param method 被拦截的方法(父类的方法)
* @param args 方法参数
* @param proxy 方法代理对象,用于调用父类的方法
* @return 方法执行结果
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
String methodName = method.getName();
// 前置增强
System.out.println("【CGLIB代理】前置日志 - 开始执行 " + methodName
+ ",时间: " + LocalDateTime.now());
Object result = null;
try {
// 注意:这里调用的是 proxy.invokeSuper(),而不是 method.invoke()
// invokeSuper 会调用父类(目标类)的方法,而不是代理类的方法
// 如果这里用 method.invoke(obj, args),obj是代理对象,会死循环
result = proxy.invokeSuper(obj, args);
// 后置增强
System.out.println("【CGLIB代理】后置日志 - " + methodName
+ " 执行成功,时间: " + LocalDateTime.now());
} catch (Exception e) {
System.out.println("【CGLIB代理】异常日志 - " + methodName
+ " 执行失败,异常: " + e.getCause().getMessage());
throw e;
}
return result;
}
/**
* 创建CGLIB代理对象
*/
@SuppressWarnings("unchecked")
public static <T> T createProxy(Class<T> targetClass) {
// 1. 创建Enhancer对象(CGLIB的核心类)
Enhancer enhancer = new Enhancer();
// 2. 设置父类(目标类)
enhancer.setSuperclass(targetClass);
// 3. 设置方法拦截器
enhancer.setCallback(new LogMethodInterceptor());
// 4. 创建代理对象(目标类的子类)
return (T) enhancer.create();
}
}
客户端调用:
csharp
/**
* 客户端调用CGLIB代理
*/
public class CglibProxyClient {
public static void main(String[] args) {
// 1. 创建CGLIB代理对象
UserServiceNoInterface proxy = LogMethodInterceptor.createProxy(
UserServiceNoInterface.class);
// 2. 调用代理对象的方法
proxy.saveUser("U003", "王五");
System.out.println("--- 分割线 ---");
String user = proxy.getUser("U003");
System.out.println("查询结果: " + user);
System.out.println("--- 类型信息 ---");
System.out.println("代理对象类型: " + proxy.getClass().getName());
System.out.println("代理对象的父类: " + proxy.getClass().getSuperclass().getName());
// 验证:代理对象是目标类的子类
System.out.println("是否是目标类的实例: " + (proxy instanceof UserServiceNoInterface));
}
}
运行结果:
ini
【CGLIB代理】前置日志 - 开始执行 saveUser,时间: 2025-06-15T10:40:33.789
【业务逻辑】保存用户: id=U003, name=王五
【CGLIB代理】后置日志 - saveUser 执行成功,时间: 2025-06-15T10:40:33.791
--- 分割线 ---
【CGLIB代理】前置日志 - 开始执行 getUser,时间: 2025-06-15T10:40:33.791
【业务逻辑】查询用户: id=U003
【CGLIB代理】后置日志 - getUser 执行成功,时间: 2025-06-15T10:40:33.791
查询结果: User{id='U003', name='王五'}
--- 类型信息 ---
代理对象类型: com.example.proxy.UserServiceNoInterface$$EnhancerByCGLIB$$a1b2c3d4
代理对象的父类: com.example.proxy.UserServiceNoInterface
是否是目标类的实例: true
5.4 CGLIB的注意事项
第一:final方法无法被代理
CGLIB基于继承,所以凡是不能被继承、不能被重写的方法,都无法被代理。包括:
final方法static方法private方法
第二:invokeSuper 和 invoke 的区别
这是最容易踩坑的地方:
scss
// 正确用法:调用父类(目标类)的方法
proxy.invokeSuper(obj, args);
// 错误用法:在代理对象上调用方法,会导致死循环
// method.invoke(obj, args); // obj是代理对象,会再次进入intercept()方法
第三:CGLIB生成的是子类
从输出可以看到,代理类名是 UserServiceNoInterface$$EnhancerByCGLIB$$a1b2c3d4,继承了 UserServiceNoInterface。这意味着:
proxy instanceof UserServiceNoInterface为true- 目标类如果有
public字段,子类可以直接访问
5.5 JDK 8+ 对CGLIB的替代
从JDK 16开始,Java提供了 java.lang.reflect.InvocationHandler 的一个变体方案,但真正值得注意的是 Spring已经可以不依赖CGLIB了。
Spring 4.0+ 自带了 org.springframework.cglib 包,把CGLIB的核心代码直接内嵌了,所以使用Spring的项目不需要额外引入CGLIB依赖。
六、三种代理方式对比
6.1 核心差异对比
| 对比维度 | 静态代理 | JDK动态代理 | CGLIB代理 |
|---|---|---|---|
| 实现原理 | 手写代理类,实现相同接口 | 运行时生成代理类,实现目标接口 | 运行时生成目标类的子类 |
| 是否需要接口 | 是 | 是 | 否 |
| 代理类生成时机 | 编译期 | 运行时 | 运行时 |
| 代理类与目标类关系 | 平级(实现相同接口) | 平级(实现相同接口) | 父子关系(继承) |
| 能否代理final方法 | 能(代理类本身有该方法) | 能(接口方法没有final限制) | 不能(final方法不可重写) |
| 调用方式 | 直接调用目标对象方法 | 通过反射调用 | 通过FastClass机制调用 |
| 代码维护 | 困难(类爆炸) | 容易(一个Handler代理所有) | 容易(一个Interceptor代理所有) |
| 性能(首次) | 最快(无额外开销) | 较慢(需要生成代理类) | 较慢(需要生成字节码) |
| 性能(调用) | 最快 | 较慢(反射调用) | 快(FastClass,接近原生) |
6.2 性能说明
很多人以为CGLIB比JDK动态代理慢,其实恰恰相反:
- JDK动态代理 :方法调用走的是反射,
Method.invoke()有额外的开销 - CGLIB代理:使用了FastClass机制,通过索引直接调用方法,性能接近原生调用
JDK 8+ 对反射做了大量优化,但即便如此,CGLIB的调用性能仍然优于JDK动态代理。
不过在实际项目中,这个性能差异通常可以忽略不计。真正影响性能的是代理对象的创建过程,而不是方法调用过程。
6.3 深入理解:CGLIB的FastClass机制
CGLIB之所以调用性能好,是因为它内部使用了 FastClass 机制。
简单来说,FastClass为一个类生成两个索引表:
- 一个索引表:根据方法签名,给方法编号
- 另一个索引表:根据方法编号,直接跳转到方法实现
调用时,先通过方法签名找到编号,然后通过编号直接调用。本质上是一个空间换时间的策略,避免了反射调用。
七、Spring AOP中的代理机制
7.1 Spring AOP的代理选择策略
这是面试高频题,也是很多人在实际开发中踩坑的地方。
Spring AOP的代理选择逻辑在 DefaultAopProxyFactory 中,源码如下:
arduino
// 来自 Spring 源码:org.springframework.aop.framework.DefaultAopProxyFactory
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 核心判断逻辑
if (!NativeDetector.inNativeImage()
&& (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("...");
}
// 如果目标类本身是接口,或者目标类是JDK代理类,则使用JDK动态代理
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 否则使用CGLIB代理
return new ObjenesisCglibAopProxy(config);
} else {
// 默认使用JDK动态代理
return new JdkDynamicAopProxy(config);
}
}
/**
* 判断是否没有用户自定义的代理接口
*/
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] interfaces = config.getProxiedInterfaces();
return interfaces.length == 0 || (interfaces.length == 1
&& SpringProxy.class.isAssignableFrom(interfaces[0]));
}
}
7.2 选择逻辑总结
Spring选择JDK代理还是CGLIB代理的逻辑如下:
arduino
是否满足以下任一条件?
1. config.isOptimize() == true (优化标志)
2. config.isProxyTargetClass() == true (proxyTargetClass=true)
3. 没有用户自定义的代理接口
┌───────── 是 ─────────┐ ┌──── 否 ────┐
↓ ↓ ↓
目标类是否是接口或 ↓ 使用JDK动态代理
JDK代理类? ↓
┌─ 是 → JDK动态代理 ↓
└─ 否 → CGLIB代理 ↓
7.3 如何在Spring中配置
方式一:全局配置(XML方式)
xml
<!-- 强制使用CGLIB代理 -->
<aop:config proxy-target-class="true">
<aop:aspect ref="logAspect">
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<aop:around pointcut-ref="servicePointcut" method="logAround"/>
</aop:aspect>
</aop:config>
方式二:全局配置(注解方式)
less
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用CGLIB
public class AppConfig {
// ...
}
方式三:Spring Boot中的配置
ini
# application.properties
spring.aop.proxy-target-class=true
7.4 经典踩坑:事务注解失效
这是我在项目中被问得最多的问题。来看一个典型的场景:
java
// 接口
public interface UserService {
void saveUser(User user);
}
// 实现类
@Service
public class UserServiceImpl implements UserService {
@Override
@Transactional // 事务注解加在实现类方法上
public void saveUser(User user) {
// 保存用户...
saveLog(user); // 调用本类的另一个方法
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(User user) {
// 保存日志...
}
}
问题: saveLog() 的事务不会生效。
原因分析:
- Spring默认配置下,如果
UserServiceImpl实现了接口,优先使用JDK动态代理 - JDK动态代理生成的代理对象,只代理接口中声明的方法
saveLog()不是接口方法,所以不会被代理- 更关键的是,
saveUser()内部调用saveLog()是直接调用this.saveLog(),这里的this是目标对象本身,不是代理对象
解决方案:
typescript
// 方案一:将事务方法提取到接口中
public interface UserService {
void saveUser(User user);
void saveLog(User user); // 在接口中声明
}
// 方案二:强制使用CGLIB代理
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 方案三:通过AopContext获取当前代理对象
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserService self; // 注入自己(代理对象)
@Override
@Transactional
public void saveUser(User user) {
// ...
self.saveLog(user); // 通过代理对象调用
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(User user) {
// ...
}
}
八、实际应用场景
8.1 声明式事务管理
这是代理模式在企业应用中最经典的应用。Spring的 @Transactional 注解,底层就是通过AOP代理来实现的。
scss
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
// 1. 保存订单(代理对象会在方法执行前开启事务)
orderDao.insert(order);
// 2. 扣减库存
stockService.deduct(order.getProductId(), order.getQuantity());
// 3. 发送消息(代理对象会在方法执行后提交事务)
// 如果上面任何一步报错,代理对象会自动回滚事务
mqService.sendOrderMessage(order);
}
}
8.2 日志记录
统一的日志记录,不用在每个方法里手动写 log.info()。
java
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 前置日志
log.info("调用方法: {},参数: {}", methodName, args);
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
// 后置日志
long elapsed = System.currentTimeMillis() - start;
log.info("方法执行成功: {},耗时: {}ms,结果: {}", methodName, elapsed, result);
return result;
} catch (Exception e) {
log.error("方法执行异常: {},异常: {}", methodName, e.getMessage());
throw e;
}
}
}
8.3 权限控制
在方法执行前检查当前用户是否有权限。
java
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(permission)")
public Object checkPermission(ProceedingJoinPoint joinPoint,
Permission permission) throws Throwable {
String requiredRole = permission.value();
String currentUserRole = SecurityContextHolder.getContext()
.getAuthentication().getAuthorities().iterator().next().getAuthority();
// 检查权限
if (!currentUserRole.equals(requiredRole)) {
throw new AccessDeniedException("权限不足,需要: " + requiredRole);
}
return joinPoint.proceed();
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
String value(); // 需要的角色
}
// 使用
@RestController
public class UserController {
@Permission("ADMIN")
@GetMapping("/admin/users")
public List<User> listUsers() {
return userService.listAll();
}
}
8.4 缓存
通过代理在方法执行前检查缓存,如果缓存命中直接返回,避免执行方法。
java
@Aspect
@Component
public class CacheAspect {
private final CacheManager cacheManager;
@Around("@annotation(cacheable)")
public Object aroundCache(ProceedingJoinPoint joinPoint,
Cacheable cacheable) throws Throwable {
String cacheName = cacheable.value();
String key = generateKey(joinPoint);
// 先查缓存
Cache cache = cacheManager.getCache(cacheName);
Cache.ValueWrapper wrapper = cache.get(key);
if (wrapper != null) {
log.info("缓存命中: {}:{}", cacheName, key);
return wrapper.get();
}
// 缓存未命中,执行方法
Object result = joinPoint.proceed();
// 放入缓存
cache.put(key, result);
log.info("缓存写入: {}:{}", cacheName, key);
return result;
}
}
8.5 性能监控
统计每个方法的调用次数和耗时,用于监控告警。
java
@Aspect
@Component
public class MonitorAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(monitored)")
public Object monitor(ProceedingJoinPoint joinPoint,
Monitored monitored) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
// 调用次数计数
Counter.builder("method.calls")
.tag("method", methodName)
.register(meterRegistry)
.increment();
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long duration = System.nanoTime() - start;
// 记录耗时
Timer.builder("method.duration")
.tag("method", methodName)
.register(meterRegistry)
.record(duration, TimeUnit.NANOSECONDS);
// 如果耗时超过阈值,告警
if (duration > monitored.threshold().toNanos()) {
log.warn("方法执行超时: {},耗时: {}ms",
methodName, duration / 1_000_000);
}
}
}
}
九、总结
代理模式从设计模式上来说不难,但它在Java生态中的地位极其重要。Spring AOP、事务管理、MyBatis Mapper接口、RPC框架,底层全部依赖代理机制。
三种代理方式的选择建议:
- 静态代理:基本不用。除非你的接口极其稳定且方法极少,否则别碰。
- JDK动态代理:优先选择。如果你的代码遵循"面向接口编程"的原则,JDK动态代理足够用,而且不需要额外依赖。
- CGLIB代理:当目标类没有实现接口,或者需要代理非接口方法时使用。Spring Boot 2.0+ 默认使用CGLIB,因为大部分场景下我们不需要额外的接口层。
门主踩坑总结:
- 事务注解加在
private方法上?不生效。因为代理子类无法重写private方法。 - 同一个类中方法A调用方法B,方法B的
@Transactional不生效?因为this指向的是目标对象,不是代理对象。 - 接口方法上加了
@Transactional,但实现类实现了接口?如果Spring发现接口上有事务注解,会优先使用JDK动态代理。如果实现类方法上也有@Transactional,事务注解会被覆盖。
三连支持,动力源泉
如果这篇文章帮你省下了踩坑的时间,欢迎:
点赞 ------ 让更多人看到这篇干货
在看 ------ 你的认可是我持续输出的动力
转发 ------ 分享给身边正在做AI Agent的朋友
你的每一个小动作,对我都很重要 ❤️
关于作者
你好,我是 空门技术栈 ,一个常年和Bug战斗、持续填坑的Java开发者。
专注分享:
- ✅ Java / Spring Boot / Spring AI Alibaba 企业级实战
- ✅ RAG知识库、AI Agent、多智能体协作落地经验
- ✅ Docker部署、微服务架构、线上问题排查
- ✅ 偶尔聊聊「如何保住头发」这类程序员终极话题
不搞水文,不贩卖焦虑,只写能跑通、能落地、能帮你少加班的实战内容。
关注我,咱们一起少踩坑,多写优雅代码。
更多干货推荐
- 告别手动复制接口文档!Apifox MCP + AI 自动测试让开发效率起飞
- MySQL MCP Server 从零安装到使用实战,AI 直接查询数据库
- Spring Event 用了三年,同事一句话把我问懵了
- Java 抽象类(Abstract Class)彻底讲透:从基础到多态实战
- Spring AI Alibaba 多智能体(Multi-agent)实战:6 大协作模式 + 完整代码
- Spring AI Alibaba 智能体作为工具实战:别再让主 Agent 当"人肉路由器"了
- 一文搞懂 Spring AI Alibaba Workflow:10 个实战案例带你彻底掌握 AI 工作流编排
- AI 工程进入第四时代!Loop Engineering 正在淘汰 Prompt Engineering
- RAG 知识库为什么越更新越乱?一文讲透生产级文档更新方案
- GPT-5.6 三档模型:Sol、Terra、Luna 怎么选?一篇文章讲透
- Transformers VS vLLM:大模型部署到底该选谁?从本地运行到生产上线完整解析
粉丝福利
本文涉及的 完整源码 + 可运行Demo + 配置示例,已打包整理好。
免费获取方式 :
私信空门技术栈,领取源码包。
有任何问题,也欢迎后台私信,门主看到都会回复~
对文章内容有疑问?想看下一篇写什么主题?
直接在评论区留言,你的每一条评论我都会看。说不定下一篇文章的主题,就来自你的问题!
项目合作 / 技术咨询
平时工作之余,也会接一些技术项目和咨询,主要方向:
⚔️ 企业级开发
AI 应用落地(这是我最近的主力方向)
- Spring AI Alibaba / RAG / Agent 应用开发
- 企业私有知识库搭建
- AI能力接入现有业务系统
- 大模型本地化部署与调优
️ 技术顾问 / 疑难Bug排查
- 项目架构评审与方案设计
- 线上疑难问题定位解决
- 技术选型与团队培训
如果你正遇到以下情况,欢迎找我聊聊:
- ✅ 想做AI项目,但技术方案拿不准
- ✅ 项目卡在某个Bug上很久,团队搞不定
- ✅ 想把AI接入现有业务,不知道从哪下手
- ✅ 需要靠谱的开发外包或长期技术顾问