胡琪

为今天工作,为明天投资,为未来孵化一些东西!

【设计模式之】代理模式与动态代理技术(JDK自带与CGLIB)

代理模式也是实际开发中使用非常频繁的一个设计模式,原因还是我前面提到的,程序是用来解决实际问题的,而实际生活中代理也很常见,比如朋友圈经常看到代购的帖子,在比如在国内访问谷歌,往往需要使用vpn代理。

代理模式的分类

按照代理类的生成时期来看可以分为静态代理和动态代理。

静态代理:在程序运行前代理类的代码就已确定

动态代理:在运行时动态生成代理类,也就是代理类的字节码.class文件是在运行时才生成的。JDK已经内置了对动态代理功能的提供,即通过反射包Reflect下的Proxy类的newProxyInstance方法配合InvocationHandler来实现。这个没啥好说的了。不过需要注意的是这种方式被代理的类必须实现某个接口,如果要代理的类只是一个独立的类未实现任何接口,那么是不能使用JDK的动态代理的。此时可以使用cglib库实现动态代理,这个后面在讲

按照使用场景一般可以概括为以下几类:

远程代理( Remote Proxy):为一个对象在不同的地址空间提供局部代表。例如:安卓一些系统级服务位于系统进程。而每个app进程通过这些系统服务的代理和系统进程进行交互。

虚代理(Virtual Proxy):根据需要创建开销很大的对象,实现延迟加载

保护代理(Protection Proxy):控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候

GOF的《设计模式》还提到了智能引用,目前不是很明白,对于绝大部分开发者而言用的应该也不多。

应用场景

前面实际已经提到过,主要包括远程代理,虚代理,保护代理。尤其是远程代理模式在软件架构设计中用得非常多。最典型的就是安卓系统用户的app进程通过系统服务的代理和系统进程进行交互。代理位于用户进程中,而系统服务真正的提供者位于系统进程中。2者通过IPC进行通信。这非常类似一个国家的外交大使。

从动静态角度来讲,动态代理应用场景也比较多。比如在业界开源的插件化项目中能够看到大量的动态代理的例子,比如通过代理ActivityManagerNative来拦截IActivityManager接口中定义的一些方法,如拦截startActivity方法将要启动的intent信息替换为启动预先已注册的StbuActivity从而规避启动插件Apk中的Activity组件时未注册异常。

从功能角度来讲:代理模式应用的一个非常广泛的场景就是在不修改某个类的代码的前提下扩展该类的方法的功能(此时这个类往往实现了某个接口)。比如插件化技术,mock系统的ActivityManagerNative。因为这些类是系统定义的,用户无法修改其代码,但是这个类的某些方法在插件化场景下又需要一些其他功能,这个时候就可以使用代理模式。

代理模式示例分析

先看下静态代理,以模拟前面提到的安卓中的ActivityManager和PackageManager为例,假设目前系统存在以下代码(为了简单起见,所有方法的实现只是输出一段日志):

现在我们要mock系统的PackageManager对象的getActivityInfo方法,要在该方法前打印一段我们的输出日志,那么就可以使用代理模式,创建一个PackageManager代理类,然后将系统的PackageManager对象替换为该类。

测试代码:

这里仅仅只是用来说明代理模式,所以未模拟替换系统PackageManager的过程。运行输出结果为:

从上面输出可以看到已经对原方法进行了扩展,在调用被代理类的getActivityInfo之前输出了一段日志。

从这个示例也可以看到代理模式的特点:

  • 被代理类和代理类需要实现相同的接口,因为代理类需要“看上去”是被代理类的类型,从而能够替换它。
  • 代理类会关联被代理类,也就是持有被代理类的引用,这样代理类才能具备被代理类除实现的接口函数之外新增的函数功能。通常把被代理类作为代理类的构造函数的一个参数传进去。当然如果获取或创建被代理类比较容易,那么完全可以在代理类内部创建被代理类对象而不通过依赖注入的形式。此时客户端甚至不需要知道被代理类的存在,这种情况下代理类的作用更符合GOF下的定义:为其他类提供一种代理以控制对这个对象的访问。但据我的经验来看很多场合下往往不是这样的,个人觉得GOF对代理模式下的定义不是很好,或者说我们使用的很多时候是一种广义上的代理模式。

代理类具备被代理类的所有功能(因为持有其引用),同时也具备一些自己额外的功能。但是本质上来讲,代理类只是个傀儡,原功能的真正提供者仍然是被代理类。

使用cglib库实现动态代理

JDK内置的动态代理

在讲使用cglib库实现动态代理之前先大致提下JDK内置的动态代理实现方式,因为2者从使用上来看用法非常类似。还是以模拟前面提到的安卓中的ActivityManager和PackageManager为例 ,现在我们要mock系统的PackageManager对象的getActivityInfo方法,要在该方法前打印一段我们的输出日志。

前面提到过使用静态代理完全是可以达到这个目的的,但是聪明的同学可能已经意识到,因为代理类和被代理类实现了相同的接口,那么意味着,假设该接口中定义了很多方法,那么即使我们实际上只打算扩展某一个方法的功能,那么代理类也必须覆写所有的这些方法。这也是静态代理的一个很大的缺点,而动态代理则没有这个缺点。所以当被代理类实现的接口中声明了很多方法而我们只打算对其中某几个方法进行功能扩展时使用动态代理比较好。

运用JDK自带的动态代理技术很容易做到这一点。

首先我们定义一个代理类PackageManagerProxy实现JDK内置的InvocationHandler接口,重写方法invoke。

代码如下:

其中最关键的就是invoke方法,该方法定义如下:

第一个参数proxy表示要代理的类的实例对象,第二个参数method表示要代理类相应的方法,args表示该方法对应的参数。所以如果我们要拦截PackageManager的invoke方法只需要通过method的getName方法进行过滤即可。另外为了确保我们定义的PackageManagerProxy代理类具备PackageManager所有的功能,通常我们会将PackageManager对象作为代理类PackageManagerProxy的构造函数的参数传进来。因为InvocationHandler接口的回调方法invoke只会调用IPackageManager接口中定义的方法,而实现类很可能新增了一些接口中不存在的其他方法,比如printLog。

然后在原始调用处通过java反射包下的Proxy类创建PackageManagerProxy对象

代码如下:

其中最关键的就是Proxy类的newProxyInstance方法,该方法定义如下:

第一个参数loader表示定义代理类的loader,通常传入调用被代理类的getClassLoader()方法得到的loader即可,第二个参数interfaces表示代理类实现的接口列表,因为一个类可能会实现多个接口,所以该参数是一个数组类型,通常传入调用被代理类的getInterfaces()方法的返回的接口列表即可,第三个参数h表示实现了InvocationHandler接口的代理类。其返回值类型是实现了这些接口类型的一个实例对象,因此可以将其向上强转为任何一个接口类型(假如被代理类实现了多个接口的话),但是不能强转为被代理类类型。以上面的示例来说就是可以强转为IPackageManager类型,但是不能强转为PackageManager类型。

运行上面那段程序,输出结果为:

可以看到在调用getActivityInfo之前输出了我们的打印日志,也即达到了在调用一个方法前执行一段我们自己的逻辑的目的。但是前面说过JDK内置的动态代理存在一个很大的缺陷就是被代理类必须实现某个接口,如果一个类未实现任何接口是不能被动态代理的。原因其实很简单,从前面提到的newProxyInstance方法可以看到,该方法的第二个参数是一个接口列表,实际上代理类对象packageManagerProxy在调用一个方法的时候就是去调用这些接口中声明的方法的,也正因为如此,所以前面提到过可以将被代理类作为参数传递给代理类的构造函数,以便代理类拥有被代理类所有的功能。

cglib库对动态代理的支持

讲解完了JDK自带的动态代理技术,知道了该模式的缺点之后再来看看cglib库的动态代理如何使用。cglib的官网:https://github.com/cglib/cglib/wikicglib ,上面有一些简单介绍和示例代码。cglib的内部实现使用了字节码操作框架ASM,所以如果要在项目中使用cglib,还需要引入ASM库:https://asm.ow2.io/。对字节码操作比较熟悉的同学对这个库一定不会陌生,如果还不了解ASM,可以参看:使用ASM操作java字节码,不了解也没关系,因为如果仅仅只是使用cglib不需要你对ASM有所了解,只需要知道要引入这个库就行了,不过如果你打算深入了解cglib库的实现原理的话,最好是能够对ASM有所了解。

现在假设存在一个系统类ActivityManager但是该类未实现任何接口:

那么这时是无法使用JDK自带的动态代理功能的,因为前面说过JDK反射包下的Proxy类的静态函数newProxyInstance返回的是一个实现了该函数第二个参数interfaces接口的实例对象,如果该类未实现任何接口那么在运行时JDK动态生成的代理类就不会继承任何接口。此时该类本质上就只是一个com.sun.proxy.$Proxy1类型(为什么是该类型,感兴趣的同学可以深入分析下JDK动态代理类生成的原理就知道了)。此时就无法将其向上强转为被代理类或被代理实现的某个接口类型。也就无法调用被代理类的方法。这个场景cglib库就派上用场了。下面看下如何使用cglib库来达到代理ActivityManager在程序中输入一段日志的效果。

首先定义一个CglibActivityManagerProxy类实现cglib库的MethodInterceptor方法拦截接口,然后覆写该接口的intercept方法。

代码如下:

其中最关键的就是intercept方法,该方法定义如下:

该方法和JDK内置的InvocationHandler的invoke方法非常类似,前面3个参数的类型和作用与JDK的完全一致,只不过新增了一个MethodProxy参数,这个MethodProxy表示的是被代理类调用的方法的一个代理。也就是说既可以像使用JDK那样通过method来进行过滤,也可以通过methodProxy进行操作。

然后创建Enhancer对象,调用该对象的setSuperclass方法设置被代理类,调用setCallback方法设置代理类即可。调用create方法即可返回创建的动态代理对象

运行上述代码,程序输出如下:

说明达到了动态代理一个未实现任何接口的类的效果。

NOTE:能用JDK实现动态代理的场景则不推荐用cglib去实现。

 

 

打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注