走進Mybatis
上文中簡單的介紹過了Mybatis的使用毒涧。本篇文章將介紹Mybatis如何解析XML
組件:
SqlSessionFactory
用于生成SQLSession 的工廠類SqlSession
Mybatis 的核心API,表示和數(shù)據(jù)交互的會話襟诸,用于處理所有的增刪改查功能。Configuration
Configuration可以說是一個倉庫類了。所有的配置項都集中在這個類里面。在SqlSessionFactoryBuilder構(gòu)建SqlSessionFactory的時候奕剃,會創(chuàng)建XMLConfigBuilder,然后會解析到Configuration類哑姚,再通過這個Configuration類來構(gòu)建SqlSessionFactory祭饭。所有所有的XML文件信息到最后都是通過Configuration來承載,供SQLSessionFactory創(chuàng)建SQLSession叙量。
public class Configuration {
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
//日志前綴
protected String logPrefix;
// 日志接口
protected Class <? extends Log> logImpl;
protected Class <? extends VFS> vfsImpl;
//
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
//數(shù)據(jù)庫類型
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
//
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
*
* @see <a >Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<>();
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
/*
* A map holds cache-ref relationship. The key is the namespace that
* references a cache bound to another namespace and the value is the
* namespace which the actual cache is bound to.
*/
protected final Map<String, String> cacheRefMap = new HashMap<>();
解析XML的過程,可以理解為就是在填充Configuration的過程九串。
SqlSessionFactoryBuilder().build(inputStream);
一切的解析都是由build開始绞佩。最終會調(diào)用到 XMLConfigBuilder.parseConfiguration方法。
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在該方法中對每個節(jié)點進行單獨的解析猪钮。
properties
解析Mapper.xml
在解析XML中最重要的是就是解析Mapper.xml了品山,也就是代碼中的: mapperElement(root.evalNode("mappers"))
其最終會調(diào)用到:
XMLMapperBuilder.configurationElement
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"));
//解析參數(shù)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析結(jié)果集
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析SQL節(jié)點
sqlElement(context.evalNodes("/mapper/sql"));
//解析SQL語句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
SQL節(jié)點
SQL節(jié)點可以將將重復的sql提取出來,在使用到的地方使用include醫(yī)用即可烤低,最終達到SQL重用的目的肘交。它的解析很簡單,就是把內(nèi)容放入sqlFragments容器扑馁。id為命名空間+節(jié)點ID
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
SQL語句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
這一段是Mybatis的核心所在涯呻。他會把Mapper.xml里面的SQL語句進行解析凉驻。分為兩種類型。存在動態(tài)標簽的會被解析為動態(tài)SQL, 不存在動態(tài)標簽的則被解析為靜態(tài)SQL复罐。
動態(tài)標簽指的是在SQL里面添加的 <if>涝登、<choose>、<foreach>效诅、<trim>胀滚、<set> 等的標簽。
例如:
<select id="selectByIds" resultType="com.xavier.mybatis.User">
SELECT * FROM TB_USER
where id in
<foreach collection="ids" item="id" index="index" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
在解析SQL的時候乱投,會根據(jù)SQL是否有動態(tài)標簽來確定初始化的SqlSource的類型咽笼。動態(tài)的為DynamicSqlSource, 靜態(tài)的為RawSqlSource .
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
我們來看一下parseDynamicTags方法:
MixedSqlNode rootSqlNode = parseDynamicTags(context);
如其名,他是解析SQL語句的戚炫,并且返回MixedSqlNode
而MixedSqlNode 的結(jié)構(gòu)如下剑刑。它包含了一個SqlNode的List
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
}
接著看它解析過程:
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
//如果為靜態(tài)屬性,則添加 __StaticTextSqlNode__
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//獲取節(jié)點的名字
String nodeName = child.getNode().getNodeName();
//根據(jù)節(jié)點的名字獲取節(jié)點處理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//節(jié)點處理器處理節(jié)點
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
可以看到嘹悼,這就是對子節(jié)點的處理叛甫。
- 判斷節(jié)點的內(nèi)容是不是純文本。如果是純文本就構(gòu)建靜態(tài)對象StaticTextSqlNode
- 如果是動態(tài)標簽杨伙,則獲取節(jié)點類型其监,然后交給對應(yīng)的類型處理。
為每一個動態(tài)標簽綁定不同的處理器在:
XMLScriptBuilder.initNodeHandlerMap
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
我們分析一下foreach的Handler:
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
ForEachHandler的處理方式限匣,
- 遞歸調(diào)用parseDynamicTags(),繼續(xù)處理自己的節(jié)點
- 獲取自己定義的各種參數(shù): collection, item , index, open ,close ……
- 構(gòu)建ForEachSqlNode對象抖苦,然后把它加入到 targetContents
其他類型的處理器大致如此。
所以米死,我們最后解析獲取到的MixedSqlNode對象就是一棵以樹锌历,其中的contents是這棵樹的第一級子節(jié)點。
在初始化好了SqlSource之后峦筒,Mybatis會將SqlSource連同入?yún)⒕课鳎鰠ⅲ鹊纫幌盗袇?shù)封裝為一個MappedStatement物喷,然后注冊到configuration這個大管家類中:
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
在大管家中的mappedStatements 屬性是一個StrictMap<MappedStatement> 卤材,所以存入的時候,key 為唯一識別這個Statement的東西峦失,在mybatis中是由 namespace+mappper中節(jié)點的id扇丛,在我們案例中就是:com.xavier.mybatis.UserMapper.selectUser