導(dǎo)讀
本文通過(guò)MyBatis一個(gè)低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查詢(xún)流程,從配置文件的解析到一個(gè)查詢(xún)的完整執(zhí)行過(guò)程詳細(xì)解讀MyBatis的一次查詢(xún)流程,通過(guò)本文可以詳細(xì)了解MyBatis的一次查詢(xún)過(guò)程。在平時(shí)的代碼編寫(xiě)中,發(fā)現(xiàn)了MyBatis一個(gè)低版本的bug(3.4.5之前的版本),由于現(xiàn)在很多工程中的版本都是低于3.4.5的,因此在這里用一個(gè)簡(jiǎn)單的例子復(fù)現(xiàn)問(wèn)題,并且從源碼角度分析MyBatis一次查詢(xún)的流程,讓大家了解MyBatis的查詢(xún)?cè)怼?/p>
01 問(wèn)題現(xiàn)象
在今年的敏捷團(tuán)隊(duì)建設(shè)中,我通過(guò)Suite執(zhí)行器實(shí)現(xiàn)了一鍵自動(dòng)化單元測(cè)試。Juint除了Suite執(zhí)行器還有哪些執(zhí)行器呢?由此我的Runner探索之旅開(kāi)始了!
1.1 場(chǎng)景問(wèn)題復(fù)現(xiàn)
如下圖所示,在示例Mapper中,下面提供了一個(gè)方法queryStudents,從student表中查詢(xún)出符合查詢(xún)條件的數(shù)據(jù),入?yún)⒖梢詾閟tudent_name或者student_name的集合,示例中參數(shù)只傳入的是studentName的List集合
List studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames);
期望運(yùn)行的結(jié)果是
select * from student WHERE student_name IN ( 'lct' , 'lct2' )但是實(shí)際上運(yùn)行的結(jié)果是
==> 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
通過(guò)運(yùn)行結(jié)果可以看到,沒(méi)有給student_name單獨(dú)賦值,但是經(jīng)過(guò)MyBatis解析以后,單獨(dú)給student_name賦值了一個(gè)值,可以推斷出MyBatis在解析SQL并對(duì)變量賦值的時(shí)候是有問(wèn)題的,初步猜測(cè)是foreach循環(huán)中的變量的值帶到了foreach外邊,導(dǎo)致SQL解析出現(xiàn)異常,下面通過(guò)源碼進(jìn)行分析驗(yàn)證
02 MyBatis查詢(xún)?cè)?/strong>
理解,首先 MCube 會(huì)依據(jù)模板緩存狀態(tài)判斷是否需要網(wǎng)絡(luò)獲取最新模板,當(dāng)獲取到模板后進(jìn)行模板加載,加載階段會(huì)將產(chǎn)物轉(zhuǎn)換為視圖樹(shù)的結(jié)構(gòu),轉(zhuǎn)換完成后將通過(guò)表達(dá)式引擎解析表達(dá)式并取得正確的值,通過(guò)事件解析引擎解析用戶(hù)自定義事件并完成事件的綁定,完成解析賦值以及事件綁定后進(jìn)行視圖的渲染,最終將目標(biāo)頁(yè)面展示到屏幕。
2.1 MyBatis架構(gòu)
2.1.1 架構(gòu)圖
先簡(jiǎn)單來(lái)看看MyBatis整體上的架構(gòu)模型,從整體上看MyBatis主要分為四大模塊:
接口層:主要作用就是和數(shù)據(jù)庫(kù)打交道
數(shù)據(jù)處理層:數(shù)據(jù)處理層可以說(shuō)是MyBatis的核心,它要完成兩個(gè)功能:
通過(guò)傳入?yún)?shù)構(gòu)建動(dòng)態(tài)SQL語(yǔ)句;
SQL語(yǔ)句的執(zhí)行以及封裝查詢(xún)結(jié)果集成List
框架支撐層:主要有事務(wù)管理、連接池管理、緩存機(jī)制和SQL語(yǔ)句的配置方式
引導(dǎo)層:引導(dǎo)層是配置和啟動(dòng)MyBatis 配置信息的方式。MyBatis 提供兩種方式來(lái)引導(dǎo)MyBatis :基于XML配置文件的方式和基于Java API 的方式
2.1.2 MyBatis四大對(duì)象
貫穿MyBatis整個(gè)框架的有四大核心對(duì)象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大對(duì)象貫穿了整個(gè)框架的執(zhí)行過(guò)程,四大對(duì)象的主要作用為:
ParameterHandler:設(shè)置預(yù)編譯參數(shù)
ResultSetHandler:處理SQL的返回結(jié)果集
StatementHandler:處理sql語(yǔ)句預(yù)編譯,設(shè)置參數(shù)等相關(guān)工作
Executor:MyBatis的執(zhí)行器,用于執(zhí)行增刪改查操作
2.2從源碼解讀MyBatis的一次查詢(xún)過(guò)程
首先給出復(fù)現(xiàn)問(wèn)題的代碼以及相應(yīng)的準(zhǔn)備過(guò)程
2.2.1 數(shù)據(jù)準(zhǔn)備
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 代碼準(zhǔn)備
1.mapper配置文件
2.示例代碼
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//1.獲取SqlSessionFactory對(duì)象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.獲取對(duì)象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.獲取接口的代理類(lèi)對(duì)象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
StudentCondition condition = new StudentCondition();
List studentNames = new LinkedList<>();
studentNames.add("lct");
studentNames.add("lct2");
condition.setStudentNames(studentNames);
//執(zhí)行方法
List students = mapper.queryStudents(condition);
}
2.2.3 查詢(xún)過(guò)程分析
1.SqlSessionFactory的構(gòu)建
先看SqlSessionFactory的對(duì)象的創(chuàng)建過(guò)程
//1.獲取SqlSessionFactory對(duì)象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
代碼中首先通過(guò)調(diào)用SqlSessionFactoryBuilder中的build方法來(lái)獲取對(duì)象,進(jìn)入build方法
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
調(diào)用自身的build方法

圖1 build方法自身調(diào)用調(diào)試圖例
在這個(gè)方法里會(huì)創(chuàng)建一個(gè)XMLConfigBuilder的對(duì)象,用來(lái)解析傳入的MyBatis的配置文件,然后調(diào)用parse方法進(jìn)行解析

圖2 parse解析入?yún)⒄{(diào)試圖例
在這個(gè)方法中,會(huì)從MyBatis的配置文件的根目錄中獲取xml的內(nèi)容,其中parser這個(gè)對(duì)象是一個(gè)XPathParser的對(duì)象,這個(gè)是專(zhuān)門(mén)用來(lái)解析xml文件的,具體怎么從xml文件中獲取到各個(gè)節(jié)點(diǎn)這里不再進(jìn)行講解。這里可以看到解析配置文件是從configuration這個(gè)節(jié)點(diǎn)開(kāi)始的,在MyBatis的配置文件中這個(gè)節(jié)點(diǎn)也是根節(jié)點(diǎn)
然后將解析好的xml文件傳入parseConfiguration方法中,在這個(gè)方法中會(huì)獲取在配置文件中的各個(gè)節(jié)點(diǎn)的配置

圖3 解析配置調(diào)試圖例
以獲取mappers節(jié)點(diǎn)的配置來(lái)看具體的解析過(guò)程
進(jìn)入mapperElement方法
mapperElement(root.evalNode("mappers"));

圖4 mapperElement方法調(diào)試圖例
看到MyBatis還是通過(guò)創(chuàng)建一個(gè)XMLMapperBuilder對(duì)象來(lái)對(duì)mappers節(jié)點(diǎn)進(jìn)行解析,在parse方法中
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
通過(guò)調(diào)用configurationElement方法來(lái)解析配置的每一個(gè)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中的增刪改查的標(biāo)簽來(lái)看看是如何解析一個(gè)mapper文件的
進(jìn)入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還是通過(guò)創(chuàng)建一個(gè)XMLStatementBuilder對(duì)象來(lái)對(duì)增刪改查節(jié)點(diǎn)進(jìn)行解析,通過(guò)調(diào)用這個(gè)對(duì)象的parseStatementNode方法,在這個(gè)方法里會(huì)獲取到配置在這個(gè)標(biāo)簽下的所有配置信息,然后進(jìn)行設(shè)置

圖5 parseStatementNode方法調(diào)試圖例
解析完成以后,通過(guò)方法addMappedStatement將所有的配置都添加到一個(gè)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方法調(diào)試圖例
可以看到一個(gè)mappedstatement中包含了一個(gè)增刪改查標(biāo)簽的詳細(xì)信息

圖7 mappedstatement對(duì)象方法調(diào)試圖例
而一個(gè)configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements

圖8 config對(duì)象方法調(diào)試圖例
具體的流程

圖9 SqlSessionFactory對(duì)象的構(gòu)建過(guò)程
2.SqlSession的創(chuàng)建過(guò)程
SqlSessionFactory創(chuàng)建完成以后,接下來(lái)看看SqlSession的創(chuàng)建過(guò)程
SqlSession sqlSession = sqlSessionFactory.openSession();
首先會(huì)調(diào)用DefaultSqlSessionFactory的openSessionFromDataSource方法
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
在這個(gè)方法中,首先會(huì)從configuration中獲取DataSource等屬性組成對(duì)象Environment,利用Environment內(nèi)的屬性構(gòu)建一個(gè)事務(wù)對(duì)象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();
}
}
事務(wù)創(chuàng)建完成以后開(kāi)始創(chuàng)建Executor對(duì)象,Executor對(duì)象的創(chuàng)建是根據(jù) executorType創(chuàng)建的,默認(rèn)是SIMPLE類(lèi)型的,沒(méi)有配置的情況下創(chuàng)建了SimpleExecutor,如果開(kāi)啟二級(jí)緩存的話(huà),則會(huì)創(chuàng)建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;
}
創(chuàng)建executor以后,會(huì)執(zhí)行executor = (Executor) interceptorChain.pluginAll(executor)方法,這個(gè)方法對(duì)應(yīng)的含義是使用每一個(gè)攔截器包裝并返回executor,最后調(diào)用DefaultSqlSession方法創(chuàng)建SqlSession

圖10 SqlSession對(duì)象的創(chuàng)建過(guò)程
3.Mapper的獲取過(guò)程
有了SqlSessionFactory和SqlSession以后,就需要獲取對(duì)應(yīng)的Mapper,并執(zhí)行mapper中的方法
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
在第一步中知道所有的mapper都放在MapperRegistry這個(gè)對(duì)象中,因此通過(guò)調(diào)用org.apache.ibatis.binding.MapperRegistry#getMapper方法來(lái)獲取對(duì)應(yīng)的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對(duì)應(yīng)的都是一個(gè)代理類(lèi),獲取到mapper對(duì)應(yīng)的代理類(lèi)以后執(zhí)行newInstance方法,獲取到對(duì)應(yīng)的實(shí)例,這樣就可以通過(guò)這個(gè)實(shí)例進(jìn)行方法的調(diào)用
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的獲取過(guò)程
4.查詢(xún)過(guò)程
獲取到mapper以后,就可以調(diào)用具體的方法
//執(zhí)行方法 List students = mapper.queryStudents(condition);
首先會(huì)調(diào)用org.apache.ibatis.binding.MapperProxy#invoke的方法,在這個(gè)方法中,會(huì)調(diào)用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;
}
首先根據(jù)SQL的類(lèi)型增刪改查決定執(zhí)行哪個(gè)方法,在此執(zhí)行的是SELECT方法,在SELECT中根據(jù)方法的返回值類(lèi)型決定執(zhí)行哪個(gè)方法,可以看到在select中沒(méi)有selectone單獨(dú)方法,都是通過(guò)selectList方法,通過(guò)調(diào)用org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法來(lái)獲取到數(shù)據(jù)
@Override
public 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對(duì)象中獲取MappedStatement,在statement中包含了Mapper的相關(guān)信息,然后調(diào)用org.apache.ibatis.executor.CachingExecutor#query()方法

圖12 query()方法調(diào)試圖示
在這個(gè)方法中,首先對(duì)SQL進(jìn)行解析根據(jù)入?yún)⒑驮糞QL,對(duì)SQL進(jìn)行拼接

圖13 SQL拼接過(guò)程代碼圖示
調(diào)用MapperedStatement里的getBoundSql最終解析出來(lái)的SQL為

圖14 SQL拼接過(guò)程結(jié)果圖示
接下來(lái)調(diào)用org.apache.ibatis.parsing.GenericTokenParser#parse對(duì)解析出來(lái)的SQL進(jìn)行解析

圖15 SQL解析過(guò)程圖示
最終解析的結(jié)果為

圖16 SQL解析結(jié)果圖示
最后會(huì)調(diào)用SimpleExecutor中的doQuery方法,在這個(gè)方法中,會(huì)獲取StatementHandler,然后調(diào)用org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize這個(gè)方法進(jìn)行參數(shù)和SQL的處理,最后調(diào)用statement的execute方法獲取到結(jié)果集,然后 利用resultHandler對(duì)結(jié)進(jìn)行處理

圖17 SQL處理結(jié)果圖示
查詢(xún)的主要流程為


圖18 查詢(xún)流程處理圖示
5.查詢(xún)流程總結(jié)
總結(jié)整個(gè)查詢(xún)流程如下

圖19 查詢(xún)流程抽象
2.3場(chǎng)景問(wèn)題原因及解決方案
2.3.1 個(gè)人排查
這個(gè)問(wèn)bug出現(xiàn)的地方在于綁定SQL參數(shù)的時(shí)候再源碼中位置為
@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);
}
由于所寫(xiě)的SQL是一個(gè)動(dòng)態(tài)綁定參數(shù)的SQL,因此最終會(huì)走到org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql這個(gè)方法中去
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;
}
在這個(gè)方法中,會(huì)調(diào)用 rootSqlNode.apply(context)方法,由于這個(gè)標(biāo)簽是一個(gè)foreach標(biāo)簽,因此這個(gè)apply方法會(huì)調(diào)用到org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply這個(gè)方法中去
@Override
public 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;
}
當(dāng)調(diào)用appItm方法的時(shí)候?qū)?shù)進(jìn)行綁定,參數(shù)的變量問(wèn)題都會(huì)存在bindings這個(gè)參數(shù)中區(qū)
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
進(jìn)行綁定參數(shù)的時(shí)候,綁定完成foreach的方法的時(shí)候,可以看到bindings中不止綁定了foreach中的兩個(gè)參數(shù)還額外有一個(gè)參數(shù)名字studentName->lct2,也就是說(shuō)最后一個(gè)參數(shù)也是會(huì)出現(xiàn)在bindings這個(gè)參數(shù)中的,
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}

圖20 參數(shù)綁定過(guò)程
最后判定
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;
}
可以看到在調(diào)用evaluateBoolean方法的時(shí)候會(huì)把context.getBindings()就是前邊提到的bindings參數(shù)傳入進(jìn)去,因?yàn)楝F(xiàn)在這個(gè)參數(shù)中有一個(gè)studentName,因此在使用Ognl表達(dá)式的時(shí)候,判定為這個(gè)if標(biāo)簽是有值的因此將這個(gè)標(biāo)簽進(jìn)行了解析

圖21 單個(gè)參數(shù)綁定過(guò)程
最終綁定的結(jié)果為

圖22 全部參數(shù)綁定過(guò)程
因此這個(gè)地方綁定參數(shù)的地方是有問(wèn)題的,至此找出了問(wèn)題的所在。
2.3.2 官方解釋
翻閱MyBatis官方文檔進(jìn)行求證,發(fā)現(xiàn)在3.4.5版本發(fā)行中bug fixes中有這樣一句

圖23 此問(wèn)題官方修復(fù)github記錄
修復(fù)了foreach版本中對(duì)于全局變量context的修改的bug
issue地址為https://github.com/mybatis/mybatis-3/pull/966
修復(fù)方案為https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a
可以看到官方給出的修改方案,重新定義了一個(gè)對(duì)象,分別存儲(chǔ)全局變量和局部變量,這樣就會(huì)解決foreach會(huì)改變?nèi)肿兞康膯?wèn)題。

圖24 此問(wèn)題官方修復(fù)代碼示例
2.3.3 修復(fù)方案
升級(jí)MyBatis版本至3.4.5以上
如果保持版本不變的話(huà),在foreach中定義的變量名不要和外部的一致
03 源碼閱讀過(guò)程總結(jié)
理解,首先 MCube 會(huì)依據(jù)模板緩存狀態(tài)判斷是否需要網(wǎng)絡(luò)獲取最新模板,當(dāng)獲取到模板后進(jìn)行模板加載,加載階段會(huì)將產(chǎn)物轉(zhuǎn)換為視圖樹(shù)的結(jié)構(gòu),轉(zhuǎn)換完成后將通過(guò)表達(dá)式引擎解析表達(dá)式并取得正確的值,通過(guò)事件解析引擎解析用戶(hù)自定義事件并完成事件的綁定,完成解析賦值以及事件綁定后進(jìn)行視圖的渲染,最終將目標(biāo)頁(yè)面展示到屏幕。
MyBatis源代碼的目錄是比較清晰的,基本上每個(gè)相同功能的模塊都在一起,但是如果直接去閱讀源碼的話(huà),可能還是有一定的難度,沒(méi)法理解它的運(yùn)行過(guò)程,本次通過(guò)一個(gè)簡(jiǎn)單的查詢(xún)流程從頭到尾跟下來(lái),可以看到MyBatis的設(shè)計(jì)以及處理流程,例如其中用到的設(shè)計(jì)模式:

圖25 MyBatis代碼結(jié)構(gòu)圖
組合模式:如ChooseSqlNode,IfSqlNode等
模板方法模式:例如BaseExecutor和SimpleExecutor,還有BaseTypeHandler和所有的子類(lèi)例如IntegerTypeHandler
Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
工廠(chǎng)模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory
代理模式:MyBatis實(shí)現(xiàn)的核心,比如MapperProxy、ConnectionLogger
審核編輯:湯梓紅
-
源碼
+關(guān)注
關(guān)注
8文章
678瀏覽量
30966 -
mybatis
+關(guān)注
關(guān)注
0文章
64瀏覽量
7064
發(fā)布評(píng)論請(qǐng)先 登錄
Mybatis緩存之一級(jí)緩存
MyBatis流式查詢(xún)輕松幫你解決分頁(yè)慢的問(wèn)題
Fluent Mybatis、原生Mybatis和Mybatis Plus對(duì)比
源碼學(xué)習(xí)之MyBatis的底層查詢(xún)原理
一文掌握MyBatis的動(dòng)態(tài)SQL使用與原理
MyBatis-Plus為什么不支持聯(lián)表

一文了解MyBatis的查詢(xún)?cè)?/h1>
評(píng)論