16、代理模式:静态代理、动态代理(JDK与CGLIB)、AOP原理

代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常这样——找中介租房、找律师打官司、找经纪人谈代言。代码里也一样,有时候我们不想直接操作某个对象,或者想在操作前后加点「私活」,这时候代理就派上用场了。

我个人习惯把代理模式分成两类:静态代理动态代理。动态代理里又分JDK代理和CGLIB代理。嗯,咱们一个一个来聊。

16.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("[日志] 添加用户完成");
    }
}

使用的时候:

UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("张三");
静态代理的优点:简单、直观,适合代理类数量固定的场景。
静态代理的痛点:每个目标类都需要一个对应的代理类。如果系统里有100个Service,你就得写100个代理类。维护成本高,代码冗余严重。

16.2 动态代理:运行时生成代理

动态代理解决了静态代理的痛点。它不是在编译期写死代理类,而是在运行时动态生成代理对象。Java里主要有两种实现方式:JDK动态代理和CGLIB动态代理。

16.2.1 JDK动态代理

JDK动态代理基于接口实现。它要求目标类必须实现一个接口。代理对象在运行时通过java.lang.reflect.ProxyInvocationHandler生成。

我曾经接手过一个项目,里面用了大量的静态代理,光代理类就有好几十个。后来我重构时全部换成了JDK动态代理,代码量直接砍掉了一半。嗯,这里要注意:JDK动态代理只支持接口代理,如果你的类没有实现接口,那就得用CGLIB了。

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

    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("[日志] 方法 " + method.getName() + " 执行完毕");
        return result;
    }

    public static Object createProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new LogInvocationHandler(target)
        );
    }
}

使用:

UserService target = new UserServiceImpl();
UserService proxy = (UserService) LogInvocationHandler.createProxy(target);
proxy.addUser("李四");
小提示:JDK动态代理底层用了反射,性能上比静态代理略差。但在大多数业务场景下,这点性能差异完全可以忽略。如果你对性能有极致要求,可以考虑CGLIB。

16.2.2 CGLIB动态代理

CGLIB(Code Generation Library)通过继承的方式生成代理。它不需要目标类实现接口,直接对类进行代理。CGLIB在运行时生成目标类的子类,并重写父类的方法。

我记得有一次,一个第三方库的类没有实现任何接口,但我们需要给它的方法加缓存。JDK动态代理用不了,静态代理又太麻烦。最后用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 LogMethodInterceptor implements MethodInterceptor {
    private Object target;

    public LogMethodInterceptor(Object target) {
        this.target = target;
    }

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) 
            throws Throwable {
        System.out.println("[CGLIB日志] 方法 " + method.getName() + " 开始...");
        Object result = method.invoke(target, args);
        System.out.println("[CGLIB日志] 方法 " + method.getName() + " 结束");
        return result;
    }

    public static Object createProxy(Object target) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(new LogMethodInterceptor(target));
        return enhancer.create();
    }
}

使用:

UserService target = new UserServiceImpl();
UserService proxy = (UserService) LogMethodInterceptor.createProxy(target);
proxy.addUser("王五");
注意:CGLIB不能代理final类,也不能代理final方法。因为它是通过继承实现的,final类不能被继承,final方法不能被重写。

16.3 JDK vs CGLIB:怎么选?

对比维度 JDK动态代理 CGLIB动态代理
实现方式 基于接口(InvocationHandler + Proxy) 基于继承(Enhancer + MethodInterceptor)
目标类要求 必须实现接口 无接口要求,但不能是final类
性能(创建代理) 较快 较慢(需要生成字节码)
性能(调用方法) 较慢(反射调用) 较快(直接调用,无反射)
适用场景 面向接口编程 类代理、无接口场景

我个人习惯:优先用JDK动态代理。因为大部分项目都是面向接口编程的,JDK动态代理足够用了。只有当目标类没有实现接口时,才考虑CGLIB。Spring框架也是这个策略——默认使用JDK动态代理,如果目标类没有实现接口,自动切换为CGLIB。

16.4 AOP原理:代理模式的高级应用

AOP(面向切面编程),说白了就是代理模式在框架层面的封装。你想想看,AOP的核心是什么?就是在不修改原有代码的情况下,给方法增加额外的行为。这不就是代理模式干的事吗?

Spring AOP的底层实现,就是JDK动态代理和CGLIB动态代理的组合。当你定义一个@Aspect切面,配置好@Before@After等通知时,Spring在运行时为你创建代理对象,把切面逻辑织入到目标方法中。

我曾经排查过一个AOP失效的问题:一个Service方法被同一个类中的另一个方法调用,结果切面没有生效。原因很简单——Spring AOP基于代理,同一个类内部的方法调用走的是this引用,而不是代理对象。所以切面不会触发。解决方案是:要么把方法拆分到不同的Bean中,要么通过AopContext.currentProxy()获取当前代理对象。

AOP的核心三要素
  • 切面(Aspect):横切关注点的模块化,比如日志、事务
  • 通知(Advice):切面的具体行为,比如@Before、@After、@Around
  • 切入点(Pointcut):定义在哪些方法上应用通知

来看一个简单的AOP示例:

@Aspect
@Component
public class LogAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[AOP日志] 方法 " + methodName + " 即将执行");
    }

    @AfterReturning("execution(* com.example.service.*.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[AOP日志] 方法 " + methodName + " 执行完毕");
    }
}

这个切面会拦截com.example.service包下所有类的所有方法,在方法执行前后打印日志。你不需要修改任何业务代码,只需要定义这个切面,Spring会自动为你生成代理对象。

16.5 代理模式的核心知识图谱

下面这张图,帮你理清代理模式的整体脉络:

代理模式知识体系 代理模式 静态代理 动态代理 编译期生成代理类 每个目标类对应一个代理类 运行时动态生成代理 JDK动态代理 CGLIB动态代理 基于接口 基于继承 AOP(面向切面) 切面 + 通知 + 切入点 底层:JDK + CGLIB

16.6 避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 内部方法调用导致AOP失效:同一个类中的方法互相调用,不会经过代理对象。解决方案是注入自身或者使用AopContext.currentProxy()
  • CGLIB代理final方法:CGLIB无法重写final方法,如果目标类有final方法,代理不会生效。我曾经因为这个排查了半天,最后发现是一个工具类的某个方法被标记了final。
  • JDK动态代理的类型转换异常:JDK动态代理生成的代理对象只能转换为接口类型,不能转换为具体的实现类。如果你在代码里强转成实现类,会抛出ClassCastException
  • 性能陷阱:虽然动态代理很方便,但如果你的方法调用频率极高(比如每秒百万次),反射带来的性能损耗还是需要考虑的。这种情况下,可以考虑使用字节码增强技术(如ByteBuddy)或者直接使用静态代理。

代理模式,说白了就是「不直接干活,找个中间人」。静态代理适合小规模场景,动态代理适合大规模、灵活的场景。而AOP,就是代理模式在框架层面的集大成者。理解了代理模式,你就理解了Spring AOP的一半。


公众号:蓝海资料掘金营,微信deep3321