一、什么是代理模式?
在深入动态代理之前,我们需要先理解代理模式(Proxy Pattern)的基本思想。
代理模式是为其他对象提供一种代理以控制对这个对象的访问。简单来说,我们使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,扩展目标对象的功能。
举个例子:你想租房子,不会直接找房东,而是找中介。中介帮你找房、谈价格,还会额外帮你验房、签合同,你只需要最终和房东签合同就行。在这个场景中:
- 房东 = 目标对象(Real Object)
- 中介 = 代理对象(Proxy)
- 租房行为 = 需要被增强的方法
代理模式的核心价值有两个:
- 控制访问:不让调用者直接接触目标对象,由代理统一管理
- 增强功能:在不修改目标对象代码的前提下,给方法加前置/后置逻辑
代理模式分为静态代理 和动态代理两种实现方式。
二、静态代理:一个"写死"的中间人
静态代理需要提前编写一个代理类,它实现与目标对象相同的接口,并在方法中调用目标对象的方法,同时添加额外逻辑。
// 1. 定义接口
public interface RentHouse {
void rent();
}
// 2. 目标对象(房东)
public class Landlord implements RentHouse {
@Override
public void rent() {
System.out.println("房东:房子租给你");
}
}
// 3. 代理对象(中介)
public class HouseProxy implements RentHouse {
private RentHouse landlord;
public HouseProxy(RentHouse landlord) {
this.landlord = landlord;
}
@Override
public void rent() {
// 前置增强
System.out.println("中介:帮你验房,确认水电没问题");
// 调用目标方法
landlord.rent();
// 后置增强
System.out.println("中介:帮你签租房合同");
}
}
静态代理的致命缺陷:一个接口对应一个代理类,接口方法一变,代理类也要改。当需要代理的对象数量增多时,代理类会爆炸式增长,代码重复、维护成本高。正因为如此,日常开发中几乎看不到使用静态代理的场景。
三、动态代理:运行时生成的"影分身"
动态代理在程序运行时自动生成代理类,无需手动编写。它真正实现了"一次编写,处处生效"。
想象一下,明星(真实对象)很忙,不能亲自处理所有事。于是他找了个经纪人(代理),对外说:"有事找我!"粉丝以为在跟明星打交道,其实是经纪人在接电话、谈合同、安排行程。
Java 中有两种主流动态代理方式:
3.1 JDK 动态代理(基于接口)
JDK 动态代理是Java原生支持的代理方式,核心三件套:
-
接口:规定代理对象的行为
-
真实类:实现接口的目标对象
-
InvocationHandler:定义代理逻辑的处理者
// 1. 定义接口
public interface UserService {
void saveUser(String name);
String getUser(int id);
}// 2. 目标类实现接口
public class UserServiceImpl implements UserService {
@Override
public void saveUser(String name) {
System.out.println("正在保存用户:" + name);
}@Override public String getUser(int id) { System.out.println("正在查询用户:" + id); return "用户" + id; }}
// 3. InvocationHandler 拦截所有方法调用
public class LoggingHandler implements InvocationHandler {
private Object target;public LoggingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 System.out.println("【代理拦截】开始执行:" + method.getName()); long start = System.currentTimeMillis(); // 反射调用真实方法 Object result = method.invoke(target, args); // 后置增强 long cost = System.currentTimeMillis() - start; System.out.println("【代理拦截】执行完成,耗时:" + cost + "ms"); return result; }}
// 4. 创建代理对象
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LoggingHandler(target)
);
proxy.saveUser("张三"); // 自动被拦截增强
关键限制 :JDK 动态代理只能代理实现了接口的类。原因是代理对象需要实现与目标对象相同的接口,而 Java 不支持多继承。
3.2 CGLIB 动态代理(基于继承)
CGLIB(Code Generation Library)是一个高性能的字节码生成库,它通过继承目标类来创建代理,无需目标类实现接口。
// 1. 目标类(可以不实现接口)
public class ProductService {
public void addProduct(String name) {
System.out.println("添加商品:" + name);
}
}
// 2. MethodInterceptor 定义拦截逻辑
public class LoggingInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("【CGLIB拦截】开始执行:" + method.getName());
long start = System.currentTimeMillis();
// 调用父类方法
Object result = proxy.invokeSuper(obj, args);
long cost = System.currentTimeMillis() - start;
System.out.println("【CGLIB拦截】执行完成,耗时:" + cost + "ms");
return result;
}
}
// 3. 创建代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ProductService.class);
enhancer.setCallback(new LoggingInterceptor());
ProductService proxy = (ProductService) enhancer.create();
proxy.addProduct("iPhone"); // 自动被拦截增强
CGLIB 通过字节码技术(ASM框架)为目标类创建子类,在子类中重写目标方法并在调用前后插入增强逻辑。
CGLIB 的限制:
- 无法代理
final修饰的方法(无法被子类重写) - 创建代理对象的时间比 JDK 方式长,但运行性能更高
3.3 JDK 动态代理 vs CGLIB 对比
| 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 实现原理 | 基于接口,通过反射调用目标方法 | 基于继承,通过字节码生成子类 |
| 前置条件 | 目标类必须实现至少一个接口 | 目标类无需实现接口 |
| 核心API | Proxy + InvocationHandler | Enhancer + MethodInterceptor |
| 创建速度 | 快 | 慢(需要生成字节码) |
| 运行性能 | 较慢(反射调用) | 快(直接调用,约10倍性能差) |
| final 方法 | 可以代理(接口方法不能是 final) | 无法代理 |
| 依赖 | JDK 内置 | 第三方库 |
选择建议:
- 对于单例对象 或实例池中的对象,优先用 CGLIB(运行性能更好)
- 对于频繁创建代理对象的场景,优先用 JDK 动态代理(创建速度更快)
- Spring Boot 2.x 之后,默认使用 CGLIB 代理
四、AOP 核心概念详解
AOP(Aspect Oriented Programming,面向切面编程)是 Spring 核心两大思想之一(另一个是 IoC)。它允许在不修改原有业务代码的前提下,对方法进行增强,统一处理日志、事务、权限、监控等横切逻辑。
4.1 为什么需要 AOP?
在传统 OOP(面向对象编程)中,处理日志、权限、事务这类横切关注点(即多个模块都需要的功能,但与业务逻辑本身不直接相关)时,往往需要在每个方法中重复编写相同的代码,导致:
- 代码重复率高(可达 60% 以上)
- 维护成本极高(一个改动要改几十个地方)
- 业务代码与通用逻辑耦合严重
AOP 正是为了解决这个问题而诞生------将这些重复逻辑抽离出来,做成一个"切面",自动织入到目标方法中。
4.2 AOP 核心概念(一图胜千言)
AOP 有 7 个核心概念,理解它们是掌握 AOP 的关键:
| 概念 | 英文 | 说明 | 生活类比 |
|---|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,把通用功能抽离成一个类 | 小区的保安系统 |
| 连接点 | Join Point | 程序中所有可以被拦截的方法执行点 | 所有人进出大门的时刻 |
| 切入点 | Pointcut | 通过表达式匹配到的真正需要被增强的方法 | 只检查没有门禁卡的外卖员 |
| 通知/增强 | Advice | 在切入点执行的逻辑(前置/后置/环绕等) | 登记信息、联系业主、决定是否放行 |
| 目标对象 | Target | 被代理的业务对象 | 小区里的住户 |
| 织入 | Weaving | 将切面应用到目标对象的过程 | 保安检查并放行的过程 |
| 引入 | Introduction | 动态为目标类添加方法或字段(不常用) | --- |
通俗理解:切面 = 切入点 + 通知。切面类里定义了什么方法需要被增强(切入点表达式),以及增强的具体逻辑是什么(通知方法)。
4.3 五种通知类型
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before |
目标方法执行前 |
| 后置通知 | @After |
目标方法执行后(无论是否异常) |
| 返回通知 | @AfterReturning |
目标方法正常返回后 |
| 异常通知 | @AfterThrowing |
目标方法抛出异常后 |
| 环绕通知 | @Around |
包裹目标方法,可控制执行前后 |
其中 @Around 最强大,可以完全控制目标方法的执行流程,但需要手动调用 joinPoint.proceed() 来触发原始方法。
4.4 切入点表达式
最常用的 execution 表达式语法:
execution(修饰符? 返回值类型 包名.类名.方法名(参数列表))
示例:
// 匹配 service 包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
// 匹配 Controller 包下所有 public 方法
@Pointcut("execution(public * com.example.controller.*.*(..))")
// 匹配带有 @Log 注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")
五、动态代理与 AOP 的关系
一句话概括:AOP 是思想,动态代理是实现这个思想的底层技术。
5.1 AOP 底层原理就是动态代理
Spring AOP 的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。
Spring AOP 使用了两种动态代理技术:
- JDK 动态代理:当目标对象实现了接口时使用
- CGLIB 动态代理:当目标对象没有实现接口时使用
5.2 Spring 如何选择代理方式?
Spring 代理选择的默认逻辑:
// Spring 内部 DefaultAopProxyFactory 的简化逻辑
public AopProxy createAopProxy(AdvisedSupport config) {
if (config.isProxyTargetClass() || !hasInterfaces(targetClass)) {
// 强制使用 CGLIB 或没有接口时
return new CglibAopProxy(config);
} else {
// 有接口时默认使用 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
}
- Spring MVC:默认使用 JDK 动态代理,只有目标类没有实现接口时才使用 CGLIB
- Spring Boot:2.x 版本后默认使用 CGLIB
注意 :
@Transactional、@Async、@Cacheable等注解也都属于 AOP 的一种应用。
5.3 从动态代理到 AOP 的层次关系
┌─────────────────────────────────────────────┐
│ AOP(面向切面编程) │
│ @Aspect、@Before、@Around、@Pointcut... │
├─────────────────────────────────────────────┤
│ Spring AOP 框架层 │
│ ProxyFactory、Advisor、AdvisedSupport │
├─────────────────────────────────────────────┤
│ 动态代理技术层 │
│ JDK 动态代理(接口) CGLIB(继承) │
├─────────────────────────────────────────────┤
│ Java 反射 / ASM │
│ 底层字节码操作 │
└─────────────────────────────────────────────┘
- 动态代理负责在运行时生成代理对象,拦截方法调用
- AOP 基于动态代理,将切面逻辑(通知)织入到目标方法的执行流程中
- 你写的
@Aspect注解,最终都会被 Spring 翻译成动态代理对象
六、常见问题与避坑指南
6.1 AOP 注解失效问题
当同一个类内部,一个方法(不带注解)调用另一个带 AOP 注解的方法时,AOP 注解会失效。
原因 :内部调用直接通过 this.method() 执行,绕过了代理对象。
解决方案:
// 1. 通过 @Autowired 注入自己(Spring 4.3+)
@Autowired
private UserService self;
public void methodA() {
self.methodB(); // 走代理
}
// 2. 使用 AopContext.currentProxy()
((UserService) AopContext.currentProxy()).methodB();
// 3. 将 methodB 抽取到另一个 Bean 中
6.2 final 方法无法被代理
- JDK 动态代理:可以代理
final方法(因为接口方法不能是final) - CGLIB 动态代理:无法代理
final方法(因为不能被子类重写)
建议在设计需要被增强的方法时,避免使用 final 修饰。
6.3 性能考量
- CGLIB 创建代理对象的时间比 JDK 方式约多 8 倍,但运行性能高约 10 倍
- 对于单例 Bean,CGLIB 更合适(一次创建,多次使用)
- 对于频繁创建的场景,JDK 方式更合适
Spring Boot 2.x 之后默认使用 CGLIB,实际上对日常开发影响不大,无需刻意调整。
七、总结
核心知识点回顾
| 知识点 | 核心要点 |
|---|---|
| 代理模式 | 为对象提供替身,控制访问并增强功能 |
| 静态代理 | 提前编写代理类,灵活性和扩展性差 |
| JDK 动态代理 | 基于接口 + 反射,要求目标类实现接口 |
| CGLIB 动态代理 | 基于继承 + 字节码,目标类无需接口 |
| AOP 概念 | 切面、连接点、切入点、通知、目标对象、织入 |
| 二者关系 | 动态代理是 AOP 的底层实现技术 |
一条清晰的理解链
代理模式 → 动态代理 (解决静态代理"类爆炸"问题)→ AOP (基于动态代理的编程思想)→ Spring AOP(开箱即用的 AOP 框架)