设计模式(九)代理模式

487 阅读11分钟

设计模式之禅在讲解代理模式时用了一个游戏代练的比喻。这个比喻非常的有代表性,对于理解代理模式很有帮助。它大致的思想是:大家都有过玩游戏的经历,也知道游戏代练。那么事实上游戏的代练在帮我的游戏账号打怪升级的时候,和代理模式里面的代理类做的事情不正是一样的事情吗?

代理模式的定义如下:Provide a surrogate or placeholder for another object to control acess to it.(为其他对象提供一种代理以控制对这个对象的访问)。其通用类图如下:

  • Subject是一个抽象类也可以是一个接口,是一个最普通的业务类型定义无特殊要求

  • RealSubject是Subject的子类或实现类,它才是被代理角色。是业务逻辑的具体执行者。

  • Proxy叫做委托类,代理类。它负责对真实对象的应用。它在真实对象处理完毕前后做预处理和善后处理工作。

上面是一个静态代理的场景。代理一般实现的模式为JDK静态代理:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

代理模式的优点

职责清晰

真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。

高扩展性

具体主题角色是随时都会发生变化的,只要它实现了接口,甭管它如何变化,都逃不脱如来佛的手掌(接口),那我们的代理类完全就可以在不做任何修改的情况下使用。

智能化

这在我们以上讲解中还没有体现出来,不过在我们以下的动态代理章节中你就会看到代理的智能化,读者有兴趣也可以看看Struts是如何把表单元素映射到对象上的。

动态代理

动态代理是在实现阶段不用关心代理谁,而在运行阶段才指定代理那一个对象,相对的来说,自己写代理类的方式就是静态代理。

现在有一个非常流行的名称叫做:面向横切面编程,也就是AOP(Aspect Oriented Programming),其核心就是采用了动态代理机制。

Spring AOP实现动态代理采用了两种方式,即JDK动态代理和CGLIB动态代理。

JDK动态代理是如何实现的

核心:代理模式加反射。

JDK动态代理与静态代理有相同之处,都要创建代理类,代理类都要实现接口。但是不同之处在于:

JDK静态代理是通过直接编码创建的,而JDK动态代理是利用反射机制在运行时根据指定参数创建代理类的。即JDK动态代理是更加通用的的一种方式,因为我们需要被代理的类往往是不止一个的。

若我们要自己实现一个JDK代理的话,则可以按如下核心代码实现:

public class JDKProxy implements InvocationHandler {
 
    //产生代理对象,被代理的对象必须实现一个接口
    public Object newProxy(Object targetObject) {//将目标对象传入进行代理
        Object object = Proxy.newProxyInstance(
                targetObject.getClass().getClassLoader(),
                targetObject.getClass().getInterfaces(), 
                this);//返回代理对象
        return object;
    }
 
    //实现InvocationHandler的 invoke方法
    public Object invoke(Object proxy, Method method, Object[] args)//invoke方法
            throws Throwable {
        System.out.println("before");    //一般我们进行逻辑处理的函数比如这个地方是模拟检查权限
        
        Object  ret  = method.invoke(proxy, args);       //调用invoke方法,ret存储该方法的返回值,通过反射获取代理方法然后进行调用,而在cglib中则是直接调用的,因为继承的缘故
 
        System.out.println("after"); //后置增强
        
        return ret;
    }
}

第一个方法根据被代理类生成代理类,第二个方法实现增强逻辑,第二个方法调用被代理方法是通过反射(method.invoke)实现的。

先来看第一个方法newProxy

这个方法的目标是根据被代理对象去创建一个代理类。关键方法为

 public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)

此方法一共三个参数:

  • ClassLoader 对象

  • 一组interface接口

  • 一个InvocationHandler对象

该方法关键实现如下:

 @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);
 
        final Class<?>[] intfs = interfaces.clone();
        
        ...
 
        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);
 
        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
 
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
             ...
        }
    }

  • getProxyClass0方法的两个参数分别是classloader以及被代理类的接口,这个方法会生成一个Class对象出来。即代理类。

  • 然后使用反射获得构造器: final Constructor<?> cons = cl.getConstructor(constructorParams);

  • 返回实例:return cons.newInstance(new Object[]{h});

生成的代理类:

  • 会去实现代理的接口,由于java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类,所以JDK的动态代理不支持对实现类的代理,只支持接口的代理。

  • 提供了一个使用InvocationHandler作为参数的构造方法 生成静态代码块来初始化接口中方法的Method对象,以及Object类的equals、hashCode、toString方法。

  • 重写了Object类的equals、hashCode、toString,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法。

  • 代理类实现代理接口的目标方法中,只是简单的调用了InvocationHandler的invoke方法,我们可以在invoke方法中进行一些特殊操作,甚至不调用实现的方法,直接返回。

经过我们的反编译,该类大致如下:

import com.dr.designPattern.proxy.dynamicProxy.UserManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
 
public final class $Proxy extends Proxy implements UserManager {
 
   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m4;
   private static Method m0;
 
 
   public $Proxy(InvocationHandler var1) throws  {
      super(var1);
   }
 
   public final boolean equals(Object var1) throws  {
      try {
         return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
      } catch (RuntimeException | Error var3) {
         throw var3;
      } catch (Throwable var4) {
         throw new UndeclaredThrowableException(var4);
      }
   }
 
   public final void addUser(String var1, String var2) throws  {
      try {
         super.h.invoke(this, m3, new Object[]{var1, var2});
      } catch (RuntimeException | Error var4) {
         throw var4;
      } catch (Throwable var5) {
         throw new UndeclaredThrowableException(var5);
      }
   }
 
   public final String toString() throws  {
      try {
         return (String)super.h.invoke(this, m2, (Object[])null);
      } catch (RuntimeException | Error var2) {
         throw var2;
      } catch (Throwable var3) {
         throw new UndeclaredThrowableException(var3);
      }
   }
 
   public final void delUser(String var1) throws  {
      try {
         super.h.invoke(this, m4, new Object[]{var1});
      } catch (RuntimeException | Error var3) {
         throw var3;
      } catch (Throwable var4) {
         throw new UndeclaredThrowableException(var4);
      }
   }
 
   public final int hashCode() throws  {
      try {
         return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
      } catch (RuntimeException | Error var2) {
         throw var2;
      } catch (Throwable var3) {
         throw new UndeclaredThrowableException(var3);
      }
   }
 
   static {
      try {
         m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
         m3 = Class.forName("com.dr.designPattern.proxy.dynamicProxy.UserManager").getMethod("addUser", new Class[]{Class.forName("java.lang.String"), Class.forName("java.lang.String")});
         m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
         m4 = Class.forName("com.dr.designPattern.proxy.dynamicProxy.UserManager").getMethod("delUser", new Class[]{Class.forName("java.lang.String")});
         m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      } catch (NoSuchMethodException var2) {
         throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
         throw new NoClassDefFoundError(var3.getMessage());
      }
   }
}

第二个方法是我们自己定义的JDKProxy类实现的InvocationHandler接口的invoke方法。可以看到上面生成的代理类就是通过调用invoke这个方法来进行目标方法的调用。

在动态代理中InvocationHandler是核心,每个代理实例都具有一个关联的调用处理程序(InvocationHandler)。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序(InvocationHandler)的 invoke 方法。所以对代理方法的调用都是通InvocationHadler的invoke来实现中,而invoke方法根据传入的代理对象,方法和参数来决定调用代理的哪个方法。invoke方法定义如下:

public Object invoke(Object proxy, Method method, Object[] args)

这个时候我们再回头看生成的代理类$Proxy,并以其中实现的方法addUser方法为例来梳理一遍调用流程:

首先代理类$Proxy继承了Proxy类并实现了被代理类UserManager

->$Proxy类实现了被代理类中的addUser和delUser方法。

->addUser方法的实现为:调用父类Proxy类的Invoke方法->因此,调用addUser方法就变成了调用被代理类的addUser方法->进而变成了调用Proxy类的invoke方法-> 而invoke方法是我们自己实现的->进而变成了调用有我们自己实现的逻辑(增强)的invoke方法

->invoke方法不仅要实现增强,还需调用被代理方法,因此这里传入了一个方法叫m3

->m3定义即为被代理方法addUser

->因此,在最终的invoke方法里面既能实现增强,又能调用被代理方法,并且,由于被代理方法是通过反射加载调用的,因此它可以是在运行时确定的,即动态的。

通过反编译出来的代理类,我们甚至可以直接new一个它的实例出来调用它的方法,如下:

$Proxy p = new $Proxy();
p.addUser("ray","test");

事实上这并没有什么神奇的地方,代理类和我们自己定义的普通的类并没有任何本质上的区别。

一些问题

问题1:为什么JDK动态代理只能代理实现了接口的类?

因为JDK动态代理生成的代理类需要去继承Proxy类,而java是单继承的,因此它只能去实现被代理类的接口(实现的接口)

问题2:为什么JDK动态代理生成的代理类要去继承Proxy类?

不仔细看的话,这里的代理类会给人一种继承Proxy类没有用的地方,很有迷惑性,我也是将生成的代理类代码亲自拿来试了一下才发现它的用处。注意,在代理类$Proxy中有多处方法调用,是通过使用了super关键字去拿父类中定义的InvocationHandler实例然后完成调用的。而Super关键字正是在调用父类Prioxy类的方法。

在代理类Proxy中调用父类的方法都是使用了InvocationHandler类的实例,在代理类Proxy中通过构造方法传入进来,而我们应该还记得,在Proxy的newProxyInstance方法中的第三个参数,正是InvocationHandler的一个实例。

因此这里调用方法时,自然就是使用这个实例去调用invoke方法,同时传入对应的方法参数。这下,所有的东西都串联起来了,问题三的答案事实上也已经有了。

问题3:为什么我们去调用代理类的目标方法,它会去调用invoke方法?

因为JDK生成真正的代理类中,是继承了Proxy类并实现了我们定义的被代理接口,而这个代理类在实现我们定义的接口方法时,是通过反射调用了InvocationHandlerImpl的invoke方法,然后在这个invoke方法当中,我们实现了增强的逻辑以及对被代理方法的真正调用。

问题4:JDK动态代理在哪些地方用到了反射?

经过上面的分析,至少有以下地方用到了反射:

    1. 在调用Proxy.newInstance方法时用反射获取接口传入方法
  • 2.在生成代理类$Proxy时,通过反射获取构造方法生成代理类

  • 3.在invoke方法当中调用被代理方法时,通过反射进行调用

因此,反射对于jdk动态代理的实现至关重要,而代理更多的是起到一个设计思想,代码架构上的作用。

CGLib动态代理是如何实现的

核心:代理模式加继承

具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。

方法的调用并不是通过反射来完成的,而是直接对方法进行调用,因为是继承,这一点与jdk动态代理是不一样的

另外JDK代理只能对接口进行代理,Cglib则是对实现类进行代理。重点代码如下:

public class CGLibProxy implements MethodInterceptor {
 
    //根据目标对象生成一个子类作为他的代理类
    public Object createProxyObject(Object obj) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());//设置父类为被代理类
        enhancer.setCallback(this);
        Object proxyObj = enhancer.create();
        return proxyObj;// 返回代理对象
    }
 
    public Object intercept(Object proxy, Method method, Object[] args,
            MethodProxy methodProxy) throws Throwable {
        
        System.out.println("before");
      
        Object obj = methodProxy.invoke(proxy, args);
        
        System.out.println("after");
        return obj;
    }
 
    
}

总的来说思想和JDK动态代理差别不大,都是根据被代理类生成一个代理类,MethodInterceptor类的角色和JDK动态代理中的InvocationHandler是一样的。但是Cglib的实现逻辑更为复杂。

但是在CGLib代理类中,因为是继承的缘故,因此会重写被代理类的方法,重写的逻辑就是调用我们这里实现的intercept方法,同时传入对应的参数。

第一个方法createProxyObject

这个方法会生成一个代理类,这个代理类当中重写了被代理类中的目标方法,同时,也提供了方法直接去调用被代理类的方法。

第二个方法intercept方法

在我们实现的intercept方法当中,当然也可以使用反射直接调用method方法,但是这里采取的实现是调用了MethodProxy类的方法去调用,这个方法没有用到反射机制。它的实现如下:

public Object invoke(Object obj, Object[] args) throws Throwable {
        try {
            this.init();
            MethodProxy.FastClassInfo fci = this.fastClassInfo;
            return fci.f1.invoke(fci.i1, obj, args);
        } catch (InvocationTargetException var4) {
            throw var4.getTargetException();
        } catch (IllegalArgumentException var5) {
            if (this.fastClassInfo.i1 < 0) {
                throw new IllegalArgumentException("Protected method: " + this.sig1);
            } else {
                throw var5;
            }
        }
    }

可以看到,是用了一个叫做FastClass的类来实现的。

FastClass实现机制简介

FastClass其实就是对Class对象进行特殊处理,提出下标概念index,通过索引保存方法的引用信息,将原先的反射调用,转化为方法的直接调用,从而体现所谓的fast,下面通过一个例子了解一下FastClass的实现机制。

1、定义原类


class Test {
    public void f(){
        System.out.println("f method");
    }
 
    public void g(){
        System.out.println("g method");
    }
}

2、定义Fast类

class FastTest {
    public int getIndex(String signature){
        switch(signature.hashCode()){
        case 3078479:
            return 1;
        case 3108270:
            return 2;
        }
        return -1;
    }
 
    public Object invoke(int index, Object o, Object[] ol){
        Test t = (Test) o;
        switch(index){
        case 1:
            t.f();
            return null;
        case 2:
            t.g();
            return null;
        }
        return null;
    }
}

在FastTest中有两个方法,getIndex中对Test类的每个方法根据hash建立索引,invoke根据指定的索引,直接调用目标方法,避免了反射调用。所以当调用methodProxy.invoke方法时,实际上是调用代理类的方法,代理类则是直接调用了被代理类的原方法(因为是继承的缘故,可以直接调用)。

在CGLibProxy类中重写的在intercept方法当中就可以进行逻辑增强,事实上,从技术上讲这里也可以通过反射调用被代理的原方法。

note:不论在哪,身在何职,身居何位,都不要忘记心中那一团似火不灭的璀璨和光芒。