从代理模式说起,简单聊聊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动态代理的实现

相关推荐
徐*红27 分钟前
java 线程池
java·开发语言
尚学教辅学习资料27 分钟前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
2401_8576363927 分钟前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
1 9 J29 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship30 分钟前
Java面试题(2)
java·开发语言
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨3 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java