代理模式:静态代理、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("[日志] 添加用户结束");
    }
}

静态代理的缺点很明显:每个目标类都要写一个代理类。如果系统里有几十个接口,那代理类的数量就爆炸了。而且接口一旦变化,代理类也得跟着改,维护成本很高。

避坑指南:我曾经在一个老项目里看到有人用静态代理给所有DAO层加事务,结果接口一多,代理类文件堆成山。后来重构时全换成了动态代理,代码量直接砍掉三分之二。

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("李四");
个人经验:Spring AOP默认的策略是:如果目标类实现了接口,就用JDK动态代理;否则用CGLIB。但你可以强制Spring使用CGLIB,设置proxy-target-class="true"即可。我建议在项目里统一用一种方式,避免混用带来的调试困扰。

4. 三种代理方式的对比

特性 静态代理 JDK动态代理 CGLIB动态代理
是否需要接口
代理类生成时机 编译期 运行时 运行时
性能(首次调用) 较慢(需生成字节码) 较慢(需生成字节码)
性能(后续调用)
能否代理final类/方法 能(通过接口) 不能
代码维护成本

5. AOP实现原理:代理模式的集大成者

AOP(面向切面编程),说白了就是代理模式在框架层面的应用。Spring AOP的核心就是动态代理。你写的@Before@After@Around注解,最终都会被转换成代理对象的方法拦截逻辑。

我记得有一次面试,面试官问我:「Spring AOP是怎么把切面织入到目标方法里的?」其实答案很简单:Spring在Bean初始化阶段,发现这个Bean有匹配的切面,就会用动态代理包装它。后续每次调用,实际上都是在调用代理对象的方法。

AOP的术语虽然多(切点、通知、连接点、织入),但核心逻辑就三步:

  1. 定义切点:哪些方法需要被拦截
  2. 定义通知:拦截后做什么(日志、事务、权限等)
  3. 织入:把通知应用到切点上,生成代理对象
// 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());
    }
}
核心要点:AOP的本质就是「代理模式 + 反射 + 注解解析」。你理解了代理模式,AOP的原理就懂了一大半。

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();
    }
}

你想想看,如果没有延迟加载,一个页面可能只需要展示用户基本信息,却要把所有关联数据都查出来,性能得多差。延迟加载配合代理模式,按需加载,节省资源,这是大型系统的必备技巧。

避坑指南:我曾经在一个项目里遇到N+1查询问题,就是因为延迟加载使用不当。循环遍历用户列表时,每个用户都触发一次订单查询,性能直接崩了。解决方案是:要么用JOIN FETCH一次性加载,要么用批量抓取(Batch Fetch)。延迟加载虽好,但别滥用。

7. 知识体系总览

下面这张图总结了代理模式的核心知识体系,从静态代理到动态代理,再到AOP和延迟加载,一图看明白。

代理模式知识体系 代理模式 静态代理 动态代理 应用场景 编译期生成代理类 需实现接口 JDK动态代理 CGLIB动态代理 运行时生成字节码 AOP面向切面编程 延迟加载 远程代理(RPC) 核心思想:控制访问、增强功能、解耦横切关注点

代理模式的核心思想,说白了就是控制访问。你不想让调用者直接操作真实对象,或者想在操作前后加点逻辑,那就找个代理。静态代理适合简单场景,动态代理适合大规模系统,AOP和延迟加载则是代理模式在框架层面的经典应用。

我个人觉得,代理模式是设计模式里「性价比」最高的几个之一。理解它,你不仅能写出更优雅的代码,还能深入理解Spring、Hibernate这些主流框架的底层原理。嗯,值得花时间好好消化。

一句话总结:代理模式就是「不直接干,找人替干」。静态代理是手写替身,JDK动态代理是反射生成替身,CGLIB是继承生成替身。AOP和延迟加载,都是这个思路的延伸。
公众号:蓝海资料掘金营,微信deep3321