转载

"秒懂"代理模式 -- java

分享人: 元哥

1、代理模式

  • 设计模式中的结构型设计模式

  • 从字面意思理解,代理即A替B完成某件事。例如媒婆帮靓仔找一个广州的女朋友,那么媒婆就是代理方,代理的事情就是帮忙介绍男女朋友;靓仔就是委托方,委托的事情就是找一个广州的女朋友。

1)、静态代理

  • 静态代理类图

    "秒懂"代理模式 -- java
  • Code实现

"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
  • 在这里我们会先声明一个Target接口,声明的方法就是findGirlFriend。
  • 然后声明一个类Handsome表示靓仔,实现Target接口的方法findGirlFriend
  • 接着声明一个类MiddleProxy表示媒婆,持有一个目标接口Target的对象,并且实现Target接口的方法。在这个方法中调用持有对象的方法的前后进行增强。增强的效果就是本来靓仔得自己一个人去做找女朋友这件事;但有了媒婆就先让她展示她的资源,然后靓仔找一个自己喜欢的广州的女朋友,最后媒婆给联系方式。

优点

  • 在调用的时候我们不用知道MiddleProxy是如何实现的,只需要知道被代理即可。对例子而言,靓仔不需要知道媒婆这个代理是怎么帮忙找女朋友的,反正有了媒婆就能找到女朋友。

缺点

  • 代理类和委托类实现了相同的接口,代理类和委托类实现了相同的方法。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

    • 举个例子。上述的需求是靓仔要找个广州的女朋友,那现在假如靓仔还有个特殊的需求,要找个广州的男朋友,委托类Handsome和代理类Proxy都需要实现findBoyFriend()方法。

      "秒懂"代理模式 -- java
  • 静态代理类只能为特定的接口服务。如想要为多个接口服务则需要建立很多个代理类。

    • 如上的代码是只为 HandsomeMan 类的访问提供了代理,但是如果还要为其他类如 PrettyGril 类提供代理的话,就需要我们再次添加代理 PrettyGril 的代理类。

      "秒懂"代理模式 -- java
    • 在举个开发中的例子:在调用具体实现类之前,需要打印日志等信息,这样我们只需要添加一个代理类,在代理类中添加打印日志的功能,然后调用实现类,这样就避免了修改具体实现类。满足我们所说的开闭原则。但是如果想让每个实现类都添加打印日志的功能的话,就需要添加多个代理类,以及代理类中各个方法都需要添加打印日志功能。

2)、动态代理

a)、jdk动态代理

  • 动态代理类图
"秒懂"代理模式 -- java
  • Code实现
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
  • 同样的使用前面的例子,需求是靓仔让媒婆帮忙找女朋友。因此Target接口、Handsome类不变。
  • 但在实现MiddleProxy媒婆这个类的时候,会去实现InvocationHandler这个类,实现的方法就是invoke方法和一个newProxyInstance方法。newProxyInstance是通过Java的反射类Proxy生成一个参数传进来对象的代理;invoke方法就是成员属性target中的所有成员方法进行增强。(可能你们会有很多疑问点,会在后面一一描述解决)

解决静态代理的问题

  • 当有新使用动态代理

    • 对比类图
    "秒懂"代理模式 -- java
    "秒懂"代理模式 -- java
    • 1)使用静态代理,当HandsomeMan类中有新的方法例如findBoyFriend()需要代理,相应findBoyFriend()方法需要在MiddleProxy类中进行实现;而使用动态代理,在MiddleProxy中不需要实现

    • 2)

      ![image-20191212212233267](/Users/cvter/Library/Application Support/typora-user-images/image-20191212212233267.png)

      "秒懂"代理模式 -- java
      • 使用静态代理。当有新的委托方时,例如靓女也需要找媒婆介绍男朋友,这时候有个PrettyGirl类,那么由于先前的Target接口类中声明的方法是findGrilFriend, 而我们需要的是findBoyFriend,Target接口不再适用,因此会导致MidlleProxy方法不再适用,需要新建新的目标接口和代理类来满足需求。
      • 使用动态代理,newProxyInstance无论传进来的对象是什么,持有的对象始终是Object,就只需要再写新的接口,不要需要再写新的代理类。

JDK动态代理是怎么实现的

"秒懂"代理模式 -- java
"秒懂"代理模式 -- java
  • 我们来对比静态代理和动态代理。在静态代理中target对象一个是MiddleProxy类的对象,而在动态代理中target对象是$Proxy0类的对象。

  • 我们知道静态代理的MiddleProxy类是怎么做的,因为是我们声明的;但$Proxy0类原先是不存在,但是他是在调用newProxyInstance方法后生成,而方法中实现的是通过反射类Proxy.newProxyInstance。

    public Object newProxyInstance(Object target) {
    	this.target = target;
    	return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    复制代码
  • 我们来通过特殊的方式,将$Proxy0类的字节码也就是class输出到文件,然后经过idea反编译得到以下

    public final class $Proxy0 extends Proxy implements Target {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
        public $Proxy0(InvocationHandler var1) throws  {...}
        public final boolean equals(Object var1) throws  {...}
        public final String toString() throws  {...}
        public final void findGrilFriend() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
        public final int hashCode() throws  {...}
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m3 = Class.forName("proxy.dynamic_proxy.Target").getMethod("findGrilFriend");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    复制代码
    • 代码有点儿多我们不要慌。首先我们看到$Proxy0类继承Proxy,并实现Target接口,那么它肯定实现了findGrilFriend方法。

    • 那么我们来大胆猜想,$Proxy0类是通过反射类Proxy.newProxyInstance()生成的,方法接收的参数就是Target接口对象,那么如果方法接收的参数是其他接口对象,例如BTarget接口对象,里面实现的方法findBoyFriend,那么$Proxy0类就会去实现BTarget接口的方法。因此可以认为,动态代理是静态代理的升级版,在程序运行过程中会动态根据传入的接口对象,动态生成指定接口对象的代理类$Proxy0的对象。

    • 接下来,我们继续看findGrilFriend方法,调用的是 super.h.invoke(this, m3, (Object[])null); super.h 是 Proxy类的成员属性(InvocationHandler),在Proxy.newProxyInstance()生成$Proxy0会传this对象。

      public $Proxy0(InvocationHandler var1) throws  {
      	super(var1);
      }
      复制代码
      public class MiddleProxy implements InvocationHandler {
        public Object newProxyInstance(Object target) {
          this.target = target;
          return Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                                        target.getClass().getInterfaces(), 
                                        this);
        }
        ....
      }
      复制代码
    • m3是声明的findGrilFriend()方法

      "秒懂"代理模式 -- java
    • 总结一下过程:

      • a)经过反射类Proxy.newProxyInstance()生成的了$Proxy0类和对象,并且$Proxy0对象持有MiddleProxy对象。
      • b)当调用findGirlFriend方法,调用的是$Proxy0类中的方法,super.h.invoke()
      • c)会再调用MiddleProxy类中的invoke方法,由于动态的传进来的HandsomeMan对象,其中会调用Object obj = method.invoke(target, args);

优缺点

  • 优点:解决了静态代理中冗余的代理实现类问题。
  • 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常

b)、cglib动态代理

  • 类图

    "秒懂"代理模式 -- java
  • 代理类

    "秒懂"代理模式 -- java
  • 动态生成具体的代理类code

public class HandsomeMan$$EnhancerByCGLIB$$596495c6 extends HandsomeMan implements Factory {
  	...省略很多代码
    final void CGLIB$findGrilFriend$0() {
        super.findGrilFriend();
    }

    public final void findGrilFriend() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$findGrilFriend$0$Method, CGLIB$emptyArgs, CGLIB$findGrilFriend$0$Proxy);
        } else {
            super.findGrilFriend();
        }
    }
}
复制代码
"秒懂"代理模式 -- java
  • 总结一下流程;

    • 1)首先我们通过newProxyInstance动态生成代理类HandsomeMan$$EnhancerByCGLIB$$和对象

      public class MiddleProxy implements MethodInterceptor {
        public Object newProxyInstance(Object target){
          //工具类
          Enhancer enhancer=new Enhancer();
          //设置被代理的对象,也可以理解为设置父类,因为动态代理类是继承了被动态代理类。
          enhancer.setSuperclass(target.getClass());
          //设置回调函数
          enhancer.setCallback(this);
          //创建子类的动态代理类对象
          return enhancer.create();
        }
      ....
      }
      复制代码
    复制代码
  • 2)在子类HandsomeMan$$EnhancerByCGLIB$$对象调用findGirlFriend方法时,会调用MiddleProxy类的intercept方法

    • 3)在MiddleProxy类的intercept方法中,会在调用父类方法前后进行增强

      public class MiddleProxy implements MethodInterceptor {
        ....
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
          System.out.println("这里有很多单身的人.深圳、广州、东莞你找一个喜欢的。");
          Object obj = methodProxy.invokeSuper(o, objects);
          System.out.println("给你喜欢的这个人的联系方式。");
          return obj;
        }
      }
      复制代码

区别

  • 由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK 方式解决不了;

  • CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。

  • 但是 CGLib 在创建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。 同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理。

优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。 缺点:技术实现相对难理解些。

注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如 hashCode()equals()toString() 等,但是 getClass()wait() 等方法不会,因为它是final方法,CGLIB无法代理。

原文  https://juejin.im/post/5df6e0f9518825123a5ab4b4
正文到此结束
Loading...