什么是代理模式?应用场景有哪些?
面试考察点
-
概念理解:面试官不仅仅是想知道代理模式的定义,更是想知道你是否理解它的核心思想——"不直接访问目标对象,通过代理间接访问",以及为什么要这么设计。
-
技术深度:能否区分静态代理和动态代理,能否讲清楚 JDK 动态代理和 CGLIB 的底层原理差异。这块能答好的候选人不多,答好了就是加分项。
-
框架关联:面试官特别想看你是否能把代理模式和 Spring AOP、MyBatis Mapper、RPC 远程调用等实际框架联系起来,证明你不止停留在理论层面。
核心答案
一句话定义:代理模式就是给目标对象提供一个 "替身",由替身控制对目标对象的访问。客户端不直接和目标对象打交道,而是通过代理对象间接访问。
打个生活中的比方:你要租房,不直接找房东,而是找中介。中介就是 "代理",房东就是 "目标对象"。中介可以在租房前后加一些额外操作(比如带看房、签合同、收中介费),而你只需要和中介打交道就行。
上面的图展示了代理模式的核心结构。关键角色有三个:
Subject(公共接口):定义了代理和目标对象的共同行为,客户端面向接口编程RealSubject(目标对象):真正干活的那个,也就是 "房东"Proxy(代理对象):持有目标对象的引用,在调用目标方法前后可以增加额外逻辑,也就是 "中介"
核心思路就是:代理和目标对象实现同一个接口,代理内部持有目标的引用,客户端只跟代理打交道。
深度解析
一、静态代理
静态代理的 "静态" 体现在:代理类在编译期就确定了,需要手动编写代理类。
// 公共接口
public interface UserService {
void addUser(String name);
}
// 目标对象
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
// 代理对象(手动编写)
public class UserServiceProxy implements UserService {
private UserService target; // 持有目标对象的引用
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String name) {
System.out.println("[日志] 开始添加用户..."); // 前置增强
target.addUser(name); // 调用目标方法
System.out.println("[日志] 添加用户完成"); // 后置增强
}
}
// 使用
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("犬小哈");
输出:
[日志] 开始添加用户...
添加用户:犬小哈
[日志] 添加用户完成
静态代理的缺点很明显——一个接口就得写一个代理类,如果项目里有 50 个 Service,你就得写 50 个代理类,这谁顶得住?而且接口一旦增加方法,目标类和代理类都得改。
二、JDK 动态代理
动态代理的 "动态" 体现在:代理类在运行时由 JVM 动态生成,不需要手动编写。JDK 提供了 java.lang.reflect.Proxy 来实现。
public class JdkProxyFactory {
private Object target;
public JdkProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 目标类实现的接口
new InvocationHandler() { // 调用处理器
@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 代理] 执行完成:" + method.getName());
return result;
}
}
);
}
}
// 使用
UserService target = new UserServiceImpl();
UserService proxy = (UserService) new JdkProxyFactory(target).getProxyInstance();
proxy.addUser("犬小哈");
JDK 动态代理的核心原理:
- 通过
Proxy.newProxyInstance()在运行时动态生成一个代理类的.class文件 - 这个代理类实现了目标类所实现的所有接口
- 代理类内部通过
InvocationHandler的invoke()方法来转发请求给目标对象
JDK 动态代理有一个限制:目标类必须实现接口。如果目标类没有实现任何接口,JDK 动态代理就用不了。这个限制在面试中经常被问到。
三、CGLIB 动态代理
CGLIB(Code Generation Library)采用的方式不同——它通过字节码技术生成目标类的子类来实现代理,不需要目标类实现接口。
public class CglibProxyFactory implements MethodInterceptor {
public Object getProxyInstance(Class<?> targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass); // 设置父类(目标类)
enhancer.setCallback(this); // 设置回调
return enhancer.create(); // 生成子类代理对象
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[CGLIB 代理] 开始执行:" + method.getName());
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("[CGLIB 代理] 执行完成:" + method.getName());
return result;
}
}
// 使用(注意:UserServiceImpl 不需要实现任何接口)
UserService proxy = (UserService) new CglibProxyFactory().getProxyInstance(UserServiceImpl.class);
proxy.addUser("犬小哈");
CGLIB 也有一个限制:因为是基于继承的,所以不能代理 final 修饰的类和方法。
四、JDK 动态代理 vs CGLIB 对比
这是面试中的 "高频考点",必须记住:
| 对比项 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 基于接口,代理类实现目标接口 | 基于继承,代理类继承目标类 |
| 是否需要接口 | ✅ 必须 | ❌ 不需要 |
final 类/方法 | 不影响 | ❌ 无法代理 |
| 性能 | 生成代理快,调用稍慢 | 生成代理慢,调用更快 |
| Spring 默认策略 | 目标有接口时优先使用 | 目标无接口时使用 |
Spring 在 4.x 之后默认使用 CGLIB(spring.aop.proxy-target-class=true),之前版本默认使用 JDK 动态代理。Spring Boot 2.x 起默认也是 CGLIB。
五、实际应用场景
这块面试官特别爱问,因为能答出 "理论" 的人多,能结合实际场景的人少。
1. Spring AOP
Spring AOP 是代理模式最经典的应用。@Transactional、@Async、@Cacheable 这些注解背后的原理全都是动态代理。比如 @Transactional:
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 业务逻辑
}
}
Spring 会为 OrderService 生成一个代理对象,createOrder() 执行前后自动加上事务开启和提交/回滚的逻辑。你自己完全不用写事务管理的代码,代理对象帮你搞定了。
2. MyBatis Mapper 代理
@Mapper
public interface UserMapper {
User selectById(Long id);
}
你只写了个接口,连实现类都没有,为什么能直接调用?因为 MyBatis 用 JDK 动态代理给接口生成了代理对象,invoke() 方法里根据映射文件执行 SQL 并返回结果。
3. RPC 远程调用
Dubbo、Feign 这类 RPC 框架,你调用一个远程方法就像调用本地方法一样:
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getById(@PathVariable Long id);
}
背后的原理也是动态代理。代理对象的 invoke() 里帮你在网络层完成序列化、发送请求、反序列化响应这一整套流程。
4. 日志 / 权限 / 性能监控
在方法执行前后记录日志、校验权限、统计耗时,都可以通过代理统一处理,而不是在每个方法里重复写。
面试高频追问
-
JDK 动态代理和 CGLIB 的区别是什么?
- JDK 基于接口,CGLIB 基于继承。JDK 要求目标类必须实现接口,CGLIB 不能代理
final类。详见上方对比表格。
- JDK 基于接口,CGLIB 基于继承。JDK 要求目标类必须实现接口,CGLIB 不能代理
-
Spring AOP 用的是哪种代理?
- 默认情况下,目标类实现了接口就用 JDK 动态代理,没实现接口就用 CGLIB。Spring Boot 2.x 起默认全部走 CGLIB。
-
为什么 JDK 动态代理必须实现接口?
- 因为 JDK 生成的代理类继承了
Proxy类,Java 不支持多继承,所以只能通过实现接口来代理。
- 因为 JDK 生成的代理类继承了
-
CGLIB 为什么不能代理
final方法?- CGLIB 通过生成子类来代理,
final方法无法被子类重写,自然就无法增强。
- CGLIB 通过生成子类来代理,
常见面试变体
- "手写一个动态代理的例子"
- "Spring AOP 的底层原理是什么?"
- "MyBatis 的 Mapper 接口没有实现类,为什么能直接调用?"
- "动态代理和装饰器模式有什么区别?"
记忆口诀
核心思想:不碰真人找中介,代理帮你加逻辑。
JDK vs CGLIB:JDK 靠接口,CGLIB 靠继承;一个要接口,一个怕 final。
应用场景:AOP 切面、Mapper 代理、RPC 调用、日志权限。
总结
代理模式的核心就是 "找个替身控制访问"。面试时先说概念,再对比静态/动态代理的区别,重点展开 JDK 动态代理和 CGLIB 的原理差异,最后结合 Spring AOP、MyBatis、RPC 三个实际场景举例。这条线拉完,面试官基本没有追问的空间了。
