乐趣区

关于sql:源码学习之MyBatis的底层查询原理

导读

本文通过 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 办法

@Override
public 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) 办法来获取到数据

@Override
public <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 这个办法中去

@Override
public 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

@Override
public 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…

作者:李春廷

退出移动版