代理模式的原理、种类和各种实例详解

代理模式是开发中常用的一种设计模式,每一种设计模式的出现都会极大的解决某方面的问题,代理模式也是一样,本文将会用通俗的语言来解释什么是代理模式?代理模式的种类、代码示例、每种代理模式的优缺点和代理模式适用的场景。

代理模式是什么?

首先我们用一个小故事来描述下什么是代理模式,这会让你更快的理解代理模式的相关角色,为后面的各种代理打下基础。

假如,你是一个大明星,人气很旺,粉丝也特别多。因为人气高,所以很多商家想找你代言广告,但是想要找你代言的人特别多,每个商家你都需要进行商务洽谈,如果聊得不错决定合作,后续还需要签署很多合同文件、记录、备案等。这么多商家找你代言,其中你只能选择其中几个代言,即便只选择几个,你也忙不过来。于是你就想了一个办法,给自己找了一个经纪人,给经纪人制定标准让他去对接各商家,经纪人做事很认真负责,不仅剔除了很多不良的商家还对有资格的商家做了详细的记录,记录商家的代言费、商家详细信息、商家合同等信息。于是在商务代言这件事情上你只需要专心代言拍广告,其他的事情交由经纪人一并处理。

分析下整个事件,可以知道,经纪人就是代理人,明星就是被代理人。在明星的广告代言中,经纪人处理的商务洽谈和签约环节相当于代理,这就是代理模式在实际生活中的简单案例。

其实不止经纪人和明星,生活中还有很多行为本质就是代理模式,比如:某些大牌的饮料三级代理销售、酒水的省市县的代理人、三国时曹操挟天子以令诸侯等等。

说了这么多案例,都是关于代理模式的,那既然这么多人都在用代理模式,那代理模式一定解决了生活中的某些棘手的问题,那究竟是什么问题呢?

在明星和经纪人这个案例中,因为把代言这个商业行为做了细分,让明星团队中每个人负责代言的一部分,使每人只需要专注于自己的事,提高每个人的专业度的同时,也提高了效率,这就叫专业,专人专事。因为经纪人专注广告代言的代理行为,商业经验丰富,所以经纪人也可以用他的专业知识为其他明星做广告代言的代理,这就叫能力复用。

那么,如何使用代码展示经纪人代理明星的广告行为呢?这其中有是如何运用代理模式的呢?

类比上面的明星和经纪人的例子:

假如有个明星类,我们想在调用明星类的代言方法之前做一些其他操作比如权限控制、记录等,那么就需要一个中间层,先执行中间层,在执行明星类的代言方法。

那讲到这里,想必又有人问,直接在明星类上加一个权限控制、记录等方法不就行了么,为什么非要用代理呢?

这就是本文最重要的一个核心知识,程序设计中的一个原则:类的单一性原则。这个原则很简单,就是每个类的功能尽可能单一,在这个案例中让明星类保持功能单一,就是对代理模式的通俗解释。

那为什么要保持类的功能单一呢?

因为只有功能单一,这个类被改动的可能性才会最小,其他的操作交给其他类去办。在这个例子中,如果在明星类里加上权限控制功能,那么明星类就不再是单一的明星类了,是明星加经纪人两者功能的合并类。

如果我们只想用权限控制功能,使用经纪人的功能给其他明星筛选广告商家,如果两者合并,就要创建这个合并类,但是我们只使用权限功能,这就导致功能不单一,长期功能的累加会使得代码极为混乱,难以复用。

所以类的单一性原则和功能复用在代码设计上很重要,这也是使用代理模式的核心。

而这整个过程所涉及到的角色可以分为四类:

主题接口:类比代言这类行为的统称,是定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;真实主题:类比明星这个角色,是真正实现业务逻辑的类;代理类:类比经纪人这个角色,是用来代理和封装真实主题;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 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。

参考:《代理模式原理及实例讲解 》《为什么使用代理模式》

欢迎关注我的:java之旅