本篇,我们来探讨Java中另外一个动态特性:动态代理。动态代理是一种强大的功能,它可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为,接下来详细介绍。
简介
要理解动态代理,首先要理解静态代理,了解静态代理之后,就可以很好理解动态代理。动态代理有两种实现方式:一种是Java SDK提供的;另一种是第三方库(如cglib)提供的。
静态代理
代理模式
1)节省成本比较高的实际对象的创建开销,按需延迟加载,创建代理时并不会真正创建实际对象,而只是保存实际对象的地址,在需要时再加载或创建。
2)执行权限检查,代理检查权限后,再调用实际对象
3)屏蔽网络差异和复杂性,代理在本地,而实际对象在其他服务器上,调用本地代理时,本地大力请求其他服务器。
public class SampleStaticProxy { |
分析一下,代理和实际对象一般有相同的接口,在这个demo中,共同的接口是IService,实际对象是
ConcreteService
,代理是
ProxyService
。
ProxyService
内部有一个 IService 的成员变量,指向实际对象,在构造方法中被初始化,对于方法
sayHello
的调用,它转发给了实际对象,在调用前后输出了一些跟踪调试信息,应该很好理解。上述例子是固定的所以称为静态代理。
输出跟踪调试信息可能是一个通用的需求,可以想象,如果每个类都需要,而又不希望修改类定义,我们需要为每个类创建代理,实现所有接口,那这个工作就太繁琐了,如果再有其他的切面需求,整个工作可能又要重新再来。这时,就需要动态代理了,接下来介绍两种实现方式:Java SDK 和 cglib。
动态代理
Java SDK动态代理
用法
在静态代理中,代理类是直接定义在代码中的,在动态代理中,代理类是动态生成的,那么怎么动态生成呢?先看示例代码:
public class SampleDynamicProxy { |
分析一下,IService 和 ConcreteService 依旧没变,程序输出也没变化,但是代理对象
proxyService
的创建方式改变了。其使用 java.lang.reflect包中的Proxy类的静态方法
newProxyInstance
来创建代理对象,这个方法的声明如下:
public static Object newProxyInstance(ClassLoader loader, |
有三个参数,具体如下:
2)interfaces表示代理类要实现的接口列表,是一个数组, 元素的类型只能是接口,不能是普通的类
3)h的类型是
InvocationHandler
,它是一个接口,也定义在java.lang.reflect包中,它只定义了一个方法invoke,对代理接口所有方法的调用都会转给该方法。
newProxyInstance
的返回值类型为
Object
,可以强制转换为
interfaces
数组中的某个接口类型。但是记住,
它不能强制转换为某个类类型
,比如这里的
ConcreteService
,即使它实际代理的对象类型确实是
ConcreteService
。
SampleInvocationHandler
实现了
InvocationHandler
,它的构造方法接受一个参数
obj
表示被代理的对象,invoke方法处理所有的接口调用,它有三个参数:
1)proxy表示代理对象本身,需要注意,它不是被代理的对象,这个参数一般用处不大。
在
SampleInvocationHandler
的
invoke
实现中,我们调用了
method
的
invoke
方法,传递了实际对象
mRealObj
作为参数,达到了调用实际对象对应方法的目的,在调用任何方法前后,输出跟踪调试语句。需要注意,
不能将proxy作为参数传递给method.invoke
,比如:
Object result = method.invoke(proxy, args); |
上面的语句会出现死循环,因为proxy表示当前代理对象,这又会调用到
SampleInvocationHandler
的 invoke 方法。
接下来,我们看一下这里面的基本原理
基本原理
Proxy.newProxyInstance 在了解其内部源码实现之前,我们先用一段代码替代之前的创建 ProxyService 的代码。
Class<?> proxyClass = Proxy.getProxyClass(IService.class.getClassLoader(), new Class<?>[]{ISearchView.class}); |
分为三步:
1)通过Proxy.getProxyClass创建代理类定义,类定义会被缓存;
2)获取代理类的构造方法,构造方法有一个 InvocationHandler 类型的参数;
3)创建 InvocationHandler 对象,创建代理对象。
Proxy.getProxyClass 需要两个参数:一个是ClassLoader;另一个是接口数组。它会动态生成一个类,类名以 $Proxy 开头,后跟一个数字。对于上述示例,动态生成的类定义如下所示,为简化起见,忽略异常处理代码:
【注】这里需要手动去获取JDK动态代理生成类$Proxy0的内容
private static void saveProxy0() throws IOException { |
public final class $Proxy0 extends Proxy implements IService { |
$Proxy0
的父类是
Proxy
,它有一个构造方法,接受一个
InvocationHandler
类型的参数,保存为实例变量 h,h定义在父类Proxy中,它实现了 IService 接口,对于每个方法,如 sayHello,它调用
InvocationHandler
的
invoke
方法,对于 Object 中的方法,如
equals
、
toString
、
hashCode
,
$Proxy0
同样转发给了
InvocationHandler
。
可以看出,
这个类定义本身与被代理的对象并没有关系,与
InvocationHandler
的具体实现也没有关系,而主要与接口数组有关,给定这个接口数组,它动态创建每个接口的实现代码,实现就是转发给
InvocationHandler
,与被代理对象的关系以及对它的调用由
InvocationHandler
的实现管理。
动态代理优点
相比于静态代理,动态代理看起来复杂很多,但是也有很大优点。使用动态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每个被代理的类型都创建一个静态代理类。
private static <T> T getProxy(Class<T> interfaces, T realObj) { |
cglib 动态代理
JDK动态代理的局限在于,只能为接口创建代理,返回的代理对象也只能转换到某个接口类型。如果一个类没有接口,或者希望代理非接口中定义的方法,那就只能另寻他法。有一个第三方类库 cglib ,可以做到这一点,并且Spring、Hibernate等都使用该类库。 但是但是但是 ,一个很致命的缺点是:cglib底层采用的是ASM字节码生成框架,使用字节码技术生成代理类,即生成.class文件,而我们在Android中加载的是优化后的.dex文件,也就是说我们需要可以动态生成.dex文件的代理类,因此cglib在Android中是无法使用的。
cglib 如何使用
下面先看个简单的示例,jar包下载地址 cglib-3.3.0.jar ,这里还会出错,因为会用到ASM,因此还需gradle集成 implementation group: ‘org.ow2.asm’, name: ‘asm’, version: ‘9.0’
public class SampleCglib { |
这里,ConcreteService是被代理的类,它没有接口。getProxy()为一个雷生成代理对象,这个代理对象可以安全地转换为被代理的类型,它使用了cglib的Enhancer类。
Enhancer
类的
setSuperclass
设置被代理的类,
setCallback
设置被代理类的public非final方法被调用时的处理类(即示例的SampleInterceptor)。Enhancer支持多种类型,这里使用的类实现了
MethodInterceptor
接口,它与 JDK 中
InvocationHandler
类似,方法名变成了
intercept
,并且多了个
MethodProxy
类型的参数。
但是与
InvocationHandler
不同的是,
SampleInterceptor
中没有被代理的对象,它通过
MethodProxy
的
invokeSuper
方法调用被代理类的方法:
Object result = methodProxy.invokeSuper(o, objects); |
但是注意,不能这样调用被代理类的方法:
Object result = method.invoke(o, objects); |
因为,o 是代理对象,调用这个方法还是会调用
SampleInterceptor
的
intercept
方法,造成死循环。在main方法里,我们并没有创建被代理的对象,创建的对象直接就是代理对象。
【注】如果想保存cglib生成的class文件,可以加入如下代码:
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/SZSS/Desktop/cglib/JavaHelper"); |
请记住,这段代码一定要设置在获取代理类前面,否则会失效。
cglib 实现机制
当然,可以通过jd-gui查看字节码文件,了解其实现原理,这个等后面字节码专栏写完,回来继续深挖这一块。立个flag吧!
Java SDK代理与cglib代理比较
- 从面相对象的角度看
- 从代理的角度看
JDK代理面向的是一组接口,它为这些接口动态创建了一个实现类。接口的具体实现逻辑是 通过自定义的InvocationHandler实现,这个实现是自定义的,也就是说,背后是不一定有真正被代理的对象,也可能是多个实际对象,根据情况动态选择 。而 cglib代理面向的是一个具体的类,它动态创建了一个新类,继承了该类,通过代理方法重写了其方法。
JDK代理的是
对象
,需要先有一个实际对象,自定义的 InvocationHandler 引用该对象,然后创建一个代理类和代理对象,客户端访问的是代理对象,代理对象最后再调用实际对象的方法;
cglib代理的是
类
,创建的对象只有一个。
如果目的都是为了一个类的方法增强功能,JDK要求该类必须要有接口,且只能处理接口中的方法,但是cglib则没有这个限制。
动态代理的应用-AOP
利用cglib动态代理,我们可以实现一个极简的AOP框架,演示AOP的基本思路和技术原理。首先先简单的过一遍简单AOP框架的用法,然后分析其实现原理。熟悉AspectJ的可以通过这里知其所以然了。
简单AOP框架实现
定义一个新注解 @Aspect,该注解用于注解切面类,指定一个参数,该参数用于标记要增强的类。
(ElementType.TYPE) |
接下来约定,切面类可以声明三个方法 before/after/exception,分别表示在主体类的方法 调用前 、 调用后 、 出现异常时 调用这三个方法。
定义两个切面类,一个服务日志切面类,一个是异常切面类
ServiceLogAspect.java
.class, ServiceB.class}) ({ServiceA |
ExceptionAspect.java
.class) (ServiceB |
ServiceLogAspect目的是在类ServiceA和serviceB所有方法的执行前后加一些日志,而ExceptionAspect的目的是在类ServiceB的方法执行出现异常时收到通知,并输出一些信息。
它们都没有修改类本身,并且类本身做的事就是比较通用和业务化的,与ServiceA和ServiceB的具体逻辑关系也并不密切,但又想改变 ServiceA 和 ServiceB 的行为,那么这就是AOP的思维。
切面定义好之后,需要实现一个容器类,用来获取被切面注解的类并处理。
再定义一个新注解类,用来实现DI
(RetentionPolicy.RUNTIME) |
ServiceA.java
public class ServiceA { |
ServiceB.java
public class ServiceB { |