Appearance
代理模式
为什么需要代理?
代理的本质是在不修改原代码的前提下,增强或控制对目标对象的访问。AOP(面向切面编程)的底层就是代理,Spring 事务、日志、权限控制都依赖它。
常见场景:
- 日志记录:记录方法调用的参数和返回值
- 性能监控:统计方法执行耗时
- 事务管理:方法执行前后开启/提交/回滚事务
- 权限校验:调用方法前检查用户权限
- 缓存:方法调用前检查缓存,有则直接返回
- 远程调用:RPC 框架通过代理隐藏网络通信细节
静态代理
手动编写代理类,代理类和目标类实现同一接口。
基本实现
java
// 共同接口
interface UserService {
void save(String name);
void delete(int id);
User findById(int id);
}
// 目标类
class UserServiceImpl implements UserService {
public void save(String name) {
System.out.println("保存用户: " + name);
}
public void delete(int id) {
System.out.println("删除用户: " + id);
}
public User findById(int id) {
System.out.println("查询用户: " + id);
return new User(id, "用户" + id);
}
}
// 静态代理类
class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
public void save(String name) {
System.out.println("=== 事务开始 ===");
try {
target.save(name);
System.out.println("=== 事务提交 ===");
} catch (Exception e) {
System.out.println("=== 事务回滚 ===");
throw e;
}
}
public void delete(int id) {
System.out.println("=== 事务开始 ===");
try {
target.delete(id);
System.out.println("=== 事务提交 ===");
} catch (Exception e) {
System.out.println("=== 事务回滚 ===");
throw e;
}
}
public User findById(int id) {
// 每个方法都要重复相同逻辑!
System.out.println("=== 事务开始 ===");
try {
return target.findById(id);
} finally {
System.out.println("=== 事务结束 ===");
}
}
}静态代理的缺点
- 代码冗余:每个方法都要写相同的增强逻辑
- 维护成本高:接口变更时,代理类也要同步修改
- 数量爆炸:N 个接口 x M 种增强 = NxM 个代理类
JDK 动态代理
JDK 动态代理在运行时动态生成代理类,无需手动编写。核心是两个组件:
java.lang.reflect.Proxy— 生成代理对象java.lang.reflect.InvocationHandler— 拦截所有方法调用
基本实现
java
interface UserService {
void save(String name);
void delete(int id);
}
class UserServiceImpl implements UserService {
public void save(String name) {
System.out.println("保存用户: " + name);
}
public void delete(int id) {
System.out.println("删除用户: " + id);
}
}
// 动态代理 — 一个 InvocationHandler 搞定所有方法
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(">>> 调用方法: " + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("<<< 方法结束: " + method.getName() + " 耗时: " + (end - start) + "ms");
return result;
}
}
// 使用
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.save("张三");
proxy.delete(1);
}
}输出:
>>> 调用方法: save
保存用户: 张三
<<< 方法结束: save 耗时: 0ms
>>> 调用方法: delete
删除用户: 1
<<< 方法结束: delete 耗时: 0msJDK 动态代理原理
Proxy.newProxyInstance() 底层做了三件事:
1. 动态生成字节码
JVM 在运行时用 ProxyGenerator.generateProxyClass() 生成一个 $Proxy0.class:
java
// 可以把生成的代理类 dump 到磁盘查看
// JDK 8:
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// JDK 11+:
// System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");生成的反编译代码大致如下:
java
public final class $Proxy0 extends Proxy implements UserService {
private static Method m3; // save
private static Method m4; // delete
private static Method m5; // findById
static {
m3 = Class.forName("UserService").getMethod("save", String.class);
m4 = Class.forName("UserService").getMethod("delete", int.class);
m5 = Class.forName("UserService").getMethod("findById", int.class);
}
public $Proxy0(InvocationHandler h) {
super(h);
}
public final void save(String name) {
super.h.invoke(this, m3, new Object[]{name});
}
public final void delete(int id) {
super.h.invoke(this, m4, new Object[]{id});
}
public final User findById(int id) {
return (User) super.h.invoke(this, m5, new Object[]{id});
}
}super.h 就是传入的 InvocationHandler,所有方法调用全部汇聚到 invoke() 里。
2. 动态加载类
用传入的 ClassLoader 把生成的字节码加载进 JVM。
3. 反射创建实例
Constructor.newInstance(handler) 反射创建代理对象,把 InvocationHandler 传进去。
JDK 动态代理的局限性
- 必须基于接口:目标类必须实现至少一个接口
- 性能开销:每个方法调用都有反射开销
- 无法代理
equals、hashCode、toString之外的 Object 方法
CGLIB 代理
CGLIB(Code Generation Library)通过继承目标类生成子类来代理,不需要接口。底层使用 ASM 字节码框架。
基本实现
java
// 目标类 — 不需要接口
class UserService {
public void save(String name) {
System.out.println("保存用户: " + name);
}
public final void delete(int id) {
System.out.println("删除用户: " + id);
}
}
// CGLIB 代理
class CglibProxy implements MethodInterceptor {
private Object target;
public CglibProxy(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println(">>> 调用方法: " + method.getName());
// 注意:用 proxy.invokeSuper() 而不是 method.invoke()
// MethodProxy 使用 FastClass 机制,比反射快
Object result = proxy.invokeSuper(obj, args);
System.out.println("<<< 方法结束: " + method.getName());
return result;
}
@SuppressWarnings("unchecked")
public static <T> T createProxy(Class<T> clazz, MethodInterceptor interceptor) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(interceptor);
return (T) enhancer.create();
}
}
// 使用
UserService proxy = CglibProxy.createProxy(UserService.class,
new CglibProxy(new UserService()));
proxy.save("张三");CGLIB 的 FastClass 机制
CGLIB 的 MethodProxy.invokeSuper() 不是用反射,而是用 FastClass 机制:
- 为目标类和代理类各生成一个 FastClass
- FastClass 为每个方法分配一个 index
- 调用时通过 index 直接定位方法,无需反射查找
这就是 CGLIB 比 JDK 动态代理快的原因。
CGLIB 的局限性
- 不能代理 final 类:final 类无法被继承
- 不能代理 final 方法:final 方法无法被重写,但不会报错,只是不增强
- 不能代理 private 方法:私有方法无法被子类访问
- 需要额外依赖:需要引入 cglib 或 spring-core(Spring 内置了 cglib)
JDK 动态代理 vs CGLIB
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 机制 | 实现接口 | 继承目标类 |
| 要求 | 必须有接口 | 不能是 final 类 |
| 生成时机 | 运行时 | 运行时 |
| 底层 | 反射 + 动态字节码 | ASM 字节码增强 |
| 方法调用 | Method.invoke() 反射 | MethodProxy.invokeSuper() FastClass |
| 性能 | 创建快,调用慢 | 创建慢,调用快 |
| final 方法 | 不受影响 | 无法代理 |
| private 方法 | 无法代理 | 无法代理 |
| 依赖 | JDK 内置 | 需要 cglib 或 spring-core |
| 代理类命名 | $Proxy0 | TargetClass$$EnhancerByCGLIB$$xxxx |
Spring AOP 的选择策略
Spring AOP 默认优先使用 JDK 动态代理,以下情况使用 CGLIB:
- 目标类没有实现任何接口
- 配置了
proxy-target-class="true"(XML)或@EnableAspectJAutoProxy(proxyTargetClass = true) - Spring Boot 从 2.0 开始默认使用 CGLIB(
spring.aop.proxy-target-class=true)
java
// 强制使用 CGLIB
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig { }代理的实际应用
简易 RPC 框架中的动态代理
java
// 客户端只需要接口,不需要实现类
interface HelloService {
String sayHello(String name);
}
// 通用 RPC 代理处理器
class RpcInvocationHandler implements InvocationHandler {
private String host;
private int port;
public RpcInvocationHandler(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 通过网络发送方法名 + 参数,等待远程返回结果
RpcRequest request = new RpcRequest(method.getDeclaringClass().getName(),
method.getName(), method.getParameterTypes(), args);
return sendRequest(host, port, request);
}
private Object sendRequest(String host, int port, RpcRequest request) {
// 省略网络通信细节...
return null;
}
}
// 使用
HelloService service = (HelloService) Proxy.newProxyInstance(
HelloService.class.getClassLoader(),
new Class[]{HelloService.class},
new RpcInvocationHandler("127.0.0.1", 8080)
);
String result = service.sayHello("World"); // 实际通过网络调用常见面试题
Q1: JDK 动态代理和 CGLIB 的区别?
JDK 动态代理基于接口,通过实现接口生成代理类;CGLIB 基于继承,通过生成目标类的子类来代理。JDK 代理创建快但调用慢(反射),CGLIB 创建慢但调用快(FastClass)。Spring AOP 默认使用 JDK 动态代理,无接口时使用 CGLIB。
Q2: JDK 动态代理为什么必须基于接口?
生成的代理类 $Proxy0 已经继承了 java.lang.reflect.Proxy,而 Java 是单继承,无法再继承目标类,只能通过实现接口来代理。
Q3: CGLIB 能代理 final 方法吗?
不能。final 方法无法被子类重写,CGLIB 生成的代理类不会覆盖 final 方法,调用时直接执行原方法逻辑,不会触发代理增强。
Q4: 静态代理和动态代理的优缺点?
静态代理在编译期确定代理关系,性能好但代码冗余、维护成本高。动态代理在运行时生成代理类,灵活、通用,但有性能开销。
Q5: 同一个代理类能否同时代理多个接口?
JDK 动态代理可以,newProxyInstance 的第二个参数是 Class<?>[],可以传入多个接口。生成的代理类会实现所有传入的接口。
Q6: Method.invoke() 和 MethodProxy.invokeSuper() 的区别?
Method.invoke() 是标准反射调用,每次调用都需要权限检查和方法查找。MethodProxy.invokeSuper() 是 CGLIB 的 FastClass 机制,通过方法 index 直接调用,避免了反射开销。
Q7: 如何让 Spring Boot 强制使用 JDK 动态代理?
配置 spring.aop.proxy-target-class=false,并确保目标类实现了接口。Spring Boot 2.0+ 默认 proxy-target-class 为 true(即默认 CGLIB)。
