代理模式:静态代理、JDK动态代理、CGLIB动态代理,以及AOP实现原理与延迟加载
代理模式,说白了就是「找人替你办事」。你想想看,生活中我们找中介租房、找律师打官司,本质上都是代理。在软件里也一样——我们不想直接操作某个对象,或者想在操作前后加点私货,那就找个代理对象来替我们干。
我个人习惯把代理模式分成两类:静态代理和动态代理。动态代理里又分JDK动态代理和CGLIB动态代理。嗯,咱们一个一个来聊。
1. 静态代理:最朴素的代理方式
静态代理,就是你在编译期就把代理类写死了。代理类和目标类实现同一个接口,代理类里持有目标类的引用,然后在方法调用前后加点料。
我在项目中遇到过这样一个场景:要给所有Service层的方法加日志。最笨的办法就是每个方法里都写一遍日志代码,那太蠢了。用静态代理,写一个代理类,统一处理日志,代码干净多了。
// 接口
public interface UserService {
void addUser(String name);
}
// 目标类
public class UserServiceImpl implements UserService {
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
// 静态代理类
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
public void addUser(String name) {
System.out.println("[日志] 开始添加用户");
target.addUser(name);
System.out.println("[日志] 添加用户结束");
}
}
静态代理的缺点很明显:每个目标类都要写一个代理类。如果系统里有几十个接口,那代理类的数量就爆炸了。而且接口一旦变化,代理类也得跟着改,维护成本很高。
2. JDK动态代理:反射的优雅应用
JDK动态代理,说白了就是利用Java的反射机制,在运行时动态生成代理类。你不需要手动写代理类,只需要提供一个InvocationHandler,所有的方法调用都会经过这个handler。
为什么JDK动态代理要求目标类必须实现接口?因为JDK生成的代理类默认继承了Proxy类,Java是单继承,所以只能通过接口来扩展。这是它的硬伤。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[日志] 调用方法:" + method.getName());
Object result = method.invoke(target, args);
System.out.println("[日志] 方法调用结束");
return result;
}
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogHandler(target)
);
}
}
// 使用
UserService proxy = (UserService) LogHandler.createProxy(new UserServiceImpl());
proxy.addUser("张三");
你想想看,这里最妙的地方是什么?代理逻辑和业务逻辑完全解耦。日志、事务、权限这些横切关注点,都可以塞进InvocationHandler里,业务类根本不需要知道代理的存在。
3. CGLIB动态代理:没有接口也能代理
如果目标类没有实现接口怎么办?JDK动态代理就无能为力了。这时候CGLIB就派上用场了。CGLIB通过继承的方式生成目标类的子类作为代理,所以不需要接口。
嗯,这里要注意:CGLIB不能代理final类或final方法,因为继承不了。我在项目中就踩过这个坑——一个工具类被标记成了final,CGLIB直接报错,最后只能改成JDK动态代理或者手动改类设计。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor {
private Object target;
public CglibProxy(Object target) {
this.target = target;
}
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
System.out.println("[CGLIB] 方法前处理");
Object result = proxy.invoke(target, args);
System.out.println("[CGLIB] 方法后处理");
return result;
}
public static Object createProxy(Object target) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new CglibProxy(target));
return enhancer.create();
}
}
// 使用
UserServiceNoInterface proxy = (UserServiceNoInterface)
CglibProxy.createProxy(new UserServiceNoInterface());
proxy.addUser("李四");
proxy-target-class="true"即可。我建议在项目里统一用一种方式,避免混用带来的调试困扰。
4. 三种代理方式的对比
| 特性 | 静态代理 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|---|
| 是否需要接口 | 是 | 是 | 否 |
| 代理类生成时机 | 编译期 | 运行时 | 运行时 |
| 性能(首次调用) | 快 | 较慢(需生成字节码) | 较慢(需生成字节码) |
| 性能(后续调用) | 快 | 快 | 快 |
| 能否代理final类/方法 | 能 | 能(通过接口) | 不能 |
| 代码维护成本 | 高 | 低 | 低 |
5. AOP实现原理:代理模式的集大成者
AOP(面向切面编程),说白了就是代理模式在框架层面的应用。Spring AOP的核心就是动态代理。你写的@Before、@After、@Around注解,最终都会被转换成代理对象的方法拦截逻辑。
我记得有一次面试,面试官问我:「Spring AOP是怎么把切面织入到目标方法里的?」其实答案很简单:Spring在Bean初始化阶段,发现这个Bean有匹配的切面,就会用动态代理包装它。后续每次调用,实际上都是在调用代理对象的方法。
AOP的术语虽然多(切点、通知、连接点、织入),但核心逻辑就三步:
- 定义切点:哪些方法需要被拦截
- 定义通知:拦截后做什么(日志、事务、权限等)
- 织入:把通知应用到切点上,生成代理对象
// Spring AOP示例
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("[AOP] 方法执行前:" + joinPoint.getSignature().getName());
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("[AOP] 方法执行后:" + joinPoint.getSignature().getName());
}
}
6. 延迟加载:代理模式的经典应用
延迟加载(Lazy Loading),说白了就是「用到的时候再加载」。这在ORM框架里特别常见,比如Hibernate的懒加载。你查询一个User对象,它的orders属性不会立即从数据库加载,而是等你真正调用user.getOrders()时才去查。
怎么实现的?代理对象。Hibernate返回给你的User对象,其实是一个代理对象。这个代理对象里持有真正的User对象的引用,但orders属性是空的。当你调用getOrders()时,代理对象会检查:如果orders还没加载,就去数据库查;如果已经加载了,直接返回。
// 模拟延迟加载的代理
public class LazyUserProxy implements User {
private User realUser;
private boolean loaded = false;
public List<Order> getOrders() {
if (!loaded) {
System.out.println("从数据库加载orders...");
realUser = loadFromDatabase();
loaded = true;
}
return realUser.getOrders();
}
private User loadFromDatabase() {
// 实际数据库查询逻辑
return new RealUser();
}
}
你想想看,如果没有延迟加载,一个页面可能只需要展示用户基本信息,却要把所有关联数据都查出来,性能得多差。延迟加载配合代理模式,按需加载,节省资源,这是大型系统的必备技巧。
7. 知识体系总览
下面这张图总结了代理模式的核心知识体系,从静态代理到动态代理,再到AOP和延迟加载,一图看明白。
代理模式的核心思想,说白了就是控制访问。你不想让调用者直接操作真实对象,或者想在操作前后加点逻辑,那就找个代理。静态代理适合简单场景,动态代理适合大规模系统,AOP和延迟加载则是代理模式在框架层面的经典应用。
我个人觉得,代理模式是设计模式里「性价比」最高的几个之一。理解它,你不仅能写出更优雅的代码,还能深入理解Spring、Hibernate这些主流框架的底层原理。嗯,值得花时间好好消化。