以前看了很多博客文章,一段时间后,对JDK动态代理还是模模糊糊。这次从思考方式上做一个梳理和推理,彻底搞懂动态代理模式的诞生和意义。
这篇文章从问题和自己的思维路线出发,一步步推理解决方案的演化过程,直至动态代理模式。
背景问题
程序员菜鸟经常遇到,想统计一个操作函数的执行时间,通常会这样做
long t1 = System.currentTimeMillis(); //计时开始
int count = readProcessData();
long t2 = System.currentTimeMillis(); //计时结束
long time = t2-t1;// 统计耗时
logger.info("[Cost Time=" + time + "ms]");
当需要计时的操作越来越多,不自觉就会cv大量计时代码;造成代码重复冗余不易维护。如果想修改计时单位、或者log格式,就会是一件相当麻烦的体力活。
如何将这些重复的计时代码抽离出来,统一维护并且只写一次?
类似的,还有权限管理、事务等场景,都是相似的需求模式。
解决方法的进化过程
下面描述各种解决方案
菜鸟方案0:提取到父类继承 ->组合
(1)提取到父类 继承
最先想到:将公共代码提取出来,放到父类中,由父类负责计时,子类专心做数据库操作。就像跑步运动员训练的时候,教练负责喊开始和计时、运动员只需要专心跑步。
//方案0:父类计时、子类做业务
public abstract class AbstractDataService {
/**
* 父类: 处理前后 做时间统计、日志输出等等...
*/
public void processData(){
long t1 = System.currentTimeMillis();
int count = realProcessData();
long t2 = System.currentTimeMillis();
long time = t2-t1;
logger.info("[Cost Time=" + time + "ms]");
}
public abstract int realProcessData() ;//待实现
}
//子类:专注数据处理业务
public class MyDataService extends AbstractDataService{
@Override
public int realProcessData(){
// 具体数据处理业务
}
}
//调用
AbstractDataService dataService = new MyDataService();
dataService.processData();
总结
优点:计时代码统一维护,避免了重复。
缺点:首先,使用上,想要计时功能的类,都得继承 AbstractDataService。Java是单继承语言,这种方法必然很多不灵活。其次,通过继承得到计时功能,不符合“继承”的is-a的内涵。
(2)组合
既然不能用继承,就用组合吧:
将计时器功能封装到一个独立类 TimerService 中,MyDataService类包含一个计时器类实例,在 MyDataService.readProcessData 方法前后加上计时函数。
总结
优点:计时功能解耦到单独类中,方便各个需求类使用,无需继承。
缺点:每个需要“计时”的类都要内嵌一个 TimeService 实例,同时修改自身函数。相当于原生类需要内置一个计时器,这对原生类入侵性太大,这是致命性缺陷。
教科书方案1:静态代理
针对上面菜鸟方案的缺点,教科书方案的目标:
- 在不修改原有类情况下,给他们增加 “计时” 功能。
-
尽量不修改调用端的调用代码。
下面看看静态代理实现方案类图,
加入一个代理类ProxySubject :
(1)实现原生类接口:这样调用端可以像调用原生类一样调用代理类。
(2)包含一个原生类实例: 调用原生类功能
(3)包含自己的功能逻辑(doOtherthing)。
//使用静态代理 给原生类增加“计时”功能
//接口类
public interface IDataService{
public void processData();
}
//业务实现类:被代理的类
public class RealDataService implement IDataService{
public void processData(){
System.out.println("i am doing business data process.");
}
}
//代理类
public class ProxyDataService implement IDataService{
private RealDataService realDataService;
//构造函数: 根据实际对象,构造一个代理对象
public void ProxyDataService(RealDataService srv){
this.realDataService = srv;
}
public void processData(){
startTimer();//计时开始
realDataService.processData();
stopTimer();//计时结束
}
public void startTimer(){
//...
}
public void stopTimer(){
//...
}
}
//客户程序调用
RealDataService realDataService = new RealDataService();
IDataService proxy = new ProxyDataService(realDataService);
proxy.processData();//像调用真实类一样,调用代理类
好了,这就是静态代理的应用。 对原生类无任何入侵,调用端的修改也很小,新功能需求都在代理类中实现了。是不是很优雅很完美?
更进一步,考虑更多的需求
- 如果有新方法,例如 fetchData、storeData.... 需要计时功能,我们会修改代理类,在这些方法调用前后环绕上计时器。
但是,这也是一种代码重复,能否把需要计时功能的函数、当做参数,传递给计时器代理类呢?(Method,反射) - 如果有一个新类RunMan ,继承自IHuman接口,想要计时功能。 还想要权限检查等功能。
那么,要新建一个代理类,继承IHuman,为RunMan计时;同时,可以把计时、 权限检查功能独立出来,作为专门的服务提供类TimerSerivce、AuthorChecker等。
新功能扩展后,类图如下:
总结
优点:
1 功能解耦:业务处理类都独立出来了,包括计时器、权限检查、RealSubject、RunMan等。
2 调用端改动小、入侵性小:由于实现了统一接口,client可以像调用真实类一样,调用代理类。
3 原生类无需改动、无入侵性。
缺点:类膨胀,无意义的代理类。每增加一个接口功能类RunMan,就要增加一个相应的代理类。如果大量使用静态代理,造成类膨胀。同时,代理类仅仅起到中介作用,意义不大,带来系统结构臃肿和松散。
进化方案2:自己实现的动态代理
为了避免静态代理的问题,产生了动态代理。在系统执行中,需要代理的地方根据接口以及 被代理类 MyDataServiceImpl 动态生成代理Proxy类,用完后销毁,避免类冗余问题。
具体实现,参见: 深入剖析动态代理--从静态代理到动态代理的演化 http://www.cnblogs.com/gccbuaa/p/7141182.html
总结
手工实现代理类的动态生成,有大量的非业务代码,这部分需要进一步抽象,就有了如今的Jkd或者是CGLib动态代理。
进化方案3.1:JDK动态代理
为了克服静态代理的缺点,JDK将代理类的动态创建代码 抽象出来,放到了java.lang.Proxy类中。这样,无意义的代理类由jvm动态创建,程序员只需要关注业务逻辑即可。
下面先介绍如何使用:
//接口类
public interface IDataService{
public void processData();
}
//业务实现类:被代理的类
public class RealDataService implement IDataService{
public void processData(){
System.out.println("i am doing business data process.");
}
}
// 计时器功能类:继承自InvocationHandler
public class TimerService implements InvocationHandler {
private Object realObject;//真实功能对象
public TimerService(Object realObject){
this.realObject = realObject;
}
public void before(){
System.out.println("before ...");
}
public void after(){
System.out.println("after ...");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
before();
method.invoke(realObject, args);//调用真实功能类的方法
after();
return null;
}
}
//客户端调用
RealDataService realDataService = new RealDataService();
TimerService timerService = new TimerService(realDataService);
// 以下是一次性生成代理对象proxy
Class realClass = RealDataService.class;
IDataService proxy = (IDataService)Proxy.newProxyInstance(
realClass .getClassLoader(), realClass .getInterfaces(), timerService);
proxy.processData();//调用业务函数
实现原理
本节主要参考 [深入理解Java Proxy机制] (http://blog.csdn.net/rokii/article/details/4046098/)
动态代理其实就是Java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class byte,该class会继承Proxy类,并实现所有你指定的接口(您在参数中传入的接口数组);
然后再利用您指定的classloader将 class byte加载进系统,最后生成这样一个类的对象,并初始化该对象的一些值,如invocationHandler,以及所有的接口对应的Method成员。 初始化之后将对象返回给调用的客户端。这样客户端拿到的就是一个实现你所有的接口的Proxy对象。
举例来说:Proxy.newProxyInstance方法会做如下几件事:
- 根据传入的第二个参数interfaces动态生成一个类,实现interfaces中的接口,该例中即RealDataService接口IDataService. processData 方法。并且继承了Proxy类,重写了hashcode,toString,equals等三个方法。具体实现可参看 ProxyGenerator.generateProxyClass(...); 该例中生成了$Proxy0类
- 通过传入的第一个参数classloder将刚生成的类加载到jvm中。即将$Proxy0类load
- 利用第三个参数,调用$Proxy0的$Proxy0(InvocationHandler)构造函数 创建$Proxy0的对象,并且用interfaces参数遍历其所有接口的方法,并生成Method对象初始化对象的几个Method成员变量
- 将$Proxy0的实例返回给客户端。
现在好了,我们再看客户端怎么调就清楚了
1.客户端拿到的是$Proxy0的实例对象,由于$Proxy0继承了IDataService,因此转化为IDataService没任何问题。
IDataService proxy = (IDataService)Proxy.newProxyInstance(...);
- proxy.processData();
实际上调用的是
$Proxy0.processData() -->invocationHandler.invoke()--> realService.processData();
总结
JDK动态代理克服了之前所有方案的缺点:
将代理类中的业务功能解耦出来,无意义的代理类使用动态生成方式。
唯一的缺点:局限在只能对实现了某个接口的类,做动态代理。对于没有接口的类本身,怎么办?
这需要cglib
进化分支3.2:cglib动态代理
cglib 是 动态创建 原生业务类的子类,对其中方法调用进行拦截,植入“计时”等功能。
其基本思想 是 继承+动态创建。关于继承,在菜鸟方案中说过提取“计时”功能到父类的方式。cglib的继承是反过来,继承被代理类,在子类中实现扩展。
比如下面示例代码:
//原生类:数据处理业务
public class MyDataService {
public int realProcessData(){
// 具体数据处理业务
}
}
//继承自原生类:实现计时功能
public class ChildDataService extends MyDataService {
public int realProcessData(){
before(); //事前
super.realProcessData(); //调用父类方法:实际数据处理
after(); //事后
}
}
//调用方
MyDataService srv = new ChildDataService();
srv.processData();
在cglib中,代理类也是继承原生业务类;并且,使用asm字节码操纵框架动态创建代理类。相比JDK动态代理使用反射,asm方法直接修改字节码,效率更高。
下面是cglib使用参考代码,来自参考文章[5]
//1、定义业务逻辑
public class UserServiceImpl {
public void add() {
System.out.println("This is add service");
}
public void delete(int id) {
System.out.println("This is delete service:delete " + id );
}
}
//2、实现MethodInterceptor接口,定义方法的拦截器
public class MyMethodInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] arg, MethodProxy proxy) throws Throwable {
System.out.println("Before:" + method);
Object object = proxy.invokeSuper(obj, arg);
System.out.println("After:" + method);
return object;
}
}
//3、利用Enhancer类生成代理类;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new MyMethodInterceptor());
UserServiceImpl userService = (UserServiceImpl)enhancer.create();
//4、userService.add()的执行结果:
Before: add
This is add service
After: add
总结
jdk和cglib动态代理实现的区别
1、jdk动态代理生成的代理类和委托类实现了相同的接口;
2、cglib动态代理中生成的字节码更加复杂,生成的代理类是委托类的子类,且不能处理被final关键字修饰的方法;
3、jdk采用反射机制调用委托类的方法,cglib采用类似索引的方式直接调用委托类方法;
参考资料
[1]java代理机制 http://www.cnblogs.com/machine/archive/2013/02/21/2921345.html#sec-5-1
[2]JDK中的proxy动态代理原理剖析 http://www.jianshu.com/p/e2497db97b50
[3]深入理解Java Proxy机制 http://blog.csdn.net/rokii/article/details/4046098 (十分透彻)
[4]java动态代理原理(Proxy,InvocationHandler),含$Proxy0源码 http://www.2cto.com/kf/201109/103285.html
[5]说说cglib动态代理 http://www.cnblogs.com/chinajava/p/5880887.html
[6]动态生成Java字节码之java字节码框架ASM的学习http://blog.csdn.net/zhushuai1221/article/details/52169218