java的静态代理、jdk动态代理和cglib动态代理
Java的代理就是客户端不再直接和委托类打交道,而是通过一个中间层来访问,这个中间层就是代理。
使用代理有两个好处,一是可以隐藏委托类的实现;二是可以实现客户与委托类之间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。
举个很常见的例子。工厂会生产很多的玩具,但是我们买玩具都是到商店买的,而不是到工厂去买的,工厂怎么生产我们并不关心,我们只知道到商店可以买到自己想要的玩具,并且,如果我们需要送人的话商店还可以把这些玩具使用礼品盒进行包装。在这里,这个工厂就是委托类,商店就是代理类,我们就是客户类,礼品盒就是代理类对委托类做的一些额外处理。
在Java中有很多场景需要使用代理类,比如远程RPC调用的时候就是通过代理类去实现的,还有Spring的AOP切面中,我们也是为切面生成了一个代理类等等。
代理类主要分为静态代理、JDK动态代理和CGLIB动态代理,它们各有优缺点,没有最好的,存在就是有意义的,在不同的场景下它们会有不同的用途。
Java的静态代理
静态代理首先是定义接口和接口的实现类,然后定义接口的代理对象,并将接口的实例注入到代理对象中,然后通过代理对象去调用真正的实现类,实现过程非常简单也比较容易理解。静态代理的代理关系在编译期间就已经确定了的,适合于代理类较少且确定的情况,可以实现在不修改委托类的情况下做一些额外的处理,比如包装礼盒,实现客户类与委托类的解耦等。但是缺点是只适用于委托方法少的情况下,试想一下,如果委托类有几百上千个方法,岂不是要在代理类中写一堆的代理方法,于是就有了动态代理来解决这个问题,动态代理在后面说。
定义接口和接口的实现类
- // 委托接口
- public interface SayHelloService {
- void sayHello(String userName);
- }
- // 委托实现类
- public class SayHelloServiceImpl implements SayHelloService {
- public void sayHello(String userName) {
- System.out.println("hello, " + userName);
- }
- }
接口的代理类
- // 代理类
- public class SayHelloProxy implements SayHelloService {
- private SayHelloService sayHelloService = new SayHelloServiceImpl();
- public void sayHello(String userName) {
- // 代理事前做一些事情
- System.out.println("do something before proxy...");
- // 调用委托类的方法
- sayHelloService.sayHello(userName);
- // 代理事后做一些事情
- System.out.println("do something after proxy...");
- }
- }
通过代理对象访问委托类
- // 测试静态代理类
- public class SayHelloTest {
- public static void main(String[] args) {
- SayHelloProxy sayHelloProxy = new SayHelloProxy();
- sayHelloProxy.sayHello("yanggb");
- }
- }
Java的动态代理技术
代理类在程序运行时创建的代理方式,就叫做动态代理。在了解动态代理之前,先要回顾JVM的类加载机制中的加载阶段要做的三件事情:
1.通过一个类的全名或其他途径来获取这个类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的Class对象,作为方法区中对这个类访问的入口。
而动态代理主要就发生在第一个阶段,这个阶段类的二进制字节流的来源可以有很多,比如ZIP包、网络、运行时计算生成、其他文件生成(JSP)和数据库获取等。其中的运行时计算生成就是Java的动态代理技术,在Proxy类中,就是运用了ProxyGenerator.generateProxyClass来为特定接口生成形式为*$Proxy的代理类的二进制字节流。所谓的动态代理就是想办法根据接口或者目标对象计算出代理类的字节码然后加载进JVM中。实际计算的情况会很复杂,因此一般是借助一些诸如JDK动态代理实现、CGLIB第三方库来完成的。因此为了让生成的代理类与目标对象(就是委托类)保持一致,通常有两种做法:通过接口的JDK动态代理和通过继承类的CGLIB动态代理。
因为前面说的两种动态代理都是基于反射来实现的,在运行时查找对象属性、方法、修改作用域、通过方法名称调用方法等。而在线的应用不会频繁使用反射,因为反射的性能开销较大,因此另外还有使用ASM框架的JAVASSIST,相对开销很小,下次用另外的篇幅去说。
JDK动态代理
在Java的JDK动态代理中,主要涉及两个类,一个是java.lang.reflect.Proxy,一个是java.lang.reflectInvocationHandler。要使用JDK的动态代理需要一个实现InvocationHandler接口的中间类,这个接口只有一个方法invoke()方法,对处理类的所有方法的调用都会变成对invoke()方法的调用,这样就可以在invoke()方法中添加统一的处理逻辑(也可以根据method参数判断是哪个方法)。中间类(实现了InvocationHandler的类)有一个委托类对象引用,在invoke()方法中调用了委托类对象的相应方法,通过这种聚合的方式持有委托类对象引用,把外部对invoke()方法的调用最终都转为对委托类对象的调用。
实际上,中间类与委托类构成了静态代理关系,在这个关系中,中间类是代理类,委托类是委托类。然后代理类与中间类也构成一个静态代理关系,在这个关系中,中间类是委托类,代理类是代理类。也就是说,动态代理关系是由两组静态代理关系组成的,这就是JDK动态代理的原理。
InvocationHandler接口的定义源码与参数说明(在注释中)
- public interface InvocationHandler {
- /**
- * 调用处理
- * @param proxy 代理类对象
- * @param methon 标识具体调用的是代理类的哪个方法
- * @param args 代理类方法的参数
- */
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
- }
定义接口和接口的实现类
- // 委托类接口
- public interface HelloService {
- void sayHello(String userName);
- void sayByeBye(String userName);
- }
- // 委托类
- public class HelloServiceImpl implements HelloService {
- public void sayHello(String userName) {
- System.out.println("hello, " + userName);
- }
- public void sayByeBye(String userName) {
- System.out.println("byebye, "+ userName);
- }
- }
定义一个实现InvocationHandler接口的中间类
- // 中间类
- public class HelloInvocationHandler implements InvocationHandler {
- /**
- * 中间类持有委托类对象的引用,这里会构成一种静态代理关系
- */
- private Object obj;
- /**
- * 有参构造器,传入委托类的对象
- *
- * @param obj 委托类的对象
- */
- public HelloInvocationHandler(Object obj) {
- this.obj = obj;
- }
- /**
- * 动态生成代理类对象,Proxy.newProxyInstance
- *
- * @return 返回代理类的实例
- */
- public Object newProxyInstance() {
- return Proxy.newProxyInstance(
- // 指定代理对象的类加载器
- obj.getClass().getClassLoader(),
- // 代理对象需要实现的接口,可以同时指定多个接口
- obj.getClass().getInterfaces(),
- // 方法调用的实际处理者,代理对象的方法调用都会转发到这里
- this);
- }
- /**
- * @param proxy 代理对象
- * @param method 代理方法
- * @param args 方法的参数
- */
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- // 在invoke前做一些事情
- System.out.println("do something before invoke...");
- // 执行invoke
- Object result = method.invoke(obj, args);
- // 在invoke之后做一些事情
- System.out.println("do something after invoke...");
- return result;
- }
- }
通过中间类访问委托类
- // 测试动态代理类
- public class HelloTest {
- public static void main(String[] args) {
- HelloInvocationHandler helloInvocationHandler = new HelloInvocationHandler(new HelloServiceImpl());
- HelloService helloService = (HelloService) helloInvocationHandler.newProxyInstance();
- helloService.sayHello("yanggb");
- helloService.sayByeBye("yanggb");
- }
- }
在上面的测试动态代理类中,我们调用了Proxy类的newProxyInstance()方法来获取一个代理类示例。这个代理类实现了我们指定的接口,并且会把方法调用分发到指定的调用处理器。首先通过newProxyInstance()方法获取代理类的示例,之后就可以通过这个代理类的实例调用代理类的方法,对代理类的方法调用都会调用中间类(实现了InvocationHandler接口的类)的invoke()方法,在invoke()方法中我们调用委托类的对应方法,然后加上了自己的处理逻辑。
JDK动态代理最大的特点就是动态生成的代理类和委托类实现同一个接口。JDK动态代理其实内部是通过反射机制(Proxy.newProxyInstance)实现的,也就是已知的一个对象,在运行的时候动态调用它的方法,并且调用的时候还可以加一些自己的逻辑。
CGLIB动态代理
JDK动态代理依赖接口实现,而当我们只有类没有接口的时候,就需要使用另一种动态代理技术:CGLIB动态代理。首先CGLIB动态代理是第三方框架实现的,需要在Maven工程中引入cglib的包:
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>2.2</version>
- </dependency>
CGLIB代理是针对类来实现代理的,原理是对指定的委托类生成一个子类并重写其中的业务方法来实现代理。代理类对象是由Enhancer类创建的。CGLIB创建动态代理类的模式是:
1.查找目标类上的所有非final的public方法(final的方法不能被重写)。
2.将这些方法的定义转为字节码。
3.将字节码转换成相应的代理的Class对象,然后通过反射获得代理类的实例对象。
4.实现MethodInterceptor接口,用来处理对代理类上所有方法的请求。
定义一个简单类
- // 委托类,是一个简单类
- public class HelloClass {
- public void sayHello(String userName){
- System.out.println("hello, " + userName);
- }
- public void sayByeBye(String userName){
- System.out.println("byebye, " + userName);
- }
- }
定义一个实现MethodInterceptor类的拦截类
- // HelloInterceptor 用于对方法调用拦截以及回调
- public class HelloInterceptor implements MethodInterceptor {
- /**
- * CGLIB 增强类对象,代理类对象是由 Enhancer 类创建的,
- * Enhancer 是 CGLIB 的字节码增强器,可以很方便的对类进行拓展
- */
- private Enhancer enhancer = new Enhancer();
- /**
- *
- * @param obj 被代理的对象
- * @param method 代理的方法
- * @param args 方法的参数
- * @param proxy CGLIB方法代理对象
- * @return cglib生成用来代替Method对象的一个对象,使用MethodProxy比调用JDK自身的Method直接执行方法效率会有提升
- */
- public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
- System.out.println("方法调用之前");
- Object o = proxy.invokeSuper(obj, args);
- System.out.println("方法调用之后");
- return o;
- }
- /**
- * 使用动态代理创建一个代理对象
- * @param c 类名
- */
- public Object newProxyInstance(Class<?> c) {
- // 设置产生的代理对象的父类,增强类型
- enhancer.setSuperclass(c);
- // 定义代理逻辑对象为当前对象,要求当前对象实现 MethodInterceptor 接口
- enhancer.setCallback(this);
- // 使用默认无参数的构造函数创建目标对象,这是一个前提,被代理的类要提供无参构造方法
- return enhancer.create();
- }
- }
通过拦截类访问委托类
- // 测试类
- public class HelloTest {
- public static void main(String[] args) {
- HelloInterceptor helloInterceptor = new HelloInterceptor();
- HelloClass sayHelloClass = (HelloClass) helloInterceptor.newProxyInstance(HelloClass.class);
- sayHelloClass.sayHello("yanggb");
- sayHelloClass.sayByeBye("yanggb");
- }
- }
对于需要被代理的类,它只是动态生成一个子类以覆盖非final的方法,同时绑定钩子回调自定义的拦截器。它比JDK动态代理在性能上要快。值得注意的是,我们传入目标类作为代理类的父类。不同于JDK动态代理,我们不能使用目标对象来创建代理。目标对象只能被CGLIB创建。在例子中,默认的无参构造方法被使用来创建目标对象。
总结
1.静态代理比较容易理解,需要被代理的类和代理类实现自同一个接口,然后在代理类中调用真正的实现类,并且静态代理的关系在编译期间就已经确定了。而动态代理的关系是在运行期间确定的。静态代理实现简单,适合于代理类较少且确定的情况,而动态代理则给我们提供了更大的灵活性。
2.JDK动态代理所用到的代理类在程序调用到代理类对象时才由JVM真正创建,JVM根据传进来的业务实现类对象以及方法名,动态地创建了一个代理类的class文件并被字节码引擎执行,然后通过该代理类对象进行方法调用。我们需要做的只是指定代理类的预处理、调用后操作即可。
3.静态代理和动态代理都是基于接口实现的,而对于那些没有提供接口只是提供了实现类的而言,就只能选择CGLIB动态代理了。
面试题1:JDK动态代理和CGLIB动态代理的区别
1.JDK动态代理是基于Java反射实现的,必须要实现了接口的业务类才能用这种方法生成代理对象。
2.CGLIB动态代理是基于ASM框架,通过生成业务类的子类来实现的。
3.JDK动态代理的优势是最小化依赖关系,减少依赖意味着简化开发和维护,并且有JDK自身的支持。还可以平滑地进行JDK版本升级,代码实现简单。
4.基于CGLIB框架的优势是无须实现接口,达到代理类无侵入,我们只需操作我们关心的类,不必为其他相关类增加工作量,性能比较高。
面试题2:描述代理的几种实现方式,并说出优缺点
代理可以分为静态代理和动态代理,而动态代理又分为JDK动态代理和CGLIB动态代理。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是Real Object。优点是可以很好地保护实际对象的业务逻辑对外暴露,从而提高安全性。缺点是不同的接口要有不同的代理类实现,会冗余。
JDK动态代理:JDK动态代理只需要实现InvocationHandler接口,重写invoke()方法便可以完成代理的实现,原理是利用反射生成代理类Proxyxx.class代理类字节码并生成对象。JDK动态代理之所以只能代理接口是因为代理类本身已经继承了Proxy类,而Java是不允许多重继承的,只是允许实现多个接口。优点是解决了静态代理中冗余的代理实现类问题。缺点是JDK动态代理是基于接口的设计实现的,如果没有接口就会抛异常。
CGLIB动态代理:由于JDK动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了。CGLIB采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑来完成动态代理的实现。实现的具体方式是实现MethodInterceptor接口并重写intercept()方法,通过Enhancer类的回调方法来实现。但是CGLIB在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象因为无需频繁创建对象用CGLIB更合适。反之则是使用JDK动态代理的方式更合适一些。同时,由于CGLIB是采用的动态创建子类的方法,对于final方法是无法进行代理的。优点是没有接口也能够实现动态代理,而且采用的是字节码增强技术,性能也不错。缺点则是技术实现上要相对难理解些。
"室内的灯没有开,阴沉的阳光透过灰蒙蒙的窗户玻璃折射进来,我的世界只剩下了宁静。"
java的静态代理、jdk动态代理和cglib动态代理的更多相关文章
- 代理模式之静态代理,JDK动态代理和cglib动态代理
代理模式,顾名思义,就是通过代理去完成某些功能.比如,你需要购买火车票,不想跑那么远到火车站售票窗口买,可以去附近的火车票代售点买,或者到携程等第三方网站买.这个时候,我们就把火车站叫做目标对象或者委 ...
- jdk动态代理和cglib动态代理底层实现原理详细解析(cglib动态代理篇)
代理模式是一种很常见的模式,本文主要分析cglib动态代理的过程 1. 举例 使用cglib代理需要引入两个包,maven的话包引入如下 <!-- https://mvnrepository.c ...
- JDK动态代理和CGLib动态代理简单演示
JDK1.3之后,Java提供了动态代理的技术,允许开发者在运行期间创建接口的代理实例. 一.首先我们进行JDK动态代理的演示. 现在我们有一个简单的业务接口Saying,如下: package te ...
- 基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)到底有什么区别。
基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)到底有什么区别. 我还是喜欢基于Schema风格的Spring事务管理,但也有很多人在用基于@Tras ...
- Spring -- <tx:annotation-driven>注解基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)的区别。
借鉴:http://jinnianshilongnian.iteye.com/blog/1508018 基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional ...
- Spring <tx:annotation-driven>注解 JDK动态代理和CGLIB动态代理 区别。
基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)到底有什么区别. 我还是喜欢基于Schema风格的Spring事务管理,但也有很多人在用基于@Tras ...
- jdk动态代理和cglib动态代理的区别
一.原理区别: java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理. 而cglib动态代理是利用asm开源包,对代理对象类的class文件 ...
- jdk 动态代理和 cglib 动态代理
原理区别: java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理. 而cglib动态代理是利用asm开源包,对代理对象类的class文件加载 ...
- 动态代理:jdk动态代理和cglib动态代理
/** * 动态代理类:先参考代理模式随笔,了解代理模式的概念,分为jdk动态代理和cglib,jdk动态代理是通过实现接口的方式创建代理类的,cglib是通过继承类的方式实现的代理类的 * jdk动 ...
随机推荐
- Kubernetes 系列(三):Kubernetes使用Traefik Ingress暴露服务
一.Kubernetes 服务暴露介绍 从 kubernetes 1.2 版本开始,kubernetes提供了 Ingress 对象来实现对外暴露服务:到目前为止 kubernetes 总共有三种暴露 ...
- 【原】git如何撤销已提交的commit(未push)
输入git log,我们可以看到最近的3次提交,最近一次提交是test3,最早的一次是test1,其中一大串类似黄色的字母是commit id(版本号) 如果嫌输出信息太多,可加上--pretty=o ...
- c++第一个程序“Hello world!”
c++第一个程序“Hello world!” 打开编译器(这里以vs2013为例) 单击新建项目 选择Win32 控制台应用程序 点击右下角确定 点击完成 点击解决方案管理器 新建cpp文件 右 ...
- mobaxterm使用手册
Mobaxterm V14使用手册 文章出处 https://blog.51cto.com/937761/2372598 简介 MobaXterm 一款Windows系统下全功能终端软件.以下将 ...
- sublime text2解决中文乱码,支持中文的设置方法
步骤: 1.安装Sublime Package Control. 在Sublime Text 2上用Ctrl+-打开控制台并在里面输入以下代码,Sublime Text 2就会自动安装P ...
- WPF编程,C#中对话框自动关闭的一种方法(转载)
本文原文链接:https://blog.csdn.net/qq_43307934/article/details/84933196———————————————— MessageBoxTimeout是 ...
- Vue中插槽指令
08.29自我总结 Vue中插槽指令 意义 就是在组件里留着差值方便后续组件内容新增 而且由于插件是写在父级中数据可以直接父级中传输而不需要传子再传父有些情况会减少写代码量 示例 <div id ...
- Vue学习笔记和代码记录
## 安装 * 直接引入链接:建议初学者使用:* 通过node.js的NPM安装Vue:* 通过node.js的NPM安装Vue-cli脚手架(推荐安装方式): ## 基础 ### 数据类型 * 字符 ...
- JavaScript中Array(数组) 对象
JavaScript中Array 对象 JavaScript中创建数组有两种方式 (一)使用直接量表示法: var arr4 = []; //创建一个空数组var arr5 = [20]; // 创建 ...
- [JZOJ5778]【NOIP提高A组模拟2018.8.8】没有硝烟的战争
Description 被污染的灰灰草原上有羊和狼.有N只动物围成一圈,每只动物是羊或狼.该游戏从其中的一只动物开始,报出[1,K]区间的整数,若上一只动物报出的数是x,下一只动物可以报[x+1,x+ ...