代理技术简介

代理,或者称为 Proxy ,简单理解就是事情我不用去作,由其余人来替我完成。在黄勇《架构探险》一书中,我以为颇有意思的一句相关介绍是这么说的:java

赚钱方面,我就是我老婆的代理;带小孩方面,我老婆就是个人代理;家务事方面,没有代理。segmentfault

我是一个很喜欢偷懒的程序猿,一看代理的定义,哇塞,还有这么好的事情?竟然能够委托别人替我干活! 那么倒底是否是这样呢?别着急,仔细看看本文关于代理技术的介绍,最后我会专门回过头来解释这个问题的。架构

本文主要介绍了无代理、静态代理、JDK 动态代理、CGLib 动态代理的实现原理及其使用场景,及笔者对其使用逻辑的一点思考。限于本人的笔力和技术水平,不免有些说明不清楚的地方,权当抛砖引玉,还望海涵。框架

无代理

让咱们先看一个小栗子:ide

public interface Humen{

  void eat(String food);
}

上面是一个接口,下面是其实现类:测试

public class HumenImpl implements Humen{

  @Override
  public void eat(String food){
    System.out.println("eat " + food);
  }
}

拓展思考

在这里咱们能够稍微作些扩展思考。若是将来,咱们须要在这个 eat() 方法先后加上一些逻辑呢?好比说真实点的吃饭场景,第一步固然是要作饭,当咱们吃完之后,则须要有人打扫。this

固然,咱们能够把作饭和打扫的逻辑一并写在 eat() 方法内部,只是这样作,显然牺牲了不少的灵活性和拓展性。好比说,若是咱们今天决定不在家作饭了,咱们改去下馆子,那么这时候,显然,我须要改变以前的作饭逻辑为下馆子。常规的做法是怎么办呢?有两种:spa

  • 我再写个eat()方法,两个方法的名字/参数不一样,在调用的时候多作注意,调用不一样的方法/参数以实现执行不一样的逻辑.net

  • 我再也不多写个新方法,我在原来的方法中多传个标志位,在方法运行中经过if-else语句判断这个标志位,而后执行不一样的逻辑3d

这两种方法其实大同小异,本质上都是编译时就设定死了使用逻辑,一个须要在调用阶段多加判断,另外一个在方法内部多作判断。可是于业务场景拓展和代码复用的角度来看,均是问题多多。

  • 假设我将来不下馆子,也不本身作饭了,我蹭饭吃。这时候我就不须要作饭或者下订单了,那么按照上述处理思路,我至少要在全部调用的部分加个新标志位,在处理逻辑中多加一重判断,甚至或许多出了一个新方法。

  • 吃过饭须要进行打扫,我不当心弄洒了可乐也须要打扫,当我须要在别处调用打扫逻辑时,难以作到复用。

小结

聪明的客官确定想到了,既然把它们写在一个方法中有这么多问题,那么咱们把逻辑拆开,吃饭就是吃饭,作饭就是作饭,打扫就是打扫不就行了吗?事实确实是这样没错。只是原有的老代码人家就调用的是eat()方法,那咱们如何实现改动最少的代码又实现既作饭,又吃饭,而后还自带打扫的全方位一体化功能呢?

静态代理

下面咱们就用静态代理模式改造下以前的代码,看看是否是知足了咱们的需求。话很少说,上代码~

public class HumenProxy implements Humen{
  
  private Humen humen;
  
  public HumenProxy(){
    humen = new HumenImpl();
  }
  
  @Override
  public void eat(String food){
    before();
    humen.eat(food);
    after();
  }
  
  private void before(){
    System.out.println("cook");
  }

  private void after(){
    System.out.println("swap");
  }
}

main方法测试一下:

public static void main(String[] args){
  Humen humenProxy = new HumenProxy();
  humenProxy.eat("rice");
}

打印姐结果以下:

cook
eat rice
swap

能够看到,咱们使用 HumenProxy 实现了 Humen 接口(和 HumenImpl 实现相同接口),并在构造方法中 new 出一个 HumenImpl 类的实例。这样一来,咱们就能够在 HumenProxy eat() 方法里面去调用 HumenImpl 方法的 eat() 方法了。有意思的是,咱们在调用逻辑部分( main() 方法),依然持有的是 Humen 接口类型的引用,调用的也依然是 eat() 方法,只是实例化对象的过程改变了,结果来看,代理类却自动为咱们加上了 cook swap 等咱们须要的动做。

小结

小结一下,静态代理,为咱们带来了必定的灵活性,是咱们在不改变原来的被代理类的方法的状况下,经过在调用处替换被代理类的实例化语句为代理类的实例化语句的方式,实现了改动少许的代码(只改动了调用处的一行代码),就得到额外动做的功能。

拓展思考

优势

回看咱们在无代理方式实现中提出的两个问题:

  • 假设我将来不下馆子,也不本身作饭了,我蹭饭吃。这时候我就不须要作饭或者下订单了,那么按照上述处理思路,我至少要在全部调用的部分加个新标志位,在处理逻辑中多加一重判断,甚至或许多出了一个新方法。

  • 吃过饭须要进行打扫,我不当心弄洒了可乐也须要打扫,当我须要在别处调用打扫逻辑时,难以作到复用。

第一个问题,若是咱们须要改变吃饭先后的逻辑怎么办呢?如今不须要改变 HumenImpl eat() 方法了,咱们只须要在 HumenProxy eat() 方法中改变一下调用逻辑就行了。固然,若是须要同时保留原有的作饭和下订单的逻辑的话,依然须要在 HumenProxy 添加额外的判断逻辑或者直接写个新的代理类,在调用处(本例中为 main() 方法)修改实例化的过程。

第二个问题,在不一样的地方须要复用个人 cook() 或者 swap() 方法时,我可让个人 HumenProxy 再实现别的接口,而后和这里的 eat() 逻辑同样,让业务代码调用个人代理类便可。

缺点

其实这里的缺点就是上述优势的第二点,当我须要复用个人作饭逻辑时,个人代理老是须要实现一个新的接口,而后再写一个该接口的实现方法。但其实代理类的调用逻辑老是类似的,为了这么一个类似的实现效果,我却老是要写辣莫多包装代码,难道不会很累吗?

另外一方面,当咱们的接口改变的时候,无疑,被代理的类须要改变,同时咱们的额代理类也须要跟着改变,难道没有更好的办法了么?

做为一个爱偷懒的程序猿,固然会有相应的解决办法了~ 让咱们接下来看看JDK动态代理。

JDK 动态代理

依然是先看看代码:

public class DynamicProxy implements InvocationHandler{

  private Object target;

  public DynamicProxy(Object target){
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
    before();
    Object result = method.invoke(traget,args);
    after();
    return result;
  }
}

在上述代码中,咱们一方面将本来代理类中的代理对象的引用类型由具体类型改成 Object 基类型,另外一方面将方法的调用过程改成经过反射的方式,实现了不依赖于实现具体接口的具体方法,便成功代理被代理对象的方法的效果。
咱们来继续看看怎么调用:

public static void main(String[] args){
  Humen humen = new HumenImpl();

  DynamicProxy dynamicProxy = new  DynamicProxy(humen);
  
  Humen HumenProxy = (Humen) Proxy.newProInstance(
    humen.getClass().getClassLoader(),
    humen.getClass().getInterfaces(),
    dynamicProxy
  ); 

  humenProxy.eat("rice");
}

咱们能够看到,在调用过程当中,咱们使用了通用的 DynamicProxy 类包装了 HumenImpl 实例,而后调用了Jdk的代理工厂方法实例化了一个具体的代理类。最后调用代理的 eat() 方法。

咱们能够看到,这个调用虽然足够灵活,能够动态生成一个具体的代理类,而不用本身显示的建立一个实现具体接口的代理类,不过调用这个代理类的过程仍是有些略显复杂,与咱们减小包装代码的目标不符,因此能够考虑作些小重构来简化调用过程:

public class DynamicProxy implements InvocationHandler{
  ···
  @SuppressWarnings("unchecked")
  public <T> T getProxy(){
    return (T) Proxy.newProxyInstance(
      target.getClass().getClassLoader(),
      target.getClass().getInterfaces(),
      this
    );
  }
}

咱们继续看看如今的调用逻辑:

public static void main(String[] args){
  DynamicProxy dynamicProxy = new DynamicProxy(new HumenImpl);
  Humen HumenProxy = dynamicProxy.getProxy();

  humenProxy.eat("rice");
}

拓展思考

优势

相比以前的静态代理,咱们能够发现,如今的调用代码多了一行。不过相较这多出来的一行,更使人兴奋的时,咱们经过实用 jdk 为咱们提供的动态代理实现,达到了咱们的 cook() 或者 swap() 方法能够被任意的复用的效果(只要咱们在调用代码处使用这个通用代理类去包装任意想要须要包装的被代理类便可)。
当接口改变的时候,虽然被代理类须要改变,可是咱们的代理类却不用改变了。

缺点

咱们能够看到,不管是静态代理仍是动态代理,它都须要一个接口。那若是咱们想要包装的方法,它就没有实现接口怎么办呢?这个问题问的好,JDK为咱们提供的代理实现方案确实无法解决这个问题。。。
那么怎么办呢?别急,接下来就是咱们的终极大杀器,CGLib动态代理登场的时候了。

CGLib 动态代理

CGLib 是一个类库,它能够在运行期间动态的生成字节码,动态生成代理类。继续上代码:

public class CGLibProxy implements MethodInterceptor{
  public <T> T getProxy(Class<T> cls){
    return (T) Enhancer.create(cls,this);
  }

  public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) 
     throws Throwable{
    before();
    Object result = proxy.invokeSuper(obj,args);
    after();
    return result;
  }
}

调用时逻辑以下:

public static void main(String[] args){
  CGLibProxy cgLibProxy = new CGLibProxy();
  Humen humenProxy = cgLibProxy.getProxy(HumenImpl.class);
  humenProxy.eat("rice");
}

由于咱们的 CGLib 代理并不须要动态绑定接口信息(JDK默认代理须要用构造方法动态获取具体的接口信息)。

因此其实这里调用 CGLib 代理的过程还能够再进行简化,咱们只要将代理类定义为单例模式,便可使调用逻辑简化为两行操做:

public class CGLibproxy implements MethodInterceptor{
  private static CGLibProxy instance = new CGLibProxy();
  
  private CGLibProxy(){}

  public static CGLibProxy getInstance(){
   return instance;
  }
}

调用逻辑:

public static voidf main(String[] atgs){
  Humen humenProxy = CGLibProxy.getInstance().getProxy(HumenImpl.class);
  humenProxy.eat("rice");
}

拓展思考

优势

实用 CGLib 动态代理的优点很明显,有了它,咱们就能够为没有接口的类包装前置和后置方法了。从这点来讲,它比不管是 JDK 动态代理仍是静态代理都灵活的多。

缺点

既然它比 JDK 动态代理还要灵活,那么我为何还要在前面花那么多篇幅去介绍 JDK 动态代理呢?这就不得不提它的一个很大的缺点了。

咱们想一想,JDK 动态代理 和它在调用阶段有什么不一样?对,少了接口信息。那么JDK动态代理为何须要接口信息呢?就是由于要根据接口信息来拦截特定的方法,而CGLib动态代理并没接收接口信息,那么它又是如何拦截指定的方法呢?答案是没有作拦截。。。(各位读者能够本身试试)

总结

经过上述介绍咱们能够看到,代理是一种很是有意思的模式。本文具体介绍了三种代理实现方式,静态代理、JDK动态代理 以及 CGLib动态代理。

这三种代理方式各有优劣,它们的优势在于:

  • 咱们经过在原有的调用逻辑过程当中,再抽一个代理类的方式,使调用逻辑的变化尽量的封装再代理类的内部中,达到不去改动原有被代理类的方法的状况下,增长新的动做的效果。

  • 这就使得即使在将来的使用场景中有更多的拓展,改变也依然很难波及被代理类,咱们也就能够放心的对被代理类的特定方法进行复用了

从缺点来看:

  • 静态代理和JDK动态代理都须要被代理类的接口信息以肯定特定的方法进行拦截和包装。

  • CGLib动态代理虽然不须要接口信息,可是它拦截并包装被代理类的全部方法。

最后,咱们画一张思惟导图总结一下:

clipboard.png

代理技术在实际项目中有很是多的应用,好比Spring 的AOP技术。下篇博客中,我将会着重介绍代理技术在 Spring 的AOP技术中是如何使用的相关思考,敬请期待~

参考文档

  • 黄勇—《架构探险-从零开始写Java Web框架》4.1代理技术简介

联系做者

zhihu.com
segmentfault.com
oschina.net

相关文章
相关标签/搜索