Java设计模式之代理模式
文章目录
前言
我们在日常开发中经常遇到这样的需求:在不修改原始类代码的前提下,为其添加额外的功能,比如记录日志、权限校验、性能统计等。直接侵入原有代码显然不可取,而代理模式(Proxy Pattern)正是解决这类问题的利器。Java中的代理模式分为三种实现方式:静态代理 、JDK动态代理和CGLIB代理。
代理模式的核心价值:它体现了面向对象设计中的"单一职责原则"和"开闭原则"。业务类只关心业务逻辑,日志、事务、权限等横切关注点交给代理类处理------这就是AOP(面向切面编程)的思想基础。可以说,不理解代理模式,就不可能真正理解Spring AOP、MyBatis Mapper、RPC框架等Java生态的核心组件。
一、静态代理
代理对象与目标对象实现同一接口,在代理对象中调用目标对象并添加增强逻辑:
java
// 公共接口
public interface UserService {
void addUser(String username);
void deleteUser(String username);
}
// 目标对象(真实业务)
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username) {
System.out.println("添加用户: " + username);
}
@Override
public void deleteUser(String username) {
System.out.println("删除用户: " + username);
}
}
// 静态代理类
public class UserServiceProxy implements UserService {
private UserService target; // 持有目标对象引用
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String username) {
System.out.println("[日志] 开始添加用户...");
long start = System.currentTimeMillis();
target.addUser(username); // 调用目标方法
long end = System.currentTimeMillis();
System.out.println("[日志] 添加完成,耗时: " + (end - start) + "ms");
}
@Override
public void deleteUser(String username) {
System.out.println("[权限] 校验删除权限...");
target.deleteUser(username);
}
}
// 使用
public class Client {
public static void main(String[] args) {
UserService service = new UserServiceProxy(new UserServiceImpl());
service.addUser("张三");
service.deleteUser("张三");
}
}
优点 :代码清晰,可以对不同方法做不同的增强。
缺点:每个需要代理的类都要手写一个代理类,接口增加方法时代理类也要同步修改,维护成本高。
静态代理还有价值吗? 虽然动态代理更灵活,但静态代理在某些场景仍有其独特优势:当不同方法需要完全不同的增强逻辑时(如addUser需要日志,deleteUser需要权限校验),静态代理可以针对每个方法写不同的前置/后置逻辑,代码意图非常明确。动态代理的InvocationHandler中虽然也能通过method.getName()来区分,但会变成一个大的if-else。所以静态代理适合代理方法少、增强逻辑各异的场景,动态代理适合方法多、增强逻辑统一的场景。
二、JDK动态代理
利用java.lang.reflect.Proxy和InvocationHandler在运行时动态生成代理类,无需手写代理类:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 通用的日志处理器
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 {
String methodName = method.getName();
System.out.println("[开始] " + methodName);
long start = System.currentTimeMillis();
// 反射调用目标方法
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("[结束] " + methodName
+ " 耗时: " + (end - start) + "ms");
return result;
}
// 工厂方法:创建代理对象
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target, Class<T> interfaceType) {
return (T) Proxy.newProxyInstance(
interfaceType.getClassLoader(),
new Class<?>[]{interfaceType},
new LogInvocationHandler(target)
);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
UserService service = LogInvocationHandler.createProxy(
new UserServiceImpl(), UserService.class);
service.addUser("李四");
service.deleteUser("李四");
}
}
运行原理 :Proxy.newProxyInstance()在JVM内存中动态生成一个代理类的字节码,该代理类实现了指定接口。方法调用时会路由到InvocationHandler.invoke()。
限制 :JDK动态代理要求目标对象必须实现至少一个接口 ,因为生成的代理类继承自Proxy(Java不支持多继承),只能通过实现接口来代理。
深入理解Proxy类 :你可以在代码中添加System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true")(Java 8用sun.misc.ProxyGenerator.saveGeneratedFiles),运行后JDK会将生成的代理类字节码保存到磁盘。反编译后你会发现,代理类实现了你指定的所有接口,每个方法内部都调用了InvocationHandler.invoke()。这种"不可见"的代理是很多人觉得动态代理"神秘"的原因------其实不过是用字节码技术帮我们自动生成了静态代理类。
三、CGLIB代理
当目标对象没有实现接口时,使用CGLIB(Code Generation Library)通过继承方式生成代理子类:
java
// 没有实现接口的普通类
public class OrderService {
public void createOrder(String orderId) {
System.out.println("创建订单: " + orderId);
}
public void cancelOrder(String orderId) {
System.out.println("取消订单: " + orderId);
}
}
// CGLIB代理(需要引入cglib依赖)
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyFactory implements MethodInterceptor {
private Object target;
public CglibProxyFactory(Object target) {
this.target = target;
}
// 创建代理对象
public Object getProxyInstance() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass()); // 设置父类
enhancer.setCallback(this); // 设置回调
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("[事务] 开启事务...");
Object result = method.invoke(target, args);
System.out.println("[事务] 提交事务...");
return result;
}
public static void main(String[] args) {
OrderService proxy = (OrderService)
new CglibProxyFactory(new OrderService()).getProxyInstance();
proxy.createOrder("ORD-20260529");
}
}
工作原理:CGLIB底层使用ASM框架操作字节码,生成目标类的子类,通过方法重写实现拦截。
限制 :无法代理final类和方法(因为final不可被继承/重写)。Spring在目标有接口时默认用JDK动态代理,没有接口时用CGLIB。
CGLIB代理的性能陷阱 :虽然CGLIB创建代理对象的速度比JDK慢(因为需要生成字节码),但方法调用的执行效率比JDK的反射调用高。不过从Java 7开始,JDK动态代理使用了MethodHandle优化,反射性能已大幅提升。如果你的应用频繁创建代理对象,JDK动态代理可能更合适;如果是创建少数长生命周期的代理对象并高频调用其方法,CGLIB可能更优。当然,在绝大多数业务场景中,两者的性能差异可以忽略不计------选型更应该关注"有没有接口"这个硬性条件。
四、Spring AOP中的代理
Spring AOP将代理选择自动化,开发者只需写切面逻辑:
java
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[AOP前置] " + joinPoint.getSignature());
Object result = joinPoint.proceed();
System.out.println("[AOP后置] " + joinPoint.getSignature());
return result;
}
}
Spring会根据条件自动选择JDK动态代理(有接口时)或CGLIB(无接口时)。也可以配置proxy-target-class=true强制使用CGLIB。
五、三种代理方式对比
| 特性 | 静态代理 | JDK动态代理 | CGLIB代理 |
|---|---|---|---|
| 是否需手写代理类 | 是 | 否 | 否 |
| 是否需实现接口 | 是 | 是 | 否 |
| 代理原理 | 硬编码 | 接口反射 | 子类继承 |
| 性能(调用) | 高 | 略低(反射) | 高 |
| 性能(创建) | 无需创建 | 中 | 低 |
| final方法 | 可代理 | 可代理 | 不可代理 |
总结
代理模式是现代Java框架的基石,Spring AOP和MyBatis Mapper都是代理模式的典型应用。三种实现各有适用场景:静态代理 适合代理类少、逻辑固定的场景;JDK动态代理 是接口驱动开发的最佳拍档;CGLIB弥补了无接口场景的空白。理解代理模式,对于深入掌握Spring框架至关重要。
一个实用的面试答题框架 :当面试官问"讲讲代理模式",建议你按这个思路回答------先说"代理模式的核心目的是控制对象访问和增强功能",再分三种实现讲:静态代理怎么手写,JDK动态代理怎么用Proxy.newProxyInstance,CGLIB怎么通过继承生成子类。然后自然过渡到Spring AOP是如何在这两者之间自动选择的。最后提一句"实际开发中我们通常通过Spring AOP的@Aspect声明式使用代理,很少手动编码"。这样的回答既展现了知识广度,又体现了实战经验。
✅ 亮点总结
- 静态代理 → JDK动态代理 → CGLIB代理的完整演进,覆盖了代理模式的三种实现形态
- Spring AOP自动代理选择机制(有接口=JDK,无接口=CGLIB)与
proxy-target-class配置详解 - MyBatis Mapper代理------只写接口不写实现类,动态代理在背后生成代理对象执行SQL
- 三种代理方式的全维度对比表(手写成本、接口要求、代理原理、性能差异、final方法限制)
- 代理模式与AOP的天然契合------将横切关注点(日志、事务、权限)从业务代码中剥离
适用场景
- 微服务间调用------RPC框架用动态代理将远程调用伪装成本地方法调用
- 延迟加载------Hibernate的懒加载代理对象,只在真正使用时才发起数据库查询
- 权限控制------在代理层校验用户权限,拒绝非法请求进入核心业务逻辑
扩展方向
- Spring AOP源码跟踪 :
JdkDynamicAopProxy.invoke()→InterceptorChain→ProceedingJoinPoint的完整调用链 - AspectJ编译期织入:对比Spring AOP运行时代理,了解编译期/类加载期织入的差异(推荐阅读下一篇:Java设计模式之观察者模式)
- Java动态代理的局限性:只能代理接口,了解ByteBuddy等新一代代理框架的优势