代理模式是开发中经常使用的一种设计模式,每一种设计模式的出现都会极大的解决某方面的问题,代理模式也是同样,本文将会用通俗的语言来解释什么是代理模式?代理模式的种类、代码示例、每种代理模式的优缺点和代理模式适用的场景。html
代理模式是什么?
首先咱们用一个小故事来描述下什么是代理模式,这会让你更快的理解代理模式的相关角色,为后面的各类代理打下基础。java
假如,你是一个大明星,人气很旺,粉丝也特别多。由于人气高,因此不少商家想找你代言广告,可是想要找你代言的人特别多,每一个商家你都须要进行商务洽谈,若是聊得不错决定合做,后续还须要签署不少合同文件、记录、备案等。这么多商家找你代言,其中你只能选择其中几个代言,即使只选择几个,你也忙不过来。因而你就想了一个办法,给本身找了一个经纪人,给经纪人制定标准让他去对接各商家,经纪人作事很认真负责,不只剔除了不少不良的商家还对有资格的商家作了详细的记录,记录商家的代言费、商家详细信息、商家合同等信息。因而在商务代言这件事情上你只须要专心代言拍广告,其余的事情交由经纪人一并处理。面试
分析下整个事件,能够知道,经纪人就是代理人,明星就是被代理人。在明星的广告代言中,经纪人处理的商务洽谈和签约环节至关于代理,这就是代理模式在实际生活中的简单案例。spring
其实不止经纪人和明星,生活中还有不少行为本质就是代理模式,好比:某些大牌的饮料三级代理销售、酒水的省市县的代理人、三国时曹操挟天子以令诸侯等等。数据库
说了这么多案例,都是关于代理模式的,那既然这么多人都在用代理模式,那代理模式必定解决了生活中的某些棘手的问题,那到底是什么问题呢?编程
在明星和经纪人这个案例中,由于把代言这个商业行为作了细分,让明星团队中每一个人负责代言的一部分,使每人只须要专一于本身的事,提升每一个人的专业度的同时,也提升了效率,这就叫专业,专人专事。设计模式
由于经纪人专一广告代言的代理行为,商业经验丰富,因此经纪人也能够用他的专业知识为其余明星作广告代言的代理,这就叫能力复用。数组
那么,如何使用代码展现经纪人代理明星的广告行为呢?这其中有是如何运用代理模式的呢?安全
类比上面的明星和经纪人的例子:多线程
假若有个明星类,咱们想在调用明星类的代言方法以前作一些其余操做好比权限控制、记录等,那么就须要一个中间层,先执行中间层,在执行明星类的代言方法。
那讲到这里,想必又有人问,直接在明星类上加一个权限控制、记录等方法不就好了么,为何非要用代理呢?
这就是本文最重要的一个核心知识,程序设计中的一个原则:类的单一性原则。这个原则很简单,就是每一个类的功能尽量单一,在这个案例中让明星类保持功能单一,就是对代理模式的通俗解释。
那为何要保持类的功能单一呢?
由于只有功能单一,这个类被改动的可能性才会最小,其余的操做交给其余类去办。在这个例子中,若是在明星类里加上权限控制功能,那么明星类就再也不是单一的明星类了,是明星加经纪人二者功能的合并类。
若是咱们只想用权限控制功能,使用经纪人的功能给其余明星筛选广告商家,若是二者合并,就要建立这个合并类,可是咱们只使用权限功能,这就致使功能不单一,长期功能的累加会使得代码极为混乱,难以复用。
因此类的单一性原则和功能复用在代码设计上很重要,这也是使用代理模式的核心。
而这整个过程所涉及到的角色能够分为四类:
- 主题接口:类比代言这类行为的统称,是定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
- 真实主题:类比明星这个角色,是真正实现业务逻辑的类;
- 代理类:类比经纪人这个角色,是用来代理和封装真实主题;
- Main:类比商家这个角色,是客户端,使用代理类和主题接口完成一些工做;
在java语言的发展中,出现了不少种代理方式,这些代理方式能够分类为两类:静态代理和动态代理,下面咱们就结合代码实例解释下,各种代理的几种实现方式,其中的优缺点和适用的场景。
静态代理
主题接口
package com.shuai.proxy; public interface IDBQuery { String request(); }
真实主题
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class DBQuery implements IDBQuery { public DBQuery() { try { Thread.sleep(1000);//假设数据库链接等耗时操做 } catch (InterruptedException ex) { ex.printStackTrace(); } } @Override public String request() { return "request string"; } }
代理类
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class DBQueryProxy implements IDBQuery { private DBQuery real = null; @Override public String request() { // TODO Auto-generated method stub System.out.println("在此以前,记录下什么东西吧....."); //在真正须要的时候才能建立真实对象,建立过程可能很慢 if (real == null) { real = new DBQuery(); }//在多线程环境下,这里返回一个虚假类,相似于 Future 模式 String result = real.request(); System.out.println("在此以后,记录下什么东西吧....."); return result; } }
Main客户端
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class Test { public static void main(String[] args) { IDBQuery q = new DBQueryProxy(); //使用代里 q.request(); //在真正使用时才建立真实对象 } }
能够看到,主题接口是IDBQuery,真实主题是DBQuery 实现了IDBQuery接口,代理类是DBQueryProxy,在代理类的方法里实现了DBQuery类,而且在代码里写死了代理先后的操做,这就是静态代理的简单实现,能够看到静态代理的实现优缺点十分明显。
静态代理的优缺点:
优势:
使得真实主题处理的业务更加纯粹,再也不去关注一些公共的事情,公共的业务由代理来完成,实现业务的分工,公共业务发生扩展时变得更加集中和方便。
缺点:
这种实现方式很直观也很简单,但其缺点是代理类必须提早写好,若是主题接口发生了变化,代理类的代码也要随着变化,有着高昂的维护成本。
针对静态代理的缺点,是否有一种方式弥补?可以不须要为每个接口写上一个代理方法,那就动态代理。
动态代理
动态代理,在java代码里动态代理类使用字节码动态生成加载
技术,在运行时生成加载类。
生成动态代理类的方法不少,好比:JDK 自带的动态处理、CGLIB、Javassist、ASM 库。
- JDK 的动态代理使用简单,它内置在 JDK 中,所以不须要引入第三方 Jar 包,但相对功能比较弱。
- CGLIB 和 Javassist 都是高级的字节码生成库,整体性能比 JDK 自带的动态代理好,并且功能十分强大。
- ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用 Java bytecode 编程,对开发人员要求最高,固然,也是性能最好的一种动态代理生成工具。但 ASM 的使用很繁琐,并且性能也没有数量级的提高,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,若是不是在对性能有苛刻要求的场合,仍是推荐 CGLIB 或者 Javassist。
这里介绍两种很是经常使用的动态代理技术,面试时也会经常用到的技术:JDK 自带的动态处理
、CGLIB
两种。
jDK动态代理
Java提供了一个Proxy类,使用Proxy类的newInstance方法能够生成某个对象的代理对象,该方法须要三个参数:
-
类装载器【通常咱们使用的是被代理类的装载器】
-
指定接口【指定要被代理类的接口】
-
代理对象的方法里干什么事【实现handler接口】
初次看见会有些不理解,不要紧,下面用一个实例来详细展现JDK动态代理的实现:
代理类的实现
package com.shuai.proxy.jdkproxy; import com.shuai.proxy.staticproxy.DBQuery; import com.shuai.proxy.IDBQuery; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DBQueryHandler implements InvocationHandler { private IDBQuery realQuery = null;//定义主题接口 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //若是第一次调用,生成真实主题 if (realQuery == null) { realQuery = new DBQuery(); } if ("request".equalsIgnoreCase(method.getName())) { System.out.println("调用前作点啥,助助兴....."); Object result = method.invoke(realQuery, args); System.out.println("调用后作点啥,助助兴....."); return result; } else { // 若是不是调用request方法,返回真实主题完成实际的操做 return method.invoke(realQuery, args); } } static IDBQuery createProxy() { IDBQuery proxy = (IDBQuery) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), //当前类的类加载器 new Class[]{IDBQuery.class}, //被代理的主题接口 new DBQueryHandler() // 代理对象,这里是当前的对象 ); return proxy; } }
Main客户端
package com.shuai.proxy.jdkproxy; import com.shuai.proxy.IDBQuery; public class Test { // 客户端测试方法 public static void main(String[] args) { IDBQuery idbQuery = DBQueryHandler.createProxy(); idbQuery.request(); } }
用debug的方式启动,能够看到方法被代理到代理类中实现,在代理类中执行真实主题的方法先后能够进行不少操做。
虽然这种方法实现看起来很方便,可是细心的同窗应该也已经观察到了,JDK动态代理技术的实现是必需要一个接口才行的,因此JDK动态代理的优缺点也很是明显:
优势:
- 不须要为真实主题写一个形式上彻底同样的封装类,减小维护成本;
- 能够在运行时制定代理类的执行逻辑,提高系统的灵活性;
缺点:
- JDK动态代理,真实主题 必须实现的主题接口,若是真实主题 没有实现主图接口,或者没有主题接口,则不能生成代理对象。
因为必需要有接口才能使用JDK的动态代理,那是否有一种方式能够没有接口只有真实主题实现类也可使用动态代理呢?这就是第二种动态代理:CGLIB
;
CGLIB动态代理
使用 CGLIB
生成动态代理,首先须要生成 Enhancer
类实例,并指定用于处理代理业务的回调类。在 Enhancer.create()
方法中,会使用 DefaultGeneratorStrategy.Generate()
方法生成动态代理类的字节码,并保存在 byte 数组中。接着使用 ReflectUtils.defineClass()
方法,经过反射,调用 ClassLoader.defineClass()
方法,将字节码装载到 ClassLoader 中,完成类的加载。最后使用 ReflectUtils.newInstance()
方法,经过反射,生成动态类的实例,并返回该实例。基本流程是根据指定的回调类生成 Class 字节码—经过 defineClass()
将字节码定义为类—使用反射机制生成该类的实例。
真实主题
package com.shuai.proxy.cglibproxy; class BookImpl { void addBook() { System.out.println("增长图书的普通方法..."); } }
代理类
package com.shuai.proxy.cglibproxy; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class BookImplProxyLib implements MethodInterceptor { /** * 建立代理对象 * * @return */ Object getBookProxyImplInstance() { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(BookImpl.class); // 回调方法 enhancer.setCallback(this); // 建立代理对象 return enhancer.create(); } // 回调方法 @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("开始..."); proxy.invokeSuper(obj, args); System.out.println("结束..."); return null; } }
Main客户端
package com.shuai.proxy.cglibproxy; public class Test { public static void main(String[] args) { BookImplProxyLib cglib = new BookImplProxyLib(); BookImpl bookCglib = (BookImpl) cglib.getBookProxyImplInstance(); bookCglib.addBook(); } }
CGLIB的优缺点
优势:
CGLIB经过继承的方式进行代理、不管目标对象没有没实现接口均可以代理,弥补了JDK动态代理的缺陷。
缺点:
- CGLib建立的动态代理对象性能比JDK建立的动态代理对象的性能高很多,可是CGLib在建立代理对象时所花费的时间却比JDK多得多,因此对于单例的对象,由于无需频繁建立对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
- 因为CGLib因为是采用动态建立子类的方法,对于final方法,没法进行代理。
代理模式的应用场合
代理模式有多种应用场合,以下所述:
- 远程代理,也就是为一个对象在不一样的地址空间提供局部表明,这样能够隐藏一个对象存在于不一样地址空间的事实。好比说 WebService,当咱们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理做用的,这样可让那个客户端程序调用代理解决远程访问的问题;
- 虚拟代理,是根据须要建立开销很大的对象,经过它来存放实例化须要很长时间的真实对象。这样就能够达到性能的最优化,好比打开一个网页,这个网页里面包含了大量的文字和图片,但咱们能够很快看到文字,可是图片倒是一张一张地下载后才能看到,那些未打开的图片框,就是经过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
- 安全代理,用来控制真实对象访问时的权限。通常用于对象应该有不一样的访问权限的时候;
- 指针引用,是指当调用真实的对象时,代理处理另一些事。好比计算真实对象的引用次数,这样当该对象没有引用时,能够自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其余对象不能改变它。这些都是经过代理在访问一个对象时附加一些内务处理;
- 延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库全部的数据都装载。默认状况下,它会采起延迟加载的机制,以提升系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其余第三方组件加载实际的数据,从而提高系统性能。
参考: 代理模式原理及实例讲解 为何使用代理模式