“杏市而外,尚有何人可以分统?亦须早早提拔。办大事者以多多选替手为第一义,满意之选不可得,姑节取其次,以待徐徐教育可也。 ——曾国藩·同治元年四月十二日”
一言
代理模式核心思想是为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。
概述
古今成大事者,必以多选替身为第一要义。“风云人物”们最容易犯的错误是恋权恋栈。在他们自己看来,总是以为“非我不可”,别人都不如我。曾国藩则恰恰相反,他在血腥镇压了太平天国运动之后,主动推进湘军的裁撤,交权、交人,功成身退。
用他自己的话来说就是“办大事者以多多选替手为第一义”。编程如做人,设计模式是一门哲学。代理模式的思想恰恰就是这包含大智慧的“找替身”。
我们在实际的开发过程中,常常遇到开销极大的对象、需要安全控制的对象或者一些远程对象,在面临以上场景的时候总会有一种尾大不掉的感觉。在这种时候如果能对代理模式稍加应用真的就会有种“柳暗花明”的感觉。
我习惯将代理模式分为两种:静态代理、动态代理,当然,很多资料会把动态代理细分为JDK代理和Cglib代理,这次我也会逐一进行拆解。
三叉戟登场
电视剧《三叉戟》真的是我的珍藏剧目了,尤其是预审嫌疑人的桥段,三叉戟分工明确,有的搜集证据,有的旁敲侧击,有的单刀直入…我们不妨以这个场景为例来拆解下代理模式。
静态代理
设计
代码实现
审讯能力
public interface InterrogationDao {void interrogation();
}
预审民警
public class Police implements InterrogationDao{@Overridepublic void interrogation() {System.out.println("预审警察正在提审嫌疑人");}
}
预审文员
public class PoliceProxy implements InterrogationDao{private Police police;public PoliceProxy(Police police) {this.police = police;}@Overridepublic void interrogation() {System.out.println("开始代理...预审文员协助梳理卷宗");police.interrogation();System.out.println("代理结束...预审文员协助归纳供词");}
}
测试
public class Client {public static void main(String[] args) {PoliceProxy policeProxy = new PoliceProxy(new Police());policeProxy.interrogation();}
}
优劣分析
在上例中,文员与预审民警都具有审问能力,而文员更多的是充当一个代理人的角色,预审过程中一些前置和后置的操作都由他来代理完成。预审警察则承担起主要的业务职责。
在不修改目标对象功能的前提下,能够通过代理对象对目标功能进行扩展。但是由于代理对象需要与目标对象实现一样的接口,导致代理类的数量会逐渐膨胀而且一旦接口增加方法,目标对象与代理对象势必要同时进行维护,这其实是很致命的缺陷。
动态代理
相信很多同学看到这里已经想起了一些常用的东西,没错,就是SpringAOP。SpringAOP是代理模式最普遍的应用之一,它正是通过所谓的切面编程实现了极低耦合下的业务嵌入开发。但我们仔细想想,如果是按照上文所描述的静态代理模式,整体架构的耦合度怎么可能低呢?
事实上,SpringAOP的实现并非基于静态代理,而是基于动态代理实现的。
JDK代理
java生态中,泛化关系的实现要么通过类继承、要么通过接口实现。JDK代理在处理动态代理不可避免的泛化问题时,采用了接口实现的这个路线。
在JDK代理模式中,代理对象不需要实现接口,但目标对象需要实现接口,否则就不能实现动态代理。所以说JDK代理也被称为接口代理。
JDK代理基于JDK API,动态的在内存中构建代理对象。
代码实现
审讯能力
public interface InterrogationDao {void interrogation();void sayHello();
}
预审民警
public class Police implements InterrogationDao {@Overridepublic void interrogation() {System.out.println("预审警察正在提审嫌疑人");}@Overridepublic void sayHello() {System.out.println("你这样做,对得起党和人民的信任吗?!");}
}
预审文员筹备处
public class PoliceProxyOffice {private Object target;public PoliceProxyOffice(Object target) {this.target = target;}public Object getProxyInstance(){return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),(proxy,method,args)->{System.out.println("JDK代理开始...预审文员办公室正在梳理卷宗");Object returnVal = method.invoke(target, args);System.out.println("JDK代理提交...预审文员办公室完成供词归纳");return returnVal;});}
}
测试
public class Client {public static void main(String[] args) {InterrogationDao target = new Police();InterrogationDao proxyInstance = (InterrogationDao) new PoliceProxyOffice(target).getProxyInstance();System.out.println("proxyInstance"+proxyInstance);System.out.println("proxyInstanceClass"+proxyInstance.getClass());proxyInstance.sayHello();proxyInstance.interrogation();}
}
分析
我们可以清晰的看到,通过JDK动态代理,我们直接切断了预审民警和代理类的直接耦合关系,整个过程也更加清爽和优雅。随着业务的扩展,即便业务类中的实现细节快速变化或增长,动态代理的模式下并不需要过度关心代理类的实现。依托于反射机制,无论业务类如何扩充,代理类都会巧妙的自动扩充。
Cglib代理
刚刚有说过,java生态下泛化关系的实现要么通过类继承、要么通过接口实现。
静态代理和JDK代理都要求目标对象去实现接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候可以使用目标对象子类来实现代理,这就是Cglib代理的底层原理。
在内存中构建一个子类对象从而实现对目标对象功能的扩展,通过类继承的方式实现代理,这也是为什么Cglib代理又被称为子类代理的原因。
Cglib是一个强大的高性能的代码生成包,它可以在运行期间扩展java类。正是因为这一特性,它才得以被广大的AOP框架应用,这其中就包括大名鼎鼎的SpringAOP。
代码实现
首先要注意,在没有其它依赖的情况下要引入cglib的核心包
预审警察
public class IterrogationDao {public void interrogation(){System.out.println("预审警察正在提审嫌疑人");}public String sayHello(){String msg = "你这样做,对得起党和人民的信任吗?!";System.out.println(msg);return msg;}
}
预审文员筹备处
public class PoliceProxyOffice implements MethodInterceptor {private Object target;public PoliceProxyOffice(Object target) {this.target = target;}public Object getProxyInstance(){Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target.getClass());enhancer.setCallback(this);return enhancer.create();}@Overridepublic Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("Cglib代理开始...预审文员办公室正在梳理卷宗");Object returnVal = method.invoke(target, args);System.out.println("Cglib代理提交...预审文员办公室完成供词归纳");return returnVal;}
}
测试
public class Client {public static void main(String[] args) {IterrogationDao target = new IterrogationDao();IterrogationDao proxyInstance = (IterrogationDao) new PoliceProxyOffice(target).getProxyInstance();proxyInstance.interrogation();String msg = proxyInstance.sayHello();System.out.println("msg:"+msg);}
}
分析
可以很清晰的看到,在基于cglib实现动态代理的过程中,并没有任何接口的实现。这也是JDK代理与Cglib代理的最大区别。Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。
需要注意的是,cglib既然是基于类继承的方式实现了动态代理,那么目标对象的目标方法就必须能够被继承,这也就要求目标方法必然是不可以用final修饰的。同理对于static修饰的方法,动态代理也自然会跳过。如果违反上述规定就会报错:java.lang.IllegalArgumentException
在不使用AOP框架的前提下,如何选择两种动态代理的实现方式也许是我们最关注的问题,通常情况下:
- 如果目标对象需要实现接口,用JDK代理
- 如果目标对象不需要实现接口,用Cglib代理
几种变体
前面我们花了大量的篇幅来讲静态代理和动态代理,相信大家对于这两种编程中最常见的代理模式都有了自己的理解。
事实上代理思想并不仅仅拘泥于上述的几种场景。
下面几种大家耳熟能详的场景都是代理模式的应用,比如说:
- 内网通过代理穿透防火墙实现公网访问的防火墙代理;
- 为了应对各种击穿,大家经常用缓存处理高频数据读取的缓存代理;
- 多线程编程中的多线程同步用到的同步代理等等;
结
编程是一门艺术,它使得每个人对于不同的场景都有不同的解读方式,尤其是针对设计模式而言,它不仅能让我们学会一种解决方案,更能让我们从中领悟人生的哲理。
愿我们都能不忘初心,保持热爱,奔赴山海。
关注我,共同进步,我们的世界中万物皆代码,又不只有代码。希望我的文字能澎湃你的思绪。——Wayne