zoukankan      html  css  js  c++  java
  • 代理模式

    起源

    阅读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,即委托的含义。所以也呼应文章开头所说,代理的本质就是被代理对象将自己的职责委托给代理对象

    参考文献





    网络知识搬运/梳理小工
  • 相关阅读:
    数据库事务隔离级别+Spring 声明性事务隔离级别
    (面试题)如何查找Oracle数据库中的重复记录
    Spring提供的线程池支持--百度文库
    (面试题)输出下列程序结果(考察字符串与其他类型+连接)
    (面试)有两个木桶,一个3斤,一个5斤,水无限,要怎么样得到精确地4斤水
    (面试)涉及到继承和类加载
    (面试题)用折半查找法在一组整形数组中查找某个数据
    (面试)写出下面switch语句的输出结果
    HTML 鼠标坐标和元素坐标
    HTML5 元素属性介绍
  • 原文地址:https://www.cnblogs.com/aibilim/p/53856bcf66b773481aaf981ae364dfc9.html
Copyright © 2011-2022 走看看