4

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

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

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

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

无代理

让我们先看一个小栗子:

public interface Humen{

  void eat(String food);
}

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

public class HumenImpl implements Humen{

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

拓展思考

在这里我们可以稍微做些扩展思考。如果未来,我们需要在这个 eat() 方法前后加上一些逻辑呢?比如说真实点的吃饭场景,第一步当然是要做饭,当我们吃完以后,则需要有人打扫。

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

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

  • 我不再多写个新方法,我在原来的方法中多传个标志位,在方法运行中通过if-else语句判断这个标志位,然后执行不同的逻辑

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

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

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

小结

聪明的客官肯定想到了,既然把它们写在一个方法中有这么多问题,那么我们把逻辑拆开,吃饭就是吃饭,做饭就是做饭,打扫就是打扫不就好了吗?事实确实是这样没错。只是原有的老代码人家就调用的是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


Sivan
525 声望90 粉丝

行业分两个赛道,一个是ToC,一个是 ToB。