代理模式的种类、原理及各种实例详解
代理模式是开发中常⽤的⼀种设计模式,每⼀种设计模式的出现都会极⼤的解决某⽅⾯的问题,代理模式也是⼀样,本⽂将会⽤通俗的语⾔来解释什么是代理模式?代理模式的种类、代码⽰例、每种代理模式的优缺点和代理模式适⽤的场景。
代理模式是什么?
⾸先我们⽤⼀个⼩故事来描述下什么是代理模式,这会让你更快的理解代理模式的相关⾓⾊,为后⾯的各种代理打下基础。
假如,你是⼀个⼤明星,⼈⽓很旺,粉丝也特别多。因为⼈⽓⾼,所以很多商家想你代⾔⼴告,但是想要你代⾔的⼈特别多,每个商家你都需要进⾏商务洽谈,如果聊得不错决定合作,后续还需要签署很多合同⽂件、记录、备案等。这么多商家你代⾔,其中你只能选择其中⼏个代⾔,即便只选择⼏个,你也忙不过来。于是你就想了⼀个办法,给⾃⼰了⼀个经纪⼈,给经纪⼈制定标准让他去对接各商家,经纪⼈做事很认真负责,不仅剔除了很多不良的商家还对有资格的商家做了详细的记录,记录商家的代⾔费、商家详细信息、商家合同等信息。于是在商务代⾔这件事情上你只需要专⼼代⾔拍⼴告,其他的事情交由经纪⼈⼀并处理。
分析下整个事件,可以知道,经纪⼈就是代理⼈,明星就是被代理⼈。在明星的⼴告代⾔中,经纪⼈处理的商务洽谈和签约环节相当于代理,这就是代理模式在实际⽣活中的简单案例。
其实不⽌经纪⼈和明星,⽣活中还有很多⾏为本质就是代理模式,⽐如:某些⼤牌的饮料三级代理销售、酒⽔的省市县的代理⼈、三国时曹操挟天⼦以令诸侯等等。
说了这么多案例,都是关于代理模式的,那既然这么多⼈都在⽤代理模式,那代理模式⼀定解决了⽣活中的某些棘⼿的问题,那究竟是什么问题呢?
在明星和经纪⼈这个案例中,因为把代⾔这个商业⾏为做了细分,让明星团队中每个⼈负责代⾔的⼀部分,使每⼈只需要专注于⾃⼰的事,提⾼每个⼈的专业度的同时,也提⾼了效率,这就叫专业,专⼈专事。
因为经纪⼈专注⼴告代⾔的代理⾏为,商业经验丰富,所以经纪⼈也可以⽤他的专业知识为其他明星做⼴告代⾔的代理,这就叫能⼒复⽤。
那么,如何使⽤代码展⽰经纪⼈代理明星的⼴告⾏为呢?这其中有是如何运⽤代理模式的呢?
类⽐上⾯的明星和经纪⼈的例⼦:
假如有个明星类,我们想在调⽤明星类的代⾔⽅法之前做⼀些其他操作⽐如权限控制、记录等,那么就需要⼀个中间层,先执⾏中间层,在执⾏明星类的代⾔⽅法。
那讲到这⾥,想必⼜有⼈问,直接在明星类上加⼀个权限控制、记录等⽅法不就⾏了么,为什么⾮要⽤代理呢?
这就是本⽂最重要的⼀个核⼼知识,程序设计中的⼀个原则:类的单⼀性原则。这个原则很简单,就是每个类的功能尽可能单⼀,在这个案例中让明星类保持功能单⼀,就是对代理模式的通俗解释。
那为什么要保持类的功能单⼀呢?
因为只有功能单⼀,这个类被改动的可能性才会最⼩,其他的操作交给其他类去办。在这个例⼦中,如果在明星类⾥加上权限控制功能,那么明星类就不再是单⼀的明星类了,是明星加经纪⼈两者功能的合并类。
如果我们只想⽤权限控制功能,使⽤经纪⼈的功能给其他明星筛选⼴告商家,如果两者合并,就要创建这个合并类,但是我们只使⽤权限功能,这就导致功能不单⼀,长期功能的累加会使得代码极为混乱,难以复⽤。
所以类的单⼀性原则和功能复⽤在代码设计上很重要,这也是使⽤代理模式的核⼼。
⽽这整个过程所涉及到的⾓⾊可以分为四类:
1. 主题接⼝:类⽐代⾔这类⾏为的统称,是定义代理类和真实主题的公共对外⽅法,也是代理类代理真实主题的⽅法;
2. 真实主题:类⽐明星这个⾓⾊,是真正实现业务逻辑的类;
3. 代理类:类⽐经纪⼈这个⾓⾊,是⽤来代理和封装真实主题;
4. 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 = quest();
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(); //使⽤代⾥
}
}
可以看到,主题接⼝是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⽅法可以⽣成某个对象的代理对象,该⽅法需要三个参数:
1. 类装载器【⼀般我们使⽤的是被代理类的装载器】
2. 指定接⼝【指定要被代理类的接⼝】
3. 代理对象的⽅法⾥⼲什么事【实现handler接⼝】
初次看见会有些不理解,没关系,下⾯⽤⼀个实例来详细展⽰JDK动态代理的实现:
代理类的实现
package com.shuai.proxy.jdkproxy;
import com.shuai.proxy.staticproxy.DBQuery;
import com.shuai.proxy.IDBQuery;
import flect.InvocationHandler;
import flect.Method;
import flect.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".Name())) {
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) wProxyInstance(
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 = ateProxy();
}
}
⽤debug的⽅式启动,可以看到⽅法被代理到代理类中实现,在代理类中执⾏真实主题的⽅法前后可以进⾏很多操作。
虽然这种⽅法实现看起来很⽅便,但是细⼼的同学应该也已经观察到了,JDK动态代理技术的实现是必须要⼀个接⼝才⾏的,所以JDK动态代理的优缺点也⾮常明显:
优点:
不需要为真实主题写⼀个形式上完全⼀样的封装类,减少维护成本;
可以在运⾏时制定代理类的执⾏逻辑,提升系统的灵活性;
缺点:
JDK动态代理,真实主题必须实现的主题接⼝,如果真实主题没有实现主图接⼝,或者没有主题接⼝,则不能⽣成代理对象。
由于必须要有接⼝才能使⽤JDK的动态代理,那是否有⼀种⽅式可以没有接⼝只有真实主题实现类也可以使⽤动态代理呢?这就是第⼆种动态代理:CGLIB;
CGLIB动态代理
使⽤CGLIB⽣成动态代理,⾸先需要⽣成Enhancer类实例,并指定⽤于处理代理业务的回调类。在ate()⽅法中,会使⽤DefaultGeneratorStrategy.Generate()⽅法⽣成动态代理类的字节码,并保存在 byte 数组中。接着使⽤ReflectUtils.defineClass()⽅法,通过反射,调⽤ClassLoader.defineClass()⽅法,将字节码装载到 ClassLoader 中,完成类的加载。最后使⽤wInstance()⽅法,通过反射,⽣成动态类的实例,并返回该实例。基本流程是根据指定的回调类⽣成 Class 字节码—通过defineClass()将字节码定义为类—使⽤反射机制⽣成该类的实例。
真实主题
package com.libproxy;
class BookImpl {
void addBook() {
System.out.println("增加图书的普通⽅法...");
}
}
代理类
package com.libproxy;
import lib.proxy.Enhancer;
import lib.proxy.MethodInterceptor;
import lib.proxy.MethodProxy;
import flect.Method;
public class BookImplProxyLib implements MethodInterceptor {
/**
* 创建代理对象
*
* @return
*/
Object getBookProxyImplInstance() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BookImpl.class);
// 回调⽅法
enhancer.setCallback(this);
/
/ 创建代理对象
ate();
}
// 回调⽅法
@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.libproxy;
public class Test {
public static void main(String[] args) {
BookImplProxyLib cglib = new BookImplProxyLib();
BookImpl bookCglib = (BookImpl) BookProxyImplInstance();
bookCglib.addBook();
}
}
CGLIB的优缺点
优点:
CGLIB通过继承的⽅式进⾏代理、⽆论⽬标对象没有没实现接⼝都可以代理,弥补了JDK动态代理的缺陷。
缺点:
1. CGLib创建的动态代理对象性能⽐JDK创建的动态代理对象的性能⾼不少,但是CGLib在创建代理对象时所花费的时间却⽐JDK多得多,所以对于单例的对象,因为⽆需频
繁创建对象,⽤CGLib合适,反之,使⽤JDK⽅式要更为合适⼀些。
2. 由于CGLib由于是采⽤动态创建⼦类的⽅法,对于final⽅法,⽆法进⾏代理。
代理模式的应⽤场合
代理模式有多种应⽤场合,如下所述:
1. 远程代理,也就是为⼀个对象在不同的地址空间提供局部代表,这样可以隐藏⼀个对象存在于不同地址空间的事实。⽐如说 WebService,当我们在应⽤程序的项⽬中加⼊
⼀个 Web 引⽤,引⽤⼀个 WebService,此时会在项⽬中声称⼀个 WebReference 的⽂件夹和⼀些⽂件,这个就是起代理作⽤的,这样可以让那个客户端程序调⽤代理解决远程访问的问题;
2. 虚拟代理,是根据需要创建开销很⼤的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,⽐如打开⼀个⽹页,这个⽹页⾥⾯包含了⼤量
的⽂字和图⽚,但我们可以很快看到⽂字,但是图⽚却是⼀张⼀张地下载后才能看到,那些未打开的图⽚框,就是通过虚拟代⾥来替换了真实的图⽚,此时代理存储了真实图⽚的路径和尺⼨;
3. 安全代理,⽤来控制真实对象访问时的权限。⼀般⽤于对象应该有不同的访问权限的时候;
4. 指针引⽤,是指当调⽤真实的对象时,代理处理另外⼀些事。⽐如计算真实对象的引⽤次数,这样当该对象没有引⽤时,可以⾃动释放它,或当第⼀次引⽤⼀个持久对象
时,将它装⼊内存,或是在访问⼀个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问⼀个对象时附加⼀些内务处理;
5. 延迟加载,⽤代理模式实现延迟加载的⼀个经典应⽤就在 Hibernate 框架⾥⾯。当 Hibernate 加载实体 bean 时,并不会⼀次性将数据库所有的数据都装载。默认情况下,
它会采取延迟加载的机制,以提⾼系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使⽤代理拦截原有的 getter ⽅法,在真正使⽤对象数据时才去数据库或者其他第三⽅组件加载实际的数据,从⽽提升系统性能。
参考: