从代理模式说起,简单聊聊Java的动态代理

从代理模式说起

「代理模式」是设计模式的一种,代理模式中有两个关键的成员:「代理类」(Proxy)和「被代理类」(RealSubject)

那Proxy有啥用呢,直接访问RealSubject不行嘛?

  • 如果「被代理类」十分庞大(消耗内存空间),但真正需要它的时候很少,我们不希望立即初始化「被代理类」从而占用内存,交给轻量级的「代理类」Proxy完成任务即可。
  • 我们希望对「被代理类」进行一些增强,比如说在方法开始执行前后打印参数的变化,执行结果等等信息,为了解耦「被代理类」的业务实现和这种与业务无关的行为逻辑,我们需要将这些与业务无关的行为逻辑剥离出来,就可以封装在「代理类」中
  • 往往这种与业务无关的行为逻辑有很多共性,这些逻辑可以被抽象为「切面」,也就是AOP,面向切面编程,通过代理模式可以极大地减少重复代码。

代理模式的实现方法

代理模式一般有两种实现方法:静态代理和动态代理。

静态代理

静态代理就是上述UML图的实现方法,可以看到「代理类」Proxy内聚一个RealObject,实现共同的接口,可以很轻松地在这个方法上做加强。

接口:

csharp 复制代码
public interface Subject {
    void dosth();
}

被代理类:

java 复制代码
    public class RealSubject implements Subject{
        @Override
        public void dosth(){
            System.out.println("dosth...");
        }
    }

代理类:

java 复制代码
     public class Proxy implements Subject{
        private Subject subject;
        @Override
        public void dosth(){
            System.out.println("before-------执行前增强逻辑");
            subject.dosth();
            System.out.println("after--------执行后增强逻辑");
        }
    }

静态代理的局限性

  • 每需要一个类被代理,就需要为之编写一个代理类,这会导致文件数量膨胀。
  • 上文提到的Proxy的第三个好处,即AOP的功能还没有实现,我们希望一个代理类能代理多个类,并且被代理的方法是我们可以指定的,这就需要动态代理来解决了。

动态代理

动态代理又有两种常见的实现:JDK动态代理和CGLIB动态代理。

JDK动态代理

基本使用

JDK动态代理中:「代理类」Proxy不再实现Subject接口,而是implements InvocationHandler,但仍聚合了被代理类(通过构造函数传入被代理类,Object类型)

代理类implements InvocationHandler,重写invoke方法

java 复制代码
    public class TestInterceptor implements InvocationHandler {
        private Object target;//目标对象的引用,这里设计成Object类型,更具通用性  
        public TestInterceptor(Object target){  
            this.target = target;  
        }
        
        public Object invoke(Object proxy, Method method, Object[] arg)  throws Throwable {  
            System.out.println("before-------执行前逻辑");  
            Object result = method.invoke(target, arg);//调用目标对象的方法  
            System.out.println("Before return:"+result);  
            return result;  
        }  
    }  

利用Proxy类.newProxyInstance方法

java 复制代码
    public class Main {  
        public static void main(String[] args) {  
            RealSubject target = new RealSubject();//生成目标对象  
            //接下来创建代理对象  
            Subject proxy = (Subject) Proxy.newProxyInstance(  
                    target.getClass().getClassLoader(),  
                    target.getClass().getInterfaces(), new TestInterceptor(target));  
            //调用代理类的方法,有切面逻辑 
            proxy.dosth();  
        }  
    }  

获取proxy的名称,发现是$Proxy0

实现原理

通过前文的基本使用,我们了解到了两个关键点:InvocationHandler接口和Proxy代理类。

InvocationHandler只有一个invoke方法

typescript 复制代码
public interface InvocationHandler {  
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

那么重点就是Proxy.newProxyInstance方法

java 复制代码
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
    {
        // 1. 获取到代理类的class对象
        Class<?> cl = getProxyClass0(loader, intfs);
        // 2. 拿到代理类的构造器,通过反射创建出一个代理类实例
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
             public Void run() {
                  cons.setAccessible(true);
                     return null;
                 }
               });
            }
        return cons.newInstance(new Object[]{h});
    }

那getProxyClass0这个方法是如何拿到代理的class的呢

其实这个方法做了参数的校验,然后直接调用proxyClassCache.get方法

kotlin 复制代码
return proxyClassCache.get(loader, interfaces);

这个Cache也很好理解,我们两次获取代理对象,没必要生成两个class对象,因此要复用代理类的class,我们来关注cache未命中的情况,proxyClass是如何生成的。

这个proxyClassCache是Proxy内部的一个字段如下(注意两个入参):

swift 复制代码
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

get方法最重要的部分如下:

java 复制代码
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    // 最终这个class对象就是supplier.get方法返回的
    // Supplier是一个lambda表达式
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;
    ​
    while (true) {
        // 如果有supplier了 直接get,正常就返回(一般缓存命中就在这里直接return了)
        if (supplier != null) {
            V value = supplier.get();
            if (value != null) {
                return value;
            }
        }
        // 一般缓存未命中 会先创建一个Factory , Factory是一个实现了supplier的内部类
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }
        // 尝试放入缓存
        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                supplier = factory;
            }
        } else {
            if (valuesMap.replace(subKey, supplier, factory)) {
                supplier = factory;
            } else {
                supplier = valuesMap.get(subKey);
            }
        }
    }

那么核心就是Factory.get方法了:

ini 复制代码
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
return value;

兜兜转转又回到了proxyClassCache的构造函数的第二个参数了,就是ProxyClassFactory

核心:ProxyClassFactory

代理类名的由来

java 复制代码
    // 前缀
    private static final String proxyClassNamePrefix = "$Proxy";
    // 原子long
    private static final AtomicLong nextUniqueNumber = new AtomicLong();
    // 代理类名
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

最后调用到:

java 复制代码
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces, accessFlags);
return defineClass0(loader, proxyName,
                    proxyClassFile, 0, proxyClassFile.length);

发现generateProxyClass非常晦涩难懂,并且defineClass0是个native方法,总之是生成了字节码文件并加载了class对象,只能反编译查看代理类的实现。

发现代理类继承了Proxy,被代理的方法以Method的形式存储,最终通过反射的方式来调用。

CGLIB动态代理

cglib不再需要被代理类实现一个接口,这是和JDK动态代理与静态代理的不同点,这可以帮助我们远离必须实现接口的困扰。

基本使用

被代理类:

java 复制代码
public class RealSubject{
    public void dosth(){
        System.out.println("dosth...");
    }
}
​

代理类, 实现 MethodInterceptor接口

java 复制代码
public class CglibProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("xxx");
        methodProxy.invokeSuper(object, args);
        System.out.println("xxx");
        return null;
    }
}

利用Enhancer创建代理对象。

java 复制代码
public class CglibTest {
    public static void main(String[] args) {
        CglibProxy cglibproxy = new CglibProxy();
        Enhancer enhancer = new Enhancer();
        // 设置enhancer对象的父类(被代理类)
        enhancer.setSuperclass(RealSubject.class);
        // 设置enhancer的回调对象(代理类)
        enhancer.setCallback(cglibproxy);
        // 创建代理对象
        RealSubject proxy = (RealSubject) enhancer.create();
        proxy.dosth();
    }
}

实现原理

如果要从ASM开始了解太费精力了,一种做法是直接看cglib生成的所有类反向推。

cglib主要通过ASM直接修改字节码文件,再通过类加载器加载生成一个class对象,就是我们的代理类Proxy,最后依然通过反射拿到Proxy的构造方法并创建一个实例对象并返回。

cglib的代理类Proxy实际上是继承被代理类RealSubject的,并且实现了Factory接口,因此cglib的局限性是:final类是无法被代理的

cglib实际会生成五个字节码文件,比较重要的有三个,代理类,以及两个FastClass分别对应代理类和被代理类,所以在生成代理对象时会慢一些。cglib调用原始方法是通过FastClass的下标进行调用的。而JDK动态代理是通过反射进行调用的。

建议自己动手反编译代理类看一下,篇幅原因这里就不贴了,实在不想动手也可以参考:分析cglib动态代理的实现

如何理解静态与动态的区别

静态代理,是一对一的关系。是确定了「被代理类」,专为此「被代理类」创建了一个代理类。

而动态代理,是多对多关系。解耦了「代理逻辑」与「被代理类」,彼此不相干。静态代理的主体是「被代理类」,而动态代理的主体既是「被代理类」,也是「代理逻辑」。只是我们在真正需要代理的时候,才把他们结合到一起。这里说「代理逻辑」,是因为真正的「代理类」是动态生成的,在此之前并不知道会用什么「代理逻辑」。而静态代理的「代理类」是静态的,代理逻辑是确定的。所以也可以说,静态代理我们是在编写「代理类」,而动态代理我们是在编写「代理逻辑」。

JDK与cglib的区别

  • JDK的核心是反射,cglib的核心是ASM
  • 通常JDK的效率更高,一种佐证是:Spring默认使用JDK,没有实现接口才用cglib
  • JDK是委托机制,cglib是继承关系
  • JDK要求被代理类必须实现某个接口,而cglib要求被代理类不被final修饰

SpringBoot默认cglib实现AOP

SpringBoot关于Spring AOP的配置类:AopAutoConfiguration,默认使用 Cglib 来实现AOP。

这样设置的原因是:避免在没有实现接口时报错(无法使用JDK动态代理)

可以通过在配置文件中输入如下命令关闭cglib

properties 复制代码
spring.aop.proxy-target-class=false

参考文档

分析cglib动态代理的实现

相关推荐
骄马之死5 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
GoGeekBaird6 小时前
Anthropic技能"(Skills)的经验分享
后端
王码码20357 小时前
多台服务器怎么统一看状态?Beszel 轻量监控,搭起来不费事
运维·服务器·后端·安全·阿里云·接口·web
郑洁文7 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code8 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
Cosolar8 小时前
LlamaIndex 文档解析与分块策略深度解析
人工智能·面试·架构
指令集梦境8 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
摇滚侠9 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown9 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
码云之上9 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js