定义
代理模式(Proxy)为对象提供一种代理用以控制对该对象的访问。
代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象过滤不希望客户看到的内容和服务,或者添加客户需要的额外服务。
结构
代理模式包含如下角色:
- Subject: 抽象主题角色。声明了代理主题角色和真实主题角色的共同接口。
- Proxy: 代理主题角色。可以操作真实主题角色RealSubject。
- RealSubject: 真实主题角色。定义了Proxy所代表的真实对象,在RealSubject中实现了真实的业务操作。
示例
生活中的常见的中介与代理模式存在相似之处。
买家买房子时往往会找到房产中介,说出对房子的要求,然后中介就会在房源列表中找出符合要求的房子,“筛选房源”这个步骤买家可以自己完成,也可以让中介代理完成,但是明显后一种方式就可以减少买家的工作量。此外中介还可以提供代理商谈价格、拟定合同等一条龙服务。
上面场景中涉及的角色划分如下:
Subject: 房屋购买者(Buyer);Proxy: 房产中介(BuyerProxy);RealSubject: 真实买家(RealBuyer)。
1.定义抽象主题角色。
public interface Buyer {
void find();
void agree();
}
2.定义真实主题角色。
public class RealBuyer implements Buyer {
public RealBuyer() {}
public void find() {
System.out.println("买家:提出买房。");
}
public void agree() {
System.out.println("买家:签订合同。");
}
}
3.定义代理主题角色。通过代理类来实现对被代理类的操作,可以对客户类隐藏被代理对象,并附加一些额外操作。
public class BuyerProxy implements Buyer {
private Buyer buyer; //被代理对象
public BuyerProxy(Buyer buyer) {
this.buyer = buyer;
}
public void find() {
//对客户类隐藏被代理类操作
buyer.find();
//代理类附加额外操作
System.out.println("中介:找到房子。");
System.out.println("中介:商谈价格。");
}
public void agree() {
System.out.println("中介:拟定合同。");
buyer.agree();
}
}
4.客户类使用代理对象
public class Client {
public static void main(String[] args) {
Buyer proxy = new BuyerProxy(new RealBuyer());
proxy.find();
proxy.agree();
}
}
此处房屋的购买者(Buyer)实际上是买家(RealBuyer),但是由房产中介(BuyerProxy)代理完成购买房屋的一系列事宜,此时RealBuyer类和BuyerProxy类都实现了Buyer接口。
即客户类看到的是Buyer接口的find()和agree()方法,而实际提供功能的是BuyerProxy类,并非RealBuyer类。BuyerProxy类在本身的find()和agree()方法中调用RealBuyer类的同名方法,并且对其进行了扩展。
输出结果:
买家:提出买房。
中介:找到房子。
中介:商谈价格。
中介:拟定合同。
买家:签订合同。
扩展
常见的几种代理模式(使用场景)
- 远程代理:调用者与被调用者处于不同的网络时,代理对象可以承担网络通信、性能优化等工作,从而隐藏网络细节,减少客户端的功能复杂度。
- 虚拟代理:通过一个创建开销较少的代理对象来代表创建开销很大的对象,占用大量内存或处理复杂的对象将推迟到使用它的时候才创建,一定程度上减少系统开销。
- 保护代理:通过代理对象控制对原始对象的访问,实现对原始对象的不同级别的访问权限。
- 缓冲代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
- 智能引用:在访问对象时执行附加操作。如引用计数、装入内存、检查锁状态等。
静态代理与动态代理
静态代理
在上面的示例中,代理类和被代理类都实现了同一个接口,二者之间的依赖关系在编译时确定。后续每增加一个新的被代理类,有两种可能的做法:
一是代理类增加实现新的代理类对应接口,这样需要代理类实现多个接口,代码量也会越来越大,不便于维护。
二是新增一个对应的代理类,会导致代理类越来越多,管理起来比较麻烦。
由此可知,当系统需要实现比较复杂的业务功能时,静态代理存在开发量大,不便管理的缺点。
动态代理
动态代理中的代理关系是在运行时确定的,代理类不需要显式的在代码中定义为与被代理类实现同一接口的类,代理目标可以在运行时动态指定,具有灵活性。
实现动态代理通常有两种方式,JDK动态代理和CGLib动态代理。
- JDK动态代理
JDK动态代理是Java提供的原生动态代理,Java实现动态代理使用到java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类。
动态代理类需要实现InvocationHandler接口。接口定义如下:
public interface InvocationHandler {
/**
* proxy 代理类对象
* method 代理类具体被调用的方法
* args 被调用方法所需的参数
* 所有对代理对象方法的调用都会转到invoke方法中执行
*/
Object invoke(Object proxy, Method method, Object[] args);
}
Proxy类提供了newProxyInstance方法用于生成一个代理对象。
public class Proxy implements java.io.Serializable {
...
/**
* loader 代理类的类加载器
* interfaces 代理类实现的接口
* h "调用处理器", 对调用的方法进行分派处理的类
*/
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
InvocationHandler h) {
...
}
...
}
在前面静态代理示例中可知,对被代理对象的方法调用实际是通过代理对象的同名方法进行操作的,二者实现了同一个接口;在JDK动态代理中,Proxy.newProxyInstance()生成的代理对象是根据传入的参数动态生产的,其继承了Proxy类并实现了被代理类的接口。
所有对代理对象方法的调用都会转到代理对象的invoke方法中执行,在invoke方法中调用被代理对象的方法,或者添加逻辑实现所需功能。
示例:
1.动态代理类
public class DynamicProxy implements InvocationHandler {
Object target; // 被代理对象
public DynamicProxy(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("agree")) { //添加处理逻辑
agree();
}
Object result = method.invoke(target, args); //调用target的method方法,传入参数args
if(method.getName().equals("find")) {
find();
}
return result;
}
public void find() {
System.out.println("中介:找到房子。");
System.out.println("中介:商谈价格。");
}
public void agree() {
System.out.println("中介:拟定合同。");
}
}
2.客户类创建并使用代理对象
public class DynamicClient {
public static void main(String[] args) {
DynamicProxy buyerProxy = new DynamicProxy(new RealBuyer());
//生成的代理对象实现了Buyer接口
Buyer buyer = (Buyer) Proxy.newProxyInstance(Buyer.class.getClassLoader(),
new Class[]{Buyer.class}, buyerProxy);
//find()和agree()调用会转到代理对象的invoke()方法执行
buyer.find();
buyer.agree();
}
}
输出结果:
买家:提出买房。
中介:找到房子。
中介:商谈价格。
中介:拟定合同。
买家:签订合同。
JDK动态代理能够灵活指定代理目标,可以有效减少代理对象对接口的依赖。但是JDK动态代理只支持代理实现了接口的类。对于未实现接口的类,则需要使用到CGLib动态代理。
- CGLib动态代理
CGLib(Code Generation Library)是一个强大、高性能的代码生成类库,支持在程序运行期间扩展Java类和接口。
CGLib动态代理的原理:动态生成一个被代理类的子类,子类重写被代理类的所有非定义为final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑进行扩展。它比使用java反射的JDK动态代理要快。
CGLib底层使用了ASM(字节码操作框架)来转换字节码生成新的类。
实现CGLib动态代理必须实现MethodInterceptor(方法拦截器)接口:
接口继承了Callback类,这是CGLib实现字节码生成的手段,MethodInterceptor接口是CGLib的6种callback实现之一,也是最常用的一种,类似于AOP编程中的环绕增强。代理类的所有方法调用都会转为执行这个接口中的intercept方法而不是执行原方法。
public interface MethodInterceptor extends Callback {
/**
* obj 增强对象
* method 要拦截的方法
* args 要拦截的方法的参数
* proxy 用于触发父类方法的对象
*/
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
示例:
1.定义被代理类,没有实现任何接口。
public class Buyer {
public Buyer() {}
public void find() {
System.out.println("买家:提出买房。");
}
public void agree() {
System.out.println("买家:签订合同。");
}
}
2.定义动态代理类。动态代理类实现了MethodInterceptor接口,重写intercept()方法中的拦截逻辑,在调用代理对象的方法时,CGLib会回调拦截器进行拦截,实现逻辑织入。
public class CgLibProxy implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if(method.getName().equals("agree")) {
agree();
}
//动态代理类是被代理类的子类,调用父类方法完成真实业务逻辑
//注意*此处使用method.invoke会再次触发拦截,需要使用proxy.invokeSuper
Object result = proxy.invokeSuper(object, args);
if(method.getName().equals("find")) {
find();
}
return result;
}
public void find() {
System.out.println("中介:找到房子。");
System.out.println("中介:商谈价格。");
}
public void agree() {
System.out.println("中介:拟定合同。");
}
}
3.实例化动态代理类。代理对象的生成需要使用到字节码增强器Enhancer,其与java自带的Proxy类相似,可以创建给定类的子类。
使用动态代理工厂生成代理对象。
public class CgLibProxyFactory {
public static Object getCgLibProxy(Object target) {
Enhancer enhancer = new Enhancer(); //字节码增强器,可以用来为无接口的类创建代理
enhancer.setSuperclass(target.getClass()); //作为给定类的子类
enhancer.setCallback(new CgLibProxy()); //织入逻辑
return enhancer.create();
}
}
4.客户类使用代理对象。
public class CgLibClient {
public static void main(String[] args) {
Buyer buyer = (Buyer) CgLibProxyFactory.getCgLibProxy(new Buyer());
buyer.find();
buyer.agree();
}
}
由于CGLib是通过动态生成被代理类的子类并重写父类方法进行逻辑织入,因此无法对定义为final的方法进行代理。
- AOP面向切面编程
动态代理一个典型的应用是AOP(Aspect Oriented Program,面向切面编程)。其与OOP(Object Oriented Program,面向对象编程)的区别在于OOP定义纵向关系,而AOP定义横向关系:
OOP引入封装、继承、多态等概念来建立一种对象间的层次结构;
AOP统一定义分布在代码各处,与核心业务功能无关,但影响多个类的逻辑。如日志记录、权限认证、性能监控等公共行为。AOP使用“横切”技术将这些分散而重复的代码从各处解剖分离,梳理逻辑封装为公共的可重用模块,即切面(Aspect)。
在前面的示例中,对买家、中介等角色的定义属于OOP领域。而对每一位买家买房子之前都增加一个中介代理服务,则属于AOP领域。
使用AOP的好处是每当需要横向对多个类增加同样的功能逻辑时,只需要把功能逻辑写在代理对象中,而不会对业务代码造成影响。
比如对示例中的买家角色,每次完成交易后都打印一个交易完成的日志,不使用代理的情况下,需要在买家角色对应类中增加逻辑。
public class Buyer {
public void find() {
System.out.println("买家:提出买房。");
}
public void agree() {
System.out.println("买家:签订合同。");
}
public void finlog() {
System.out.println("买家:完成交易。");
}
}
使用动态代理后,把“打印日志”这一行为作为一个切面耦合封装,只需要在拦截器的方法中增加打印日志的代码,这样就会与中介代理服务逻辑一起织入目标对象。
public class CgLibProxy implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if(method.getName().equals("agree")) {
agree();
}
Object result = proxy.invokeSuper(object, args); //动态生成的子类调用被代理类方法
if(method.getName().equals("find")) {
find();
}
System.out.println("买家:完成交易。"); //统一打印日志
return result;
}
public void find() {
System.out.println("中介:找到房子。");
System.out.println("中介:商谈价格。");
}
public void agree() {
System.out.println("中介:拟定合同。");
}
}
总结
代理模式为其他对象提供一种代理以控制对这个对象的访问,适用于需要使用比较通用和复杂的对象指针去代替一个简单的指针。
优点:能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。可以通过给代理类增加额外的功能来扩展被代理类的功能,而不用修改被代理类本身,符合开闭原则。
缺点:由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,并且实现代理模式需要额外的工作,有些代理模式的实现非常复杂。