代理模式--从诞生到动态代理的演化过程

以前看了很多博客文章,一段时间后,对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:静态代理

针对上面菜鸟方案的缺点,教科书方案的目标:

  1. 在不修改原有类情况下,给他们增加 “计时” 功能。
  2. 尽量不修改调用端的调用代码。
    下面看看静态代理实现方案类图,
    加入一个代理类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();//像调用真实类一样,调用代理类

好了,这就是静态代理的应用。 对原生类无任何入侵,调用端的修改也很小,新功能需求都在代理类中实现了。是不是很优雅很完美?
更进一步,考虑更多的需求

  1. 如果有新方法,例如 fetchData、storeData.... 需要计时功能,我们会修改代理类,在这些方法调用前后环绕上计时器。
    但是,这也是一种代码重复,能否把需要计时功能的函数、当做参数,传递给计时器代理类呢?(Method,反射)
  2. 如果有一个新类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();//调用业务函数

实现原理


使用Jdk动态代理方式的类图

本节主要参考 [深入理解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方法会做如下几件事:

  1. 根据传入的第二个参数interfaces动态生成一个类,实现interfaces中的接口,该例中即RealDataService接口IDataService. processData 方法。并且继承了Proxy类,重写了hashcode,toString,equals等三个方法。具体实现可参看 ProxyGenerator.generateProxyClass(...); 该例中生成了$Proxy0类
  2. 通过传入的第一个参数classloder将刚生成的类加载到jvm中。即将$Proxy0类load
  3. 利用第三个参数,调用$Proxy0的$Proxy0(InvocationHandler)构造函数 创建$Proxy0的对象,并且用interfaces参数遍历其所有接口的方法,并生成Method对象初始化对象的几个Method成员变量
  4. 将$Proxy0的实例返回给客户端。

现在好了,我们再看客户端怎么调就清楚了
1.客户端拿到的是$Proxy0的实例对象,由于$Proxy0继承了IDataService,因此转化为IDataService没任何问题。
IDataService proxy = (IDataService)Proxy.newProxyInstance(...);

  1. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容