Java动态代理
Contents
动态代理
在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个接口和类是实现我们动态代理所必须用到的。
InvocationHandler接口
(代理的方法调用会被转发到InvocationHandler的invoke()方法)。
proxy 动态产生的代理会调用InvocationHandler实现类,所以InvocationHandler 是实际执行者。
InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler接口的子类)。在该接口中声明了如下方法:public Object invoke(Objectproxy, Method method, Object[] args):该方法用于处理对代理类实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时将自动调用该方法。
动态代理类需要在运行时指定所代理真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会将请求自动转发给InvocationHandler对象的invoke()方法,由invoke()方法来实现对请求的统一处理。
每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。InvocationHandler这个接口的唯一一个方法 invoke 方法。
1 | Object invoke(Object proxy, Method method, Object[] args) throws Throwable |
- Proxy类
Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法。这个方法的作用就是得到一个动态的代理对象,其接收三个参数,我们来看看这三个参数所代表的含义:(本质还是通过字节码增强技术生成一个适配原来接口的新的代理类)
1 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException |
Proxy 为什么可以动态产生不同接口类型的代理呢?
因为newProxyInstanse 创建了一个实例,是通过cl这个class文件的构造方法反射生成,cl由getProxyClass0()方法获取。
为什么代理类的方法会被转发到Invocation的invoke 上
因为动态创建的代理类在创建的时候关联了InvocationHandler和要实现的接口。要实现的接口和代理类有关,所以就关联上了。
实例
首先我们定义了一个Subject类型的接口,为其声明了两个方法:
1 | public interface Subject |
接着,定义了一个类来实现这个接口,这个类就是我们的真实对象,RealSubject类:
1 | public class RealSubject implements Subject |
下一步,我们就要定义一个动态代理类了,前面说个,每一个动态代理类都必须要实现 InvocationHandler 这个接口,因此我们这个动态代理类也不例外。
1 | //可以复用的中间类.告诉代理类加的方法写在什么地方,专门添加日志用 |
LoggerHandler 是方法调用的实际处理者,生成代理对象时第三个参数就是填入的实际处理者,然后代理对象的方法调用都会转发到这里。
client 类
1 | public class Client |
1 | 输出 |
我们首先来看看 $Proxy0 这东西,我们看到,这个东西是由 System.out.println(subject.getClass().getName()); 这条语句打印出来的,那么为什么我们返回的这个代理对象的类名是这样的呢?
1 | Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject |
可能我以为返回的这个代理对象会是Subject类型的对象,或者是InvocationHandler的对象,结果却不是,首先我们解释一下为什么我们这里可以将其转化为Subject类型的对象?原因就是在newProxyInstance这个方法的第二个参数上,我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是Subject类型,所以就可以将其转化为Subject类型了。
同时我们一定要记住,通过 Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法简单来说执行了以下操作:
- 1、生成一个实现了参数 interfaces 里所有接口且继承了 Proxy 的代理类的字节码,然后用参数里的 classLoader 加载这个代理类。
- 2、使用代理类父类的构造函数 Proxy(InvocationHandler h) 来创造一个代理类的实例,将我们自定义的 InvocationHandler 的子类传入。
- 3、返回这个代理类实例.
接着我们来看看这两句
subject.rent();
subject.hello(“world”);
这里是通过代理对象来调用实现的那种接口中的方法,这个时候程序就会跳转到由这个代理对象关联到的 handler 中的invoke方法去执行,而我们的这个 handler 对象又接受了一个 RealSubject类型的参数,表示我要代理的就是这个真实对象,所以此时就会调用 handler 中的invoke方法去执行:
1 | public Object invoke(Object object, Method method, Object[] args) |
我们看到,在真正通过代理对象来调用真实对象的方法的时候,我们可以在该方法前后添加自己的一些操作,同时我们看到我们的这个 method 对象是这样的:
1 | public abstract void com.xiaoluo.dynamicproxy.Subject.rent() |
正好就是我们的Subject接口中的两个方法,这也就证明了当我通过代理对象来调用方法的时候,起实际就是委托由其关联到的 handler 对象的invoke方法中来调用,并不是自己来真实调用,而是通过代理的方式来调用的。
这就是我们的java动态代理机制
动态代理两类实现
基于接口的代理和基于继承的代理 JDK代理和Cglib代理
JDK代理
1 通过java.lang.reflect.Proxy 动态生成代理类
2 代理类需要实现InvocationHandler 接口,
3 只能基于接口进行动态代理
1 | public class RealSubject implements Subject |
1 | 动态代理类 |
1 | client |
静态代理时,接口里每增加一个方法,realsubject要实现一遍,关键的是代理类还是要实现一遍,但是动态代理就自己写代码实现了,这样就使代理量减少。
字节码增强技术
Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。
实现字节码增强的主要步骤为:
修改字节码
在内存中获取到原来的字节码,然后通过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,得到一个新的byte数组。
使修改后的字节码生效
有两种方法:
1) 自定义ClassLoader来加载修改后的字节码;
2)替换掉原来的字节码:在JVM加载用户的Class时,拦截,返回修改后的字节码;或者在运行时,使用Instrumentation.redefineClasses方法来替换掉原来的字节码;
动态代理与静态代理
每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类. 所以可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理.
一个代理只能代理一种类型,而且是在编译期就已经确定被代理的对象。而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象
动态代理可以传入不同的对象实现相同的重复功能(比如前面的加日志操作)
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强.
Cglib 动态代理技术
JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。
生成代理类对象
代理类对象是由Enhancer类创建的。Enhancer是CGLIB的字节码增强器,可以很方便的对类进行拓展,如图1.3中的为类设置Superclass。
创建代理对象的几个步骤:
- 生成代理类的二进制字节码文件;
- 加载二进制字节码,生成Class对象( 例如使用Class.forName()方法 );
- 通过反射机制获得实例构造,并创建代理类对象
对委托类进行代理
下面总结一下CGLIB在进行代理的时候都进行了哪些工作
- 生成的代理类HelloServiceImpl$$EnhancerByCGLIB$$82ef2d06继承被代理类HelloServiceImpl。在这里我们需要注意一点:如果委托类被final修饰,那么它不可被继承,即不可被代理;同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理;
- 代理类会为委托方法生成两个方法,一个是重写的sayHello方法,另一个是CGLIB$sayHello$0方法,我们可以看到它是直接调用父类的sayHello方法;
- 当执行代理对象的sayHello方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,则将调用MethodInterceptor中的intercept方法,
在JDK动态代理中方法的调用是通过反射来完成的。但是在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用 :底层将方法全部存入一个数组中,通过数组索引直接进行方法调用。
Author: corn1ng
Link: https://corn1ng.github.io/2017/11/24/Java动态代理/
License: 知识共享署名-非商业性使用 4.0 国际许可协议