导读

本文通过MyBatis一个低版本的bug(3.4.5之前的版本)动手,剖析MyBatis的一次残缺的查问流程,从配置文件的解析到一个查问的残缺执行过程具体解读MyBatis的一次查问流程,通过本文能够具体理解MyBatis的一次查问过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),因为当初很多工程中的版本都是低于3.4.5的,因而在这里用一个简略的例子复现问题,并且从源码角度剖析MyBatis一次查问的流程,让大家理解MyBatis的查问原理。

1 问题景象

1.1 场景问题复现

如下图所示,在示例Mapper中,上面提供了一个办法queryStudents,从student表中查问出合乎查问条件的数据,入参能够为student\_name或者student\_name的汇合,示例中参数只传入的是studentName的List汇合

 List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames);
  <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">        select * from student        <where>            <if test="studentNames != null and studentNames.size > 0 ">                AND student_name IN                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">                    #{studentName, jdbcType=VARCHAR}                </foreach>            </if>            <if test="studentName != null and studentName != '' ">                AND student_name = #{studentName, jdbcType=VARCHAR}            </if>        </where>    </select>

冀望运行的后果是

select * from student WHERE student_name IN ( 'lct' , 'lct2' )

然而实际上运行的后果是

==\> Preparing: select * from student WHERE student\_name IN ( ? , ? ) AND student\_name = ?

==\> Parameters: lct(String), lct2(String), lct2(String)

<== Columns: id, student_name, age

<== Row: 2, lct2, 2

<== Total: 1

通过运行后果能够看到,没有给student\_name独自赋值,然而通过MyBatis解析当前,独自给student\_name赋值了一个值,能够推断出MyBatis在解析SQL并对变量赋值的时候是有问题的,初步猜想是foreach循环中的变量的值带到了foreach外边,导致SQL解析出现异常,上面通过源码进行剖析验证

2 MyBatis查问原理

2.1 MyBatis架构

2.1.1 架构图

先简略来看看MyBatis整体上的架构模型,从整体上看MyBatis次要分为四大模块:

接口层:次要作用就是和数据库打交道

数据处理层:数据处理层能够说是MyBatis的外围,它要实现两个性能:

  • 通过传入参数构建动静SQL语句;
  • SQL语句的执行以及封装查问后果集成List<E>

框架撑持层:次要有事务管理、连接池治理、缓存机制和SQL语句的配置形式

疏导层:疏导层是配置和启动MyBatis 配置信息的形式。MyBatis 提供两种形式来疏导MyBatis :基于XML配置文件的形式和基于Java API 的形式

2.1.2 MyBatis四大对象

贯通MyBatis整个框架的有四大外围对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯通了整个框架的执行过程,四大对象的次要作用为:

  • ParameterHandler:设置预编译参数
  • ResultSetHandler:解决SQL的返回后果集
  • StatementHandler:解决sql语句预编译,设置参数等相干工作
  • Executor:MyBatis的执行器,用于执行增删改查操作

2.2 从源码解读MyBatis的一次查问过程

首先给出复现问题的代码以及相应的筹备过程

2.2.1 数据筹备

CREATE TABLE `student`  (  `id` bigint(20) NOT NULL AUTO_INCREMENT,  `student_name` varchar(255) NULL DEFAULT NULL,  `age` int(11) NULL DEFAULT NULL,  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1;-- ------------------------------ Records of student-- ----------------------------INSERT INTO `student` VALUES (1, 'lct', 1);INSERT INTO `student` VALUES (2, 'lct2', 2);

2.2.2 代码筹备

1.mapper配置文件

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="mybatis.StudentDao">    <!-- 映射关系 -->    <resultMap id="resultMap" type="mybatis.Student">        <id column="id" property="id" jdbcType="BIGINT" />        <result column="student_name" property="studentName" jdbcType="VARCHAR" />        <result column="age" property="age" jdbcType="INTEGER" />    </resultMap>    <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">        select * from student        <where>            <if test="studentNames != null and studentNames.size > 0 ">                AND student_name IN                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">                    #{studentName, jdbcType=VARCHAR}                </foreach>            </if>            <if test="studentName != null and studentName != '' ">                AND student_name = #{studentName, jdbcType=VARCHAR}            </if>        </where>    </select></mapper>

2.示例代码

public static void main(String[] args) throws IOException {        String resource = "mybatis-config.xml";        InputStream inputStream = Resources.getResourceAsStream(resource);        //1.获取SqlSessionFactory对象        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);        //2.获取对象        SqlSession sqlSession = sqlSessionFactory.openSession();        //3.获取接口的代理类对象        StudentDao mapper = sqlSession.getMapper(StudentDao.class);        StudentCondition condition = new StudentCondition();        List<String> studentNames = new LinkedList<>();        studentNames.add("lct");        studentNames.add("lct2");        condition.setStudentNames(studentNames);        //执行办法        List<Student> students = mapper.queryStudents(condition);    }

2.2.3 查问过程剖析

1.SqlSessionFactory的构建

先看SqlSessionFactory的对象的创立过程

//1.获取SqlSessionFactory对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

代码中首先通过调用SqlSessionFactoryBuilder中的build办法来获取对象,进入build办法

 public SqlSessionFactory build(InputStream inputStream) {    return build(inputStream, null, null);  }

调用本身的build办法

图1 build办法本身调用调试图例

在这个办法里会创立一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,而后调用parse办法进行解析

图2 parse解析入参调试图例

在这个办法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行解说。这里能够看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>    <properties>        <property name="dialect" value="MYSQL" />  <!-- SQL方言 -->    </properties>

而后将解析好的xml文件传入parseConfiguration办法中,在这个办法中会获取在配置文件中的各个节点的配置

图3 解析配置调试图例

以获取mappers节点的配置来看具体的解析过程

 <mappers>        <mapper resource="mappers/StudentMapper.xml"/>    </mappers>

进入mapperElement办法

mapperElement(root.evalNode("mappers"));

图4 mapperElement办法调试图例

看到MyBatis还是通过创立一个XMLMapperBuilder对象来对mappers节点进行解析,在parse办法中

public void parse() {  if (!configuration.isResourceLoaded(resource)) {    configurationElement(parser.evalNode("/mapper"));    configuration.addLoadedResource(resource);    bindMapperForNamespace();  }  parsePendingResultMaps();  parsePendingCacheRefs();  parsePendingStatements();}

通过调用configurationElement办法来解析配置的每一个mapper文件

private void configurationElement(XNode context) {  try {    String namespace = context.getStringAttribute("namespace");    if (namespace == null || namespace.equals("")) {      throw new BuilderException("Mapper's namespace cannot be empty");    }    builderAssistant.setCurrentNamespace(namespace);    cacheRefElement(context.evalNode("cache-ref"));    cacheElement(context.evalNode("cache"));    parameterMapElement(context.evalNodes("/mapper/parameterMap"));    resultMapElements(context.evalNodes("/mapper/resultMap"));    sqlElement(context.evalNodes("/mapper/sql"));    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));  } catch (Exception e) {    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);  }}

以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的

进入buildStatementFromContext办法

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {  for (XNode context : list) {    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);    try {      statementParser.parseStatementNode();    } catch (IncompleteElementException e) {      configuration.addIncompleteStatement(statementParser);    }  }}

能够看到MyBatis还是通过创立一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode办法,在这个办法里会获取到配置在这个标签下的所有配置信息,而后进行设置

图5 parseStatementNode办法调试图例

解析实现当前,通过办法addMappedStatement将所有的配置都增加到一个MappedStatement中去,而后再将mappedstatement增加到configuration中去

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,    resultSetTypeEnum, flushCache, useCache, resultOrdered,     keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

能够看到一个mappedstatement中蕴含了一个增删改查标签的详细信息

图7 mappedstatement对象办法调试图例

而一个configuration就蕴含了所有的配置信息,其中mapperRegistertry和mappedStatements

图8 config对象办法调试图例

具体的流程

图9 SqlSessionFactory对象的构建过程 图9 SqlSessionFactory对象的构建过程

2.SqlSession的创立过程

SqlSessionFactory创立实现当前,接下来看看SqlSession的创立过程

SqlSession sqlSession = sqlSessionFactory.openSession();

首先会调用DefaultSqlSessionFactory的openSessionFromDataSource办法

@Overridepublic SqlSession openSession() {  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}

在这个办法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {  Transaction tx = null;  try {    final Environment environment = configuration.getEnvironment();    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);    final Executor executor = configuration.newExecutor(tx, execType);    return new DefaultSqlSession(configuration, executor, autoCommit);  } catch (Exception e) {    closeTransaction(tx); // may have fetched a connection so lets call close()    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}

事务创立实现当前开始创立Executor对象,Executor对象的创立是依据 executorType创立的,默认是SIMPLE类型的,没有配置的状况下创立了SimpleExecutor,如果开启二级缓存的话,则会创立CachingExecutor

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 = (Executor)
interceptorChain.pluginAll(executor)办法,这个办法对应的含意是应用每一个拦截器包装并返回executor,最初调用DefaultSqlSession办法创立SqlSession

图10 SqlSession对象的创立过程

3.Mapper的获取过程

有了SqlSessionFactory和SqlSession当前,就须要获取对应的Mapper,并执行mapper中的办法

StudentDao mapper = sqlSession.getMapper(StudentDao.class);

在第一步中晓得所有的mapper都放在MapperRegistry这个对象中,因而通过调用
org.apache.ibatis.binding.MapperRegistry#getMapper办法来获取对应的mapper

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);  if (mapperProxyFactory == null) {    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");  }  try {    return mapperProxyFactory.newInstance(sqlSession);  } catch (Exception e) {    throw new BindingException("Error getting mapper instance. Cause: " + e, e);  }}

在MyBatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类当前执行newInstance办法,获取到对应的实例,这样就能够通过这个实例进行办法的调用

public class MapperProxyFactory<T> {  private final Class<T> mapperInterface;  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();  public MapperProxyFactory(Class<T> mapperInterface) {    this.mapperInterface = mapperInterface;  }  public Class<T> getMapperInterface() {    return mapperInterface;  }  public Map<Method, MapperMethod> getMethodCache() {    return methodCache;  }  @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<T>(sqlSession, mapperInterface, methodCache);    return newInstance(mapperProxy);  }}

获取mapper的流程为

图11 Mapper的获取过程

4.查问过程

获取到mapper当前,就能够调用具体的办法

//执行办法List<Student> students = mapper.queryStudents(condition);

首先会调用
org.apache.ibatis.binding.MapperProxy#invoke的办法,在这个办法中,会调用org.apache.ibatis.binding.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;}

首先依据SQL的类型增删改查决定执行哪个办法,在此执行的是SELECT办法,在SELECT中依据办法的返回值类型决定执行哪个办法,能够看到在select中没有selectone独自办法,都是通过selectList办法,通过调用
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)办法来获取到数据

@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {  try {    MappedStatement ms = configuration.getMappedStatement(statement);    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  } catch (Exception e) {    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}

在selectList中,首先从configuration对象中获取MappedStatement,在statement中蕴含了Mapper的相干信息,而后调用
org.apache.ibatis.executor.CachingExecutor#query()办法

图12 query()办法调试图示

在这个办法中,首先对SQL进行解析依据入参和原始SQL,对SQL进行拼接

图13 SQL拼接过程代码图示

调用MapperedStatement里的getBoundSql最终解析进去的SQL为

图14 SQL拼接过程后果图示

接下来调用
org.apache.ibatis.parsing.GenericTokenParser#parse对解析进去的SQL进行解析

图15 SQL解析过程图示

最终解析的后果为

图16 SQL解析后果图示

最初会调用SimpleExecutor中的doQuery办法,在这个办法中,会获取StatementHandler,而后调用
org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个办法进行参数和SQL的解决,最初调用statement的execute办法获取到后果集,而后 利用resultHandler对结进行解决

图17 SQL处理结果图示

查问的次要流程为

图18 查问流程解决图示

5.查问流程总结

总结整个查问流程如下

图19 查问流程形象

2.3 场景问题起因及解决方案

2.3.1 集体排查

这个问bug呈现的中央在于绑定SQL参数的时候再源码中地位为

 @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {   BoundSql boundSql = ms.getBoundSql(parameter);   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}

因为所写的SQL是一个动静绑定参数的SQL,因而最终会走到
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个办法中去

public BoundSql getBoundSql(Object parameterObject) {  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  if (parameterMappings == null || parameterMappings.isEmpty()) {    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);  }  // check for nested result maps in parameter mappings (issue #30)  for (ParameterMapping pm : boundSql.getParameterMappings()) {    String rmId = pm.getResultMapId();    if (rmId != null) {      ResultMap rm = configuration.getResultMap(rmId);      if (rm != null) {        hasNestedResultMaps |= rm.hasNestedResultMaps();      }    }  }  return boundSql;}

在这个办法中,会调用 rootSqlNode.apply(context)办法,因为这个标签是一个foreach标签,因而这个apply办法会调用到
org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个办法中去

@Overridepublic boolean apply(DynamicContext context) {  Map<String, Object> bindings = context.getBindings();  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);  if (!iterable.iterator().hasNext()) {    return true;  }  boolean first = true;  applyOpen(context);  int i = 0;  for (Object o : iterable) {    DynamicContext oldContext = context;    if (first) {      context = new PrefixedContext(context, "");    } else if (separator != null) {      context = new PrefixedContext(context, separator);    } else {        context = new PrefixedContext(context, "");    }    int uniqueNumber = context.getUniqueNumber();    // Issue #709     if (o instanceof Map.Entry) {      @SuppressWarnings("unchecked")       Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;      applyIndex(context, mapEntry.getKey(), uniqueNumber);      applyItem(context, mapEntry.getValue(), uniqueNumber);    } else {      applyIndex(context, i, uniqueNumber);      applyItem(context, o, uniqueNumber);    }    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));    if (first) {      first = !((PrefixedContext) context).isPrefixApplied();    }    context = oldContext;    i++;  }  applyClose(context);  return true;}

当调用appItm办法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区

private void applyItem(DynamicContext context, Object o, int i) {  if (item != null) {    context.bind(item, o);    context.bind(itemizeItem(item, i), o);  }}

进行绑定参数的时候,绑定实现foreach的办法的时候,能够看到bindings中不止绑定了foreach中的两个参数还额定有一个参数名字studentName->lct2,也就是说最初一个参数也是会呈现在bindings这个参数中的,

private void applyItem(DynamicContext context, Object o, int i) {  if (item != null) {    context.bind(item, o);    context.bind(itemizeItem(item, i), o);  }}

图20 参数绑定过程

最初断定

org.apache.ibatis.scripting.xmltags.IfSqlNode#apply

@Overridepublic boolean apply(DynamicContext context) {  if (evaluator.evaluateBoolean(test, context.getBindings())) {    contents.apply(context);    return true;  }  return false;}

能够看到在调用evaluateBoolean办法的时候会把context.getBindings()就是前边提到的bindings参数传入进去,因为当初这个参数中有一个studentName,因而在应用Ognl表达式的时候,断定为这个if标签是有值的因而将这个标签进行了解析

图21 单个参数绑定过程

最终绑定的后果为

图22 全副参数绑定过程

因而这个中央绑定参数的中央是有问题的,至此找出了问题的所在。

2.3.2 官网解释

翻阅MyBatis官网文档进行求证,发现在3.4.5版本发行中bug fixes中有这样一句

图23 此问题官网修复github记录 图23 此问题官网修复github记录

修复了foreach版本中对于全局变量context的批改的bug

issue地址为https://github.com/mybatis/my...

修复计划为https://github.com/mybatis/my...

能够看到官网给出的批改计划,从新定义了一个对象,别离存储全局变量和局部变量,这样就会解决foreach会扭转全局变量的问题。

图24 此问题官网修复代码示例

2.3.3 修复计划

  • 降级MyBatis版本至3.4.5以上
  • 如果放弃版本不变的话,在foreach中定义的变量名不要和内部的统一

3 源码浏览过程总结

MyBatis源代码的目录是比拟清晰的,基本上每个雷同性能的模块都在一起,然而如果间接去浏览源码的话,可能还是有肯定的难度,没法了解它的运行过程,本次通过一个简略的查问流程从头到尾跟下来,能够看到MyBatis的设计以及解决流程,例如其中用到的设计模式:

图25 MyBatis代码结构图

  • 组合模式:如ChooseSqlNode,IfSqlNode等
  • 模板办法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler
  • Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
  • 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory
  • 代理模式:MyBatis实现的外围,比方MapperProxy、ConnectionLogger

4 文档参考

https://mybatis.org/mybatis-3...

作者:李春廷