MyBatis笔记 三 源码

Mapper代理的创建

MapperProxyFactory类用于创建Mapper的代理,只使用了Java提供的动态代理技术。

这个类中我们能发现,实际上Mapper接口的实际方法调用被创建出来MapperProxy接管。

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  // ...
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

下面是MapperProxy的代码

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
      // set local attributes
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 过滤Object方法
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      // 过滤接口默认方法
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 先通过缓存系统获取对应的MapperMethod,然后执行
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

}

MapperProxy也没有直接执行方法调用,而是又针对每个方法创建了一个MapperMethod实例并缓存起来,再调用这个mapperMethod,传入当前的sqlSession和参数。

下面是MapperMethod中的execute方法,其中用了命令模式,或者说是策略模式???根据不同的命令类型直接调用sqlSession中对应的增删改查方法。这是还在iBatis时留下的API,MyBatis不推荐我们直接使用这些API。

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);
                if (method.returnsOptional()
                    && (result == null || !method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
                }
            }
            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;
}

SELECT的情况比其他情况复杂,因为它需要返回多种类型的返回值。这里只看其中一个,executeForMany

不过也是根据不同情况直接调用sqlSession的方法,并且对结果做一些转换。

 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result); 
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

Mapper直接调用了SqlSession中的各种增删改查方法,我们的mapper文件如何就能够直接变成增删改查方法被调用呢?

SqlSession下的四大对象

  1. StatementHandler使用数据库对象的各种Statement进行操作,并且在四大对象中起承上启下的作用
  2. ParameterHandler对SQL中的参数进行处理
  3. ResultHandler对ResultSet进行封装,返回处理
  4. Executor调度前面三个处理器对象

Executor

MyBatis中有三种执行器

  1. SIMPLE,简易执行器(默认情况下的执行器)
  2. REUSE,一种针对prepareStatement的可重用执行器
  3. BATCH,批处理执行器,批量处理SQL语句

创建执行器时,会根据类型创建对应的执行器

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

这行语句好像是注册什么插件,目前还不太懂

executor = (Executor) interceptorChain.pluginAll(executor);

随便找到SimpleExecutor中的查询方法,其中就是基于配置来创建不同的StatementHandler,并且执行,再使用ResultHandler来转换结果。

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

现在我们的关注点就落在了其他Handler上

StatementHandler

先来看StatementHandler,这个newStatementHandler是如何工作的。

它的代码很简单,首先就是创建了一个RoutingStatementHandler,它是干啥的一会再说,然后就是向StatementHandler中采用同样的方法注入插件。

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler的设计思想很巧妙,因为StatementHandler处理SQL语句,如果语句中含有占位符,那么就需要使用JDBC的PreparedStatement进行参数化,如果含有存储过程,那么需要使用另外的东西。

RoutingStatementHandler提供了一个统一的接口,在其内部会自动根据语句的类型选择该使用哪种行为。我们看下它的构造器。

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

根据语句类型的不同,它会选择三种StatementHandler的实现类来处理这些不同的行为。StatementHandler中有两个重要的方法——prepareparameterize

Executor会在创建StatementHandler后自动调用prepareparameterize,对应代码如下:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

prepare方法的目的就在于创建对应的JDBCStatement对象,如果你仔细观察prepare方法的行为,它其实是提供了一个BaseStatementHandler作为抽象模板父类,前面三种StatementHandler都继承自这个父类,父类再将创建JDBCStatement的工作留给子类,并做一些通用的繁杂的工作。

MyBatis笔记 三 源码

parameterize的目的在于对语句进行参数化。诶?之前不是说Executor中的四大对象中有一个ParameterHandler用于处理参数吗?

是,BaseStatementHandler在构造时创建了ParameterHandler,并通过StatementHandler的接口方法getParameterHandler让子类获取

MyBatis笔记 三 源码

MyBatis笔记 三 源码

而不同的子类在parameterize方法中又有不同的行为,对于SimpleStatementHandler,它根本没有参数可处理,所以它无需处理。

MyBatis笔记 三 源码

对于PreparedStatementHandler,它则是托管给了ParameterHandler进行了参数处理

MyBatis笔记 三 源码

而对于存储过程相关的CallableStatementHandler,它不仅处理了,还实现了输出参数相关的代码。

MyBatis笔记 三 源码

现在大概StatementHandler搞懂了,下面就是ParameterHandler。

ParameterHandler

ParameterHandler很简单,只有两个接口方法。

getParameterObject用于获取参数对象,这里提一嘴,不管你在Mapper中怎么设置参数,是通过一个对象也好,是通过基本类型也好,通过Map也好,通过@Param也好,最终,到了MyBatis中的参数都是一个对象。

对于基本类型,MyBatis会转换成包装类,对于@Param,会转换成Map

setParameters就是向语句中设置参数了。

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps)
      throws SQLException;
}

MyBatis提供了一个实现类,DefaultParameterHandler

@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

这里就是取出参数,然后通过对应的typeHandler来设置参数。

MyBatis中的所有参数设置都要经过typeHandler,包括基本类型,也会通过MyBatis默认提供并已经注册好的typeHandler

ResultHandler

ResultHandler接口如下,前两个用于处理结果集,后一个用于处理存储过程相关的输出参数

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

ResultHandler的实现类是DefaultResultHandler,其工作过程就是通过CGLIB或JAVASSIST做延迟加载,通过ObjectFactory和TypeHandl组装结果并返回。

然后这应该是一个SqlSession的运行图了

MyBatis笔记 三 源码

上一篇:mybatis-插件


下一篇:高龄白菜JAVA学习第七十一天(Mybatis(4)参数处理源码分析)