动态代理模式技术最佳实践探讨

Scroll Down

1 概述

动态代理技术大家都很熟悉,我们日常使用的应用框架大部分都将动态代理技术作为核心技术实现,Proxy模式是一种能够有效的降低框架的侵入性的手段。
参考大部分框架主要的实现手段一般是由系统使用方定义使用接口,然后框架通过动态代理技术对接口进行实现。在此基础上可以在被代理实现类的核心处理方法中定义。

2 框架中代理模式使用

从几个框架使用动态代理技术来实现对应的场景来分析:
这里只是简单分析下各个框架对于动态代理技术的使用以及解决的问题。

2.1 mybatis

mybatis中使用动态代理技术来实现当用户只提供接口时,mybatis提供对相应接口的实现的能力。这里之所以提供该能力主要的目的是通过该框架来将整体的各种数据库操作规范化,而不是给用户暴露相关接口,做到在操作数据库流程方面也就是对于封装JDBC操作实现规范化,这也是mybatis设计的初衷。
mybatis中使用MapperProxy来实现接口的动态代理
该类的作用是实现对接口中的方法进行调用时的执行逻辑的封装。也就是实现了对非hashcode、equals等方法的调用逻辑。MapperProxy类实现了InvocationHandler接口,并对invoke方法进行了实现。
在MapperProxy中对于不同类型的方法进行了不同的处理,定义了MapperMethodInvoker接口用于实现对不同类型的方法执行的抽象。
这里定义了两个实现类分别是:PlainMethodInvoker和DefaultMethodInvoker。PlainMethodInvoker类用于描述对用户定义的方法进行调用操作。DefaultMethodInvoker用于描述对Lookup类型的方法的调用操作。(还没有研究这种调用方式)
以PlainMethodInvoker类为例,在MapperProxy的具体方法调用操作逻辑中,mybatis通过MapperMethod来对不同的方法类型进行不同的调用逻辑操作。
在PlainMethodInvoker的invoke方法中对调用MapperMethod的execute方法。具体实现代码如下:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

在这个拦截器中mybatis主要做了如下几件事。
入参转换,主要是将接口的入参转换成Statement设置值的时候的数据。
执行SQL。
返回值转换,主要是将执行SQL返回的值通过ResultMapping中定义的规则进行转换。
mybatis中对于入参和返回值转换及中间增加了各种缓存以及插件能力,做了很多复杂的逻辑这里不再展开,有兴趣的同学可以阅读mybatis的源码,这里我们主要关注动态代理的能力。
大家可以看到,mybatis代理拦截器中实现了增删改查的模板化方法。而mybatis要求用户只需要定义对应的接口,以及入参出参的转换规则,框架会通过动态代理技术完成自动执行一整套模板化SQL操作。

2.2 Spring

SqlSessionTemple中的使用如下:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取SqlSession
    SqlSession sqlSession = getSqlSession(
        SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType,
        SqlSessionTemplate.this.exceptionTranslator);
    try {
      // 执行SqlSession
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        // 如果不在事务中,则手动提交事务
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        // 关闭SqlSession
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

可以看到SqlSessionTemple主要做了以下几件事:
获取SqlSession
执行对应的SqlSession方法
返回结果,如果不存在事务则手动commit
关闭SqlSession
SqlSessionTemple也是一样,通过定义拦截器实现具体方法的一个模板化方法的调用。

2.3 其他使用场景

比如AOP的核心处理逻辑主要是通过AopProxy来实现,这里不做过多分析,大家可以参考源码,我画了个AOP的中各个组件的结构图供大家参考。
image
Dubbo中很多组件也都使用了动态代理技术这里就不展开谈论了,大家可以查看源码。但是核心的思想都是对模板化代码进行抽象。

3 生产系统中具体使用案例

rpc层远程调用使用
我们在平时的开发调用外部系统接口过程中发现rpc层在使用过程中存在模板化逻辑:
根据本系统参数组装远程rpc调用参数
调用远程rpc
将远程执行结果组装本系统的返回结果
存在方法可能需要UMP监控、日志、缓存情况等等
这里可以使用动态代理技术,将已上逻辑内嵌到拦截器中抽象出来,并且将组装参数的定制化逻辑开放出来由用户自定义。

这样做有如下好处:
不用在写过多的重复性代码,用户只需要定义接口即可,由动态代理来进行实现。
接入外部系统更加标准化,流程可控。
参数定制化逻辑开放,定制化。

4 反面案例

mybatis-plus,使得耦合性增加。
在使用mybatis-plus时,需要用户手动调用mybatis-plus内置方法或者继承一些内置的实现类,这些代码都会嵌入到应用项目中,单纯从耦合方面来讲对于项目的侵入是很严重的。

5 总结

本文说明了使用代理模式的一些场景特点及能够解决的问题,主要包括一下三点:
对于固定化代码,比如执行SQL,远程调用等,都可以通过拦截器实现样板式代码,减少编写重复代码。
降低框架对系统的侵入性。
使系统更加具备高内聚低耦合的特性。