走进Mybatis
上文中简单的介绍过了Mybatis的使用。本篇文章将介绍Mybatis如何解析XML
组件:
SqlSessionFactory
用于生成SQLSession 的工厂类SqlSession
Mybatis 的核心API,表示和数据交互的会话,用于处理所有的增删改查功能。Configuration
Configuration可以说是一个仓库类了。所有的配置项都集中在这个类里面。在SqlSessionFactoryBuilder构建SqlSessionFactory的时候,会创建XMLConfigBuilder,然后会解析到Configuration类,再通过这个Configuration类来构建SqlSessionFactory。所有所有的XML文件信息到最后都是通过Configuration来承载,供SQLSessionFactory创建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;
//数据库类型
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 href='https://code.google.com/p/mybatis/issues/detail?id=300'>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开始。最终会调用到 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);
}
}
在该方法中对每个节点进行单独的解析。
properties
解析Mapper.xml
在解析XML中最重要的是就是解析Mapper.xml了,也就是代码中的: mapperElement(root.evalNode("mappers"))
其最终会调用到:
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"));
//解析参数
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析结果集
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析SQL节点
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节点
SQL节点可以将将重复的sql提取出来,在使用到的地方使用include医用即可,最终达到SQL重用的目的。它的解析很简单,就是把内容放入sqlFragments容器。id为命名空间+节点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语句进行解析。分为两种类型。存在动态标签的会被解析为动态SQL, 不存在动态标签的则被解析为静态SQL。
动态标签指的是在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的时候,会根据SQL是否有动态标签来确定初始化的SqlSource的类型。动态的为DynamicSqlSource, 静态的为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 的结构如下。它包含了一个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 {
//如果为静态属性,则添加 __StaticTextSqlNode__
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//获取节点的名字
String nodeName = child.getNode().getNodeName();
//根据节点的名字获取节点处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//节点处理器处理节点
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
可以看到,这就是对子节点的处理。
- 判断节点的内容是不是纯文本。如果是纯文本就构建静态对象StaticTextSqlNode
- 如果是动态标签,则获取节点类型,然后交给对应的类型处理。
为每一个动态标签绑定不同的处理器在:
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的处理方式,
- 递归调用parseDynamicTags(),继续处理自己的节点
- 获取自己定义的各种参数: collection, item , index, open ,close ……
- 构建ForEachSqlNode对象,然后把它加入到 targetContents
其他类型的处理器大致如此。
所以,我们最后解析获取到的MixedSqlNode对象就是一棵以树,其中的contents是这棵树的第一级子节点。
在初始化好了SqlSource之后,Mybatis会将SqlSource连同入参,出参,等等一系列参数封装为一个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中节点的id,在我们案例中就是:com.xavier.mybatis.UserMapper.selectUser