代理模式

起源

阅读mybatis源码时需要到动态代理技术,所以查阅许多文章,深入学习代理技术。

本文的内容并非原创,摘录自网络各处。此文想表达的主要是对代理模式做一个自我总结。

代理分类

  1. 静态代理
  2. 动态代理
    1. jdk原生动态代理
    2. cglib动态代理

术语列表

  • 代理对象
  • 被代理对象

其实代理就是一种委托,被代理对象委托代理对象完成自己的职责。代理对象作为独立个体,可以在完成被代理对象职责的同时添加一些自己的职责。

静态代理

静态代理类(对象)持有被代理对象的引用,同时实现被代理对象实现的接口。
代码示例

// 接口
interface Hello{
    String sayHello(String str);
}
// 实现
class HelloImp implements Hello{
    @Override
    public String sayHello(String str) {
        return "HelloImp: " + str;
    }
}

下面对HelloImpl类的对象进行代理

// 静态代理方式
class StaticProxiedHello implements Hello{
    //持有被代理对象引用且实现接口
    private Hello hello = new HelloImp();
    @Override
    public String sayHello(String str) {
        logger.info("You said: " + str);
        //职责委托给被代理对象
        return hello.sayHello(str);
    }
}

动态代理

动态代理就是不手动写上面那个代理对象类,但是代理模式要完成的职能是不变的,即对被代理对象的方法进行增强。
由此可以想到,想要对被代理对象方法进行增强,必然要在调用这个方法的时候进行拦截。
方法拦截技术其实就是动态代理的核心。
对于方法拦截位置,学过AOP肯定都知道有好多拦截时机,比如方法调用前,方法调用后,方法调用前后等位置。
至于如何拦截方法调用,如果使用静态代理类,自然可以手动拦截。使用动态代理时,这些工具一般由外部代码来完成。

原生动态代理

上面知道,动态代理技术的核心是方法拦截。原生动态代理技术中,完成此职责就是InvocationHandler
当借助java.lang.reflect.Proxy#newProxyInstance方法生成代理对象时,必须传入一个InvocationHandler类型的对象。此后针对java.lang.reflect.Proxy#newProxyInstance方法生成的代理对象进行方法调用时,方法会被转发到java.lang.reflect.InvocationHandler#invoke中。
口说无凭,还是直接看代码最好:

class LogInvocationHandler implements InvocationHandler{
    //可以看到,此处还是得委托实际对象来完成职责。
    //代理对象只是额外添加一些职责
    private Hello hello;
    public LogInvocationHandler(Hello hello) {
        this.hello = hello;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("sayHello".equals(method.getName())) {
            logger.info("You said: " + Arrays.toString(args));
        }
        return method.invoke(hello, args);
    }
}
Hello proxyInstance = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, invocationHandler);
public final class $Proxy0 extends Proxy implements Hello
{
  ...
  public $Proxy0(InvocationHandler invocationhandler)
  {
    super(invocationhandler);
  }
  ...
  @Override
  public final String sayHello(String str){
    //方法调用被转发到invoke方法
    return super.h.invoke(this, m3, new Object[] {str});// 将方法调用转发给invocationhandler
  }
}

代理对象的所有接口方法调用都会转发到InvocationHandler.invoke()方法,在invoke()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;之后我们通过某种方式执行真正的方法体,示例中通过反射调用了Hello对象的相应方法,还可以通过RPC调用远程方法。

注意:对于从Object中继承的方法,JDK Proxy会把hashCode()equals()toString()这三个非接口方法转发给InvocationHandler,其余的Object方法则不会转发。详见JDK Proxy官方文档

CGLib动态代理

上面已经讲过,核心在于方法拦截。所以对于使用此这种代理模式,需要实现MethodInterceptor接口,用于方法拦截。

class MyMethodInterceptor implements MethodInterceptor{
  ...
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        logger.info("You said: " + Arrays.toString(args));
        //转发给被代理对象,即父类。
        return proxy.invokeSuper(obj, args);
    }
}
Enhancer enhancer = new Enhancer();
//此处仍需委托给被代理对象完成职责
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));

上述代码中,我们通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete的具体方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很类似,都是方法调用的中转站。

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

// CGLIB代理类具体实现
public class HelloConcrete$$EnhancerByCGLIB$$e3734e52
  extends HelloConcrete
  implements Factory
{
  ...
  private MethodInterceptor CGLIB$CALLBACK_0;

  public final String sayHello(String paramString)
  {
    ...
    MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;
    //如果拦截器不为空,就转发
    if (tmp17_14 != null) {
      // 将请求转发给MethodInterceptor.intercept()方法。
      return (String)tmp17_14.intercept(this, 
              CGLIB$sayHello$0$Method, 
              new Object[] { paramString }, 
              CGLIB$sayHello$0$Proxy);
    }
    //为空就不转发
    return super.sayHello(paramString);
  }
  ...
}

mybatis中的动态代理

其实在mybatis中没有被代理对象,稍微有点特殊。没有动态代理对象,这一点一直让我很疑惑,今晚写完文章后去翻了下源码,发现的确有点特殊:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

            if (this.isDefaultMethod(method)) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }

        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        //最终并没有委托给被代理对象。而是委托给mapperMethod对象。
        return mapperMethod.execute(this.sqlSession, args);
}

可以看到此处的逻辑是如果非数据库相关方法,则不去查询数据库。
其实按照正常的逻辑,应该是传入一个Mapper接口的实现类,然后委托给这个实现类来完成查询,可实际并非如此。

总结

对于普通的动态代理,把握住方法拦截、被代理对象即可。
对于mybatis,并没有被代理对象,而是委托给mapperMethod来完成职责,这个其实稍微有点不同,应该属于变种的代理模式,毕竟纯代理模式最终还是得依靠被代理对象来完成职责。

对于这种拓展的代理模式,实际职责完成时,可以委托给任意对象来完成。这样一来,也解决我了对mybatis中代理模式的困惑,毕竟我们只有Mapper接口类。
其实如果想深刻理解代理模式,比较好的方法就是实际反编译看下生成的代理类。
至于如何把生成的代理类保存到硬盘上,可见文章末尾。

我翻了字典,查阅了代理的含义,发现英文其实等同于delegation,即委托的含义。所以也呼应文章开头所说,代理的本质就是被代理对象将自己的职责委托给代理对象

参考文献





posted @ 2020-04-09 00:22  aibilim  阅读(154)  评论(0编辑  收藏  举报