代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
说简单点,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。
通俗地来讲代理模式就是我们生活中常见的中介。
中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类
两种:1.静态代理 2 动态代理
优点:可以在不修改目标对象的前提下扩展目标对象的功能
缺点:如果需要代理多个类,每个类都会有一个代理类,会导致代理类无限制扩展;如果类中有多个方法,同样的代理逻辑需要反复实现、应用到每个方法上,一旦接口增加方法,目标对象与代理对象都要进行修改
所以静态代理不怎么常用
代理类在程序运行时创建的代理方式被成为动态代理。也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。
关键点就是怎么动态生成代理类?
常见的分为两种:
jdk动态代理
cglib动态代理
动态代理的关键点就是动态生成代理类
jdk通过 Proxy.newProxyInstance 创建动态代理类
通过设置系统属性,把生成的代理类保存下来
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
动态生成的代理类
从Proxy源码对JDK动态代理进行深入的剖析。Proxy类对外提供了4个静态方法,分别为:
getProxyClass 方法返回代理类的Class实例。这个代理类就是类加载器loader定义的、实现了一些列接口 interfaces的。如果之前已经为这个loader和interfaces创建过代理类,那么直接返回这个代理类的Class实例。如果没有,则动态创建并返回。
getProxyClass 方法并没有JDK动态代理的核心逻辑:第二行将接口的Class数组interfaces进行克隆。3-6行是类加载器和接口访问权限的校验(这里虚拟机的安全性相关逻辑,不是我们JDK代理技术的关注点,所以不做过多解释)。关键的逻辑就最后一行代码,调用 getProxyClass0 方法去获取代理类的Class实例。
getProxyClass0 也没有包含JDK动态代理的核心逻辑:2-4行只是对接口的个数进行了简单的校验,不能超过65535,我们在实际应用中一般也不会出现这种情况。最后一行代码是去缓存对象 proxyClassCache 中获取代理类的Class实例。proxyClassCache是Proxy类的静态变量
private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
思考:1、为什么JDK动态代理中要求目标类实现的接口数量不能超过65535个?
proxyClassCache 是类
java.lang.reflect.WeakCache 的实例,通过类名就可以看出来这个类是用来做缓存的。Proxy 调用 WeakCache 提供的构造函数,传入 KeyFactory 实例和 ProxyClassFactory 实例(这两个实例的用途后面会讲到)。在分析 WeakCache 的 get 方法源码之前,我们先来大概介绍下 WeakCache 缓存的数据结构。
WeakCache 缓存的数据结构是(key,sub-key)->(value)。这个结构和Redis里面的hash结构很类似,根据一级的键(key)、二级的键(sub-key)为索引,去检索出值(value)。对应到 WeakCache 类代码里面,就是一个ConcurrentMap 实例map,这个map的key就是一级键,map的value又是个 ConcurrentMap 实例,这个子map的key是二级键,子map的value就是缓存的的值。上面图中的箭头就表示着对应关系,一目了然。图中下半部分是JDK动态代理缓存的键值生成规则。下面我们看下 WeakCache 的get方法源码。
我们的调用语句是proxyClassCache.get(loader, interfaces),也就是说形参key是类加载器loader,parameter是接口的Class数组interfaces。
第2行代码是对形参parameter(interfaces)进行非空的校验,如果为空则抛出空指针异常。第3行代码是去除缓存中的陈旧数据,这里不是我们的关注点,就不详细介绍了。
第4行是根据形参key(loader)计算出缓存的一级键cacheKey,这里我们不去看具体的生成逻辑,只需要大概知道一级键是根据形参key(loader)算出来的,这里可以用一个数学函数表达式描述这个关系:key=f(loader)。
第5行代码是根据一级键查出值,这个值的Map实例valuesMap。由于之前没有为这个loader和interfaces创建过代理类,所以valuesMap为null,6-11行代码会被执行,这几行代码就是给valueMap一个初始值,然后结合上面算出来的一级键cacheKey塞进缓存实例map里面。
第12行根据key(loader)和parameter(interfaces)计算出缓存的二级键subKey。这里的subKeyFactory是Proxy调用WeakCache提供的构造函数时,传入的KeyFactory实例(上面提到过)。KeyFactory是Proxy的内部类,我们简单看下KeyFactory的apply方法,看下是怎么生成二级键的。
通过上面的代码,只需要大概知道二级键是根据interfaces计算出来的(classLoader这个参数根本没用到)。这里可以用一个数学函数表达式描述这个关系:sub-key=g(interfaces)。
我们继续上面的WeakCache.get方法分析,第13行代码是根据二级键subKey从valuesMap获取值supplier,这个值supplier也就是我们缓存数据的值。由于valuesMap是新建的,所以supplier为null。
15-37行是个循环,第一次进入的时候,factory和supplier都为null。所以22-30行代码将被执行。第23行代码是调用Factory构造函数创建一个实例factory(Factory是WeakCache的内部类),这个构造函数就是简单把传入的参数赋值给factory实例的字段。接下来26-29将构造的二级键subKey和factory塞进valuesMap,并将factory赋给supplier(Factory类继承Supplier类)。到这里缓存数据的初始化就算告一段落了,一级键是根据loader计算出来的cacheKey,二级键是根据interfaces计算出来的subKey,值是new的一个factory(Supplier实例)。
第一遍的循环并没有创建代理类,只是做了一些初始化的工作,下面继续执行这个循环体(15-37行)。这次supplier不为null了(就是上面的Factory实例factory),所以进入16-21行的代码块,第17行实质就是调用factory.get方法。这个方法返回的value也就是动态代理类的Class实例。紧接着第19行就把这个value返回。下面来看下Factory的get方法的源码。
第2行代码是根据二级键subKey得到值supplier,也就是我们在上面的WeakCache的get方法中创建的Factory实例factory。
接下来的几行代码没什么好讲的,直接看第8行代码,这行代码调用了valueFactory.apply方法创建动态代理类并将结果赋值给变量value。
9-14行针对创建代理类失败的情况下做的处理和判断逻辑。如果创建代理类成功,则继续执行后面的代码。
第15行代码把生成的代理类的Class实例(即value变量)进行缓存。缓存的值并不是直接的value,而是由value构造的一个CacheValue实例cacheValue,由于CacheValue实现了接口Value,而Value接口继承了Supplier接口,所以cacheValue就是Supplier的实例。这里我们不需要去深究CacheValue的数据结构,只需要知道缓存的值是根据代理类的Class实例去计算的,这里可以用一个数学函数表达式描述这个关系:value=h($ProxyX.class)。
第16行代码将二级键subKey和cacheValue放入valuesMap(valuesMap的类型是ConcurrentMap<Object, Supplier<V>>)。第18行是记录缓存状态的。方法的最后将代理类的Class实例value返回。
这个方法的主要逻辑是对缓存的操作,动态代理类的创建动作是通过调用valueFactory.apply得到的。这里的valueFactory是在构造WeakCache时传入的参数,上面提到的ProxyClassFactory实例。由于Factory是WeakCache的内部类,所以在Factory的get方法中可以使用这个实例valueFactory。下面我们就来看下ProxyClassFactory的apply方法。
第2-17行代码,是几个简单的校验工作:1.接口interfaces是否对类加载器loader可见(即接口interfaces是由类加载器loader加载的或者由loader的上一级类加载器加载的,这点需要读者了解JVM的类加载知识),2.参数interfaces是否都是接口,3.参数interfaces里面有没有重复数据。
第18-26行代码,是为即将生成的代理类计算出访问标志和包名。具体的约束和规则如下:
1.如果所有的接口的访问标志都是public,那么生成的代理类的访问标志是final public,否则是final。
2.对于访问标志不是public的接口,它们必须要在同一个包下,否则抛出异常。
3.如果存在访问标志不是public的接口,那么生成的代理类的包名就是这些接口的包名。否则包名是默认的ReflectUtil.PROXY_PACKAGE(即com.sun.proxy)。
第37-38行代码,是计算代理类的权限定名。代理类的简单名称生成规则前面介绍过,是特定前缀"$Proxy"加上一个序列数(0,1,2,3...)。
第39行调用ProxyGenerator的静态方法generateProxyClass去创建代理类的字节码。这个方法逻辑就是根据一些固化的规则(比如代理类里面要实现接口的方法,实现Object的equals、hashCode、toString方法,提供一个形参为InvocationHandler实例的构造函数等等),依据JAVA虚拟机规范中定义的Class类文件结构去生成字节码的。
第41行代码,调用Proxy的本地方法defineClass0将生成的代理类字节码加载到虚拟机,并返回代理类的Class实例。
到目前为止,JDK动态代理类的创建流程就全部结束了,我们说的是首次创建代理类的情况,现在我们回头来看下WeakCache的get方法,如果之前已经为类加载器loader和接口interfaces创建过了代理类,那么调用这个方法的时候是个什么样子呢?答案就是根据一级键和二级键直接从缓存中取到代理类的Class实例。这里就不再逐行分析代码了,读者自己理解下。最后用一张简单概要的时序图描绘一下Proxy的getProxyClass0方法生成代理类的调用流程。
有了上面对动态代理类的创建过程的系统理解,现在来看newProxyInstance方法就容易多了,它就是使用反射机制调用动态代理类的构造函数生成一个代理类实例的过程。
第2-7行代码,克隆interfaces,并进行权限相关校验,和前面getProxyClass方法类似,这里不做赘述。
第8行getProxyClass0方法获取代理类的Class实例,这个方法在上面也详细介绍过了。
第10-12行也是进行权限校验,检查调用者是否对这个代理类有访问权限。
第13-23行就是构造代理类实例的过程。先获取代理类的构造函数,接着对其访问权限进行判断,如果不是public,则将其设置可访问的。最后利用反射机制调用构造方法,传入参数InvocationHandler的实例h,创建代理类实例并返回。
这个方法用于判断一个类是不是代理类。
public static boolean isProxyClass(Class<?> cl) { return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);}
Proxy.class.isAssignableFrom(cl)这句话简而言之就是判断cl所表示的类是不是Proxy或者Proxy的子类。因为所有的代理类都集成Proxy,所以这个条件必须满足。满足这个条件也不能保证就是代理类,因为可能存在人为地编写一个类继承Proxy这种情况。proxyClassCache.containsValue(cl)这个方法是检查缓存中是否存在这个Class实例cl。我前面分析过,但凡生成的代理类都会被缓存,所以这个方法才是检测一个类是否是代理类的唯一标准。
这个方法用于获取代理类中的InvocationHandler实例。这个方法没有什么太多的逻辑,基本就是判断下传入的对象是否是代理类,以及一些访问权限的校验。当这些都合法的情况下,返回InvocationHandler实例。
和JDK动态代理不同,Cglib动态代理是基于Java的继承机制实现的。我们先来看一下Cglib动态代理的UML图。
从上图中可以看出,Cglib生成的动态代理类ProxyObject继承于目标类 TargetSubject,并且持有一个类型为MethodInterceptor 接口的实例引用,这个引用需要我们自己实现,这一点和JDK动态代理的InvocationHandler接口是一样的。我们还是以文章开头的例子来看,用Cglib是如何实现的。
CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。简单的实现举例:
//这是一个需要被代理的类,也就是父类,通过字节码技术创建这个类的子类,实现动态代理public class SayHello { public void say(){ System.out.println("hello everyone"); }}
该类实现了创建子类的方法与代理的方法。getProxy(SuperClass.class)方法通过入参即父类的字节码,通过扩展父类的class来创建代理对象。intercept()方法拦截所有目标类方法的调用,obj表示目标类的实例,method为目标类方法的反射对象,args为方法的动态入参,proxy为代理类实例。proxy.invokeSuper(obj, args)通过代理类调用父类中的方法。
具体实现类
可以看到Cglib在运行时生成的代理类是继承于我们自己的目标类ProxyText,从而实现代理的功能的。那么Cglib的代码到底是怎么生成的,我们继续往下看:
ProxyTest proxyTest = (ProxyTest) enhancer.create();
这段代码是我们调用Cglib生成代理类的代码,我们跟进去看一下,具体是如何实现的。
我们看到具体的实现是调用父类的create接口实现,继续跟进去看看
CgLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,但是CgLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CgLib合适,反之,使用JDK方式要更为合适一些。同时,由于CgLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
JDK动态代理是在运行时JDK根据class文件的格式动态拼装class文件,并加载到jvm中生成代理对象的。而Cglib动态代理是通过ASM库来操作class文件动态生成代理类的。同时你应该了解到:JDK动态代理是基于java中的接口实现的,Cglib是基于java中的继承实现的。
作者:一个没有追求的技术人
链接:
https://juejin.cn/post/7128942358430744583