导读
本文通过mybatis一个低版本的bug(3.4.5之前的版本)入手,分析mybatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读mybatis的一次查询流程,通过本文可以详细了解mybatis的一次查询过程。在平时的代码编写中,发现了mybatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析mybatis一次查询的流程,让大家了解mybatis的查询原理
01 问题现象 在今年的敏捷团队建设中,我通过suite执行器实现了一键自动化单元测试。juint除了suite执行器还有哪些执行器呢?由此我的runner探索之旅开始了!
1.1 场景问题复现
如下图所示,在示例mapper中,下面提供了一个方法querystudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentname的list集合
list studentnames = new linkedlist(); studentnames.add(lct); studentnames.add(lct2); condition.setstudentnames(studentnames); select * from student 0 > and student_name in #{studentname, jdbctype=varchar} foreach> if> 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
select * from student 0 > and student_name in #{studentname, jdbctype=varchar} foreach> if> 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 studentnames = new linkedlist(); studentnames.add(lct); studentnames.add(lct2); condition.setstudentnames(studentnames); //执行方法 list 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的配置文件中这个节点也是根节点
public -//mybatis.org//dtd config 3.0//en http://mybatis.org/dtd/mybatis-3-config.dtd> properties>然后将解析好的xml文件传入parseconfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置
图3 解析配置调试图例
以获取mappers节点的配置来看具体的解析过程
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 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);
图6 增加解析完成的mapper方法调试图例
可以看到一个mappedstatement中包含了一个增删改查标签的详细信息
图7 mappedstatement对象方法调试图例
而一个configuration就包含了所有的配置信息,其中mapperregistertry和mappedstatements
图8 config对象方法调试图例
具体的流程
图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 getmapper(class type, sqlsession sqlsession) { final mapperproxyfactory mapperproxyfactory = (mapperproxyfactory) 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 { private final class mapperinterface; private final map methodcache = new concurrenthashmap(); public mapperproxyfactory(class mapperinterface) { this.mapperinterface = mapperinterface; } public class getmapperinterface() { return mapperinterface; } public map getmethodcache() { return methodcache; } @suppresswarnings(unchecked) protected t newinstance(mapperproxy mapperproxy) { return (t) proxy.newproxyinstance(mapperinterface.getclassloader(), new class[] { mapperinterface }, mapperproxy); } public t newinstance(sqlsession sqlsession) { final mapperproxy mapperproxy = new mapperproxy(sqlsession, mapperinterface, methodcache); return newinstance(mapperproxy); }}
获取mapper的流程为
图11 mapper的获取过程
4.查询过程
获取到mapper以后,就可以调用具体的方法
//执行方法list 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 list 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 list 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 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 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 mapentry = (map.entry) 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记录
修复了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 源码阅读过程总结 理解,首先 mcube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
mybatis源代码的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源码的话,可能还是有一定的难度,没法理解它的运行过程,本次通过一个简单的查询流程从头到尾跟下来,可以看到mybatis的设计以及处理流程,例如其中用到的设计模式:
图25 mybatis代码结构图
组合模式:如choosesqlnode,ifsqlnode等
模板方法模式:例如baseexecutor和simpleexecutor,还有basetypehandler和所有的子类例如integertypehandler
builder模式:例如 sqlsessionfactorybuilder、xmlconfigbuilder、xmlmapperbuilder、xmlstatementbuilder、cachebuilder
工厂模式:例如sqlsessionfactory、objectfactory、mapperproxyfactory
代理模式:mybatis实现的核心,比如mapperproxy、connectionlogger
04 文档参考
https://mybatis.org/mybatis-3/zh/index.html
Jabra推出全新 Elite 系列 华为联合中国移动发布白皮书
电容,电容的作用详细介绍
高性能矢量网络分析仪产品推荐
AR、DSP、360 这款车机“包圆”高频应用
iphone8什么时候上市?iphone8最新消息:苹果十周年力作iphone 8即将发布,为何面临重重挑战
源码学习之MyBatis的底层查询原理
传音控股入选“2023中国制造业500强”
小米AIoT布局利好,生态优势能保持住吗
【轴类在线修复】结晶机轴轴承位磨损在线修复
广信材料龙南基地加快投产进程
轨到轨放大器采用低噪声LT2的远端1677线地震检波器
爱立信反超华为,拿下第一宝座
智能巡检机器人在变电站的应用
8英寸晶圆代工产能供不应求,报价传出将提高1成
9月车市仍处寒冬 新能源市场高速增长
国产手机“芯”的未来之路在何方?
三星官方宣布GalaxyFold将于今年9月重新上架发售
软通动力天枢元宇宙研究院签约江宁 助力工业元宇宙加速发展
“捷豹最便宜的SUV”-捷豹E-PACE将在伦敦进行全球首发,预售价低于26万
美国警告牙买加,拒绝华为5G建设否则面临金融重击等