原副标题:源标识符自学之MyBatis的下层查阅基本原理
编者按
责任编辑透过MyBatis两个旧版的bug(3.4.5以后的版)侧发力,预测MyBatis的一场完备的查阅业务流程,从命令行的导出到两个查阅的完备继续执行操作过程详尽阐释MyBatis的一场查阅业务流程,透过责任编辑能详尽介绍MyBatis的一场查阅操作过程。在平常的标识符撰写中,辨认出了MyBatis两个旧版的bug(3.4.5以后的版),虽然那时许多工程工程建设中的版都是高于3.4.5的,因而在这儿用两个单纯的范例Cadours难题,因而从源标识符视角预测MyBatis一场查阅的业务流程,让我们介绍MyBatis的查阅基本原理
01
难题现像
在去年的灵巧项目组工程建设中,我透过Suite开伞器同时实现了全屏智能化程序标识符。Juint除Suite开伞器除了什么样开伞器呢?继而我的Runnerexplore已经开始了!
1.1 情景难题Cadours
如下表所示图右图,在实例Mapper中,上面提供更多了两个方式queryStudents,从student表中查阅出合乎查阅前提的统计数据,入参能为student_name或是student_name的子集,实例中模块只传至的是studentName的List子集
List<String> studentNames =newLinkedList<>; studentNames.add(“lct”); studentNames.add(“lct2”); condition.setStudentNames(studentNames); <selectid=“queryStudents”parameterType=“mybatis.StudentCondition”resultMap=“resultMap”>select* fromstudent<where><iftest=“studentNames != null and studentNames.size > 0 “>AND student_name IN<foreachcollection=“studentNames”item=“studentName”open=“(“separator=“,”close=“)”>#{studentName, jdbcType=VARCHAR}</foreach></if>
<iftest=“studentName != null and studentName != “>AND student_name =#{studentName, jdbcType=VARCHAR}</if></where></select>
期望运行的结果是
select* fromstudent WHEREstudent_nameIN( 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导出出现异常,上面透过源标识符进行预测验证
02
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的一场查阅操作过程
首先给出Cadours难题的标识符以及相应的准备操作过程
2.2.1 统计数据准备
CREATETABLE`student`(`id`bigint(20) NOTNULLAUTO_INCREMENT,`student_name`varchar(255) NULLDEFAULTNULL,`age`int(11) NULLDEFAULTNULL,PRIMARY KEY(`id`) USINGBTREE) ENGINE= InnoDBAUTO_INCREMENT = 1;— —————————-— Records of student— —————————-INSERTINTO`student`VALUES(1, lct, 1);INSERTINTO`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” ><mappernamespace=“mybatis.StudentDao”><!– 映射关系 –><resultMapid=“resultMap”type=“mybatis.Student”><idcolumn=“id”property=“id”jdbcType=“BIGINT”/><resultcolumn=“student_name”property=“studentName”jdbcType=“VARCHAR”/><resultcolumn=“age”property=“age”jdbcType=“INTEGER”/>
</resultMap>
<selectid=“queryStudents”parameterType=“mybatis.StudentCondition”resultMap=“resultMap”>
select * from student<where><iftest=“studentNames != null and studentNames.size > 0 “>AND student_name IN<foreachcollection=“studentNames”item=“studentName”open=“(“separator=“,”close=“)”>#{studentName, jdbcType=VARCHAR}</foreach></if>
<iftest=“studentName != null and studentName != “>AND student_name = #{studentName, jdbcType=VARCHAR}</if></where></select>
</mapper>
2.实例标识符
publicstaticvoidmain(String[] args) throws IOException{String resource = “mybatis-config.xml”;InputStream inputStream = Resources.getResourceAsStream(resource);y对象SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder.build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession;StudentDao mapper = sqlSession.getMapper(StudentDao.class);StudentCondition condition = newStudentCondition;List<String> studentNames = newLinkedList<>;studentNames.add(“lct”);studentNames.add(“lct2”);condition.setStudentNames(studentNames);//继续执行方式List<Student> students = mapper.queryStudents(condition);}2.2.3 查阅操作过程预测
1.SqlSessionFactory的构建
先看SqlSessionFactory的对象的创建操作过程
SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder.build(inputStream); publicSqlSessionFactorybuild(InputStream inputStream){returnbuild(inputStream, null, null);}调用自身的build方式
图1 build方式自身调用调试图例
在这个方式里会创建两个XMLConfigBuilder的对象,用来导出传至的MyBatis的命令行,然后调用parse方式进行导出
图2 parse导出入参调试图例
在这个方式讲解。这儿能看到导出命令行是从configuration这个节点已经开始的,在MyBatis的命令行中这个节点也是根节点
<?xml version=”1.0″ encoding=”UTF-8″ ?><!DOCTYPE configurationPUBLIC “-//mybatis.org//DTD Config 3.0//EN”“http://mybatis.org/dtd/mybatis-3-config.dtd”><configuration><properties><propertyname=“dialect”value=“MYSQL”/><!– SQL方言 –></properties>
图3 导出配置调试图例
<mappers><mapperresource=“mappers/StudentMapper.xml”/></mappers>进入mapperElement方式
mapperElement(root.evalNode(“mappers”));图4 mapperElement方式调试图例
看到MyBatis还是透过创建两个XMLMapperBuilder对象来对mappers节点进行导出,在parse方式中
publicvoidparse(){if(!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode(“/mapper”));configuration.addLoadedResource(resource);bindMapperForNamespace;}parsePendingResultMaps;parsePendingCacheRefs;parsePendingStatements;}
透过调用configurationElement方式来导出配置的每两个mapper文件
privatevoidconfigurationElement(XNode context) {try{Stringnamespace= context.getStringAttribute(“namespace”);if(namespace== null|| namespace.equals(“”)) {thrownewBuilderException(“Mappers 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) {thrownewBuilderException(“Error parsing Mapper XML. Cause: “+ e, e);}}以导出mapper中的增删改查的标签来看看是如何导出两个mapper文件的
进入buildStatementFromContext方式
privatevoidbuildStatementFromContext(List<XNode> list, String requiredDatabaseId){for(XNode context : list) {final XMLStatementBuilder statementParser = newXMLStatementBuilder(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);图6 增加导出完成的mapper方式调试图例
能看到两个mappedstatement中包含了两个增删改查标签的详尽信息
图7 mappedstatement对象方式调试图例
而两个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements
图8 config对象方式调试图例
具体的业务流程
图9 SqlSessionFactory对象的构建操作过程
2.SqlSession的创建操作过程
SqlSessionFactory创建完成以后,接下来看看SqlSession的创建操作过程
SqlSessionsqlSession = sqlSessionFactory.openSession;首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方式
@OverridepublicSqlSession openSession{returnopenSessionFromDataSource(configuration.getDefaultExecutorType,null, false);}ctionFactory
privateSqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,booleanautoCommit){Transaction tx = null;try{finalEnvironment environment = configuration.getEnvironment;finalTransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource, level, autoCommit);finalExecutor executor = configuration.newExecutor(tx, execType);returnnewDefaultSqlSession(configuration, executor, autoCommit);} catch(Exception e) {closeTransaction(tx); // may have fetched a connection so lets call closethrowExceptionFactory.wrapException(“Error opening session. Cause: “+ e, e);} finally{ErrorContext.instance.reset;}}事务创建完成以后已经开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor
publicExecutor newExecutor(Transaction transaction, ExecutorType executorType){executorType = executorType == null? defaultExecutorType : executorType;executorType = executorType == null? ExecutorType.SIMPLE : executorType;Executor executor;if(ExecutorType.BATCH == executorType) {executor = newBatchExecutor(this, transaction);} elseif(ExecutorType.REUSE == executorType) {executor = newReuseExecutor(this, transaction);} else{executor = newSimpleExecutor(this, transaction);}if(cacheEnabled) {executor = newCachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);returnexecutor;}创建executor以后,会继续执行executor = (Executor) interceptorChain.pluginAll(executor)方式,这个方式对应的含义是使用每两个拦截器包装并返回executor,最后调用DefaultSqlSession方式创建SqlSession
图10 SqlSession对象的创建操作过程
StudentDaomapper = sqlSession.getMapper(StudentDao.class);在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因而透过调用
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);}}在MyB
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);}
}
4.查阅操作过程
调用具体的方式
//继续执行方式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.Obje
@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;}}ache.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方式
图17 SQL处理结果图示
查阅的主要业务流程为
图18 查阅业务流程处理图示
5.查阅业务流程总结
总结整个查阅业务流程如下表所示
图19 查阅业务流程抽象
2.3 情景难题原因及解决方案
2.3.1 个人排查
这个问bug出现的地方在于绑定SQL模块的时候再源标识符中位置为
@Overridepublic <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.ing.xmltags.DynamicSqlSource#getBoundSql这个方式中去
publicBoundSql getBoundSql(ObjectparameterObject) {BoundSql boundSql = sqlSource.getBoundSql(parameterObject);List<ParameterMapping> parameterMappings = boundSql.getParameterMappings;if(parameterMappings ==null|| parameterMappings.isEmpty) {boundSql = newBoundSql(configuration, boundSql.getSql, parameterMap.getParameterMappings, parameterObject);}// check for nested result maps in parameter mappings (issue #30)for(ParameterMapping pm : boundSql.getParameterMappings) {StringrmId = pm.getResultMapId;if(rmId != null) {ResultMap rm = configuration.getResultMap(rmId);if(rm != null) {hasNestedResultMaps |= rm.hasNestedResultMaps;}}}
returnboundSql;}
在这个方式中,会调用 rootSqlNode.apply(context)方式,虽然这个标签是两个foreach标签,因而这个apply方式会调用到org.apache.ibatis.ing.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.ing.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记录
修复了foreach版中对于全局变量context的修改的bug
issue地址为https://github.com/mybatis/mybatis-3/pull/966
修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a
能看到官方给出的修改方案,重新定义了两个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的难题。
图24 此难题官方修复标识符实例
2.3.3 修复方案
升级MyBatis版至3.4.5以上 如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致03
源标识符阅读操作过程总结
视图树的结构,转换完成后将透过表达式引擎导出表达式并取得正确的值,透过事件导出引擎导出用户自定义事件并完成事件的绑定,完成导出赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
MyBatis源标识符的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源标识符的话,可能还是有一定的难度,没法理解它的运行操作过程,本次透过两个单纯的查阅业务流程从头到尾跟下来,能看到MyBatis的设计以及处理业务流程,例如其中用到的设计模式:
图25 MyBatis标识符结构图
组合模式:如ChooseSqlNode,IfSqlNode等 模板方式模式:例如BaseExecutor和SimpleExecutor,除了BaseTypeHandler和所有的子类例如IntegerTypeHandler Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory 代理模式:MyBatis同时实现的核心,比如MapperProxy、ConnectionLogger04
文档参考
https://mybatis.org/mybatis-3/zh/index.html
END
从商业和开源中找到平衡
这儿有最新开源资讯、软件更新、技术干货等内容
点这儿 ↓↓↓ 记得 关注✔ 标星⭐ 哦~