MyBatis启动
org.apache.ibatis.session.Configuration
这个类算是MyBatis运行过程中最核心的类了,没有之一。它类似于MyBatis的上下文概念,贯彻了MyBatis的整个生命周期,它存放了MyBatis的基础配置信息,以及在运行时执行sql的MapperStatement。
MyBatis在正式执行前就是初始化该类,并往里面设置数据。
可以看到无论是何种方式使用MyBatis框架,第一件事就是创建生成Configuration。
Configuration的重要属性
Configuration有很多属性,可以看到很多mybatis-config.xml的身影,如的cacheEnabled、useGeneratedKeys、logImpl、defaultExecutorType...具体可以参考MyBatis官方文档对配置信息的说明:MyBatis配置。获取创建好的Configuration可以通过sqlSessionFactory.getConfiguration();来进行获取。
在配置文件中有的,我们就不做多分析了,官方文档有具体的说明,我们分析一下常用并且在配置文件中没有的属性。
mappedStatements
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
在使用MyBatis的时候你有好奇过它是啥时候解析你的Mapper.xml吗?
你有好奇过它是执行一次sql是否就要读取一次Mapper.xml吗?
你有好奇过它参数组装?传参返回结果?include是怎么解析的吗?
通过分析这个这个属性的创建就能得到所有答案。
实际MyBatis在启动过程中会将每个要执行sql的方法都生成一个对应的MappedStatement对象,并将它添加到Configuration的mappedStatements中,它是通过解析注解或者xml进行生成的。
如mapper.xml中:就会生成一个MappedStatemnt,mapper.xml的生成逻辑可以查看XMLMapperBuilder#parse解析生成的。
<select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult"> <include refid="selectConfigVo"/> where config_id = #{configId} </select>在注解中是通过MapperAnnotationBuilder#parse解析生成的。
@Delete("delete from user where id=#{id}") //删除 public void delete(int id);无论是XMLMapperBuilder还是MapperAnnotationBuilder它们都依赖了一个MapperBuilderAssistant实例,这个实例用于在提供MappedStatement的一些帮助,如创建MappedStatement,并添加到Configuration。
在创建完所有要配置的MappedStatement对象后,具体后面执行都是获取对应的MappedStatement对象,然后通过它可以获取相关解析好的SQL语句,对应的API为MappedStatement#getSqlSource#getBoundSql。
sqlFragments
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
这也是一个比较有意思属性,我们通常用的标签,里面的sql片段就是存储在这个属性的。
<sql id="selectConfigVo">select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark FROM ry.sys_config</sql> <select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult"> <include refid="selectConfigVo"/> where config_id = #{configId} </select>它是通过XMLMapperBuilder#configurationElement里面的
sqlElement(context.evalNodes("/mapper/sql"));解析的,解析的结果就是存储到Configuration的sqlFragments中的,这也就是为什么mapper之间可以通过互相引用它们的片段。
另外提一嘴,<include>标签实际底层原理是复制<sql>标签xml节点,然后插入替换掉当前的<include>标签来实现的,具体源码可以参见XMLIncludeTransformer#applyIncludes(org.w3c.dom.Node)
其它
Configuration还有非常多的属性,如:
- languageRegistry,用于注册一个脚本语言解析驱动,默认的有XMLLanguageDriver,它决定在解析sql片段的时候如何解析,如何创建SqlSource,比如我们要实现一个非XML结构的mybatis的mapper解析器,就可以自定义设置这个属性。
- resultMaps,全局的ResultMap,也可以通过namespace引用,和sqlFragements类似。
- ....还有很多,我这只列举说明了在经常使用的一些属性,通过这些属性就可以分析出整个MyBatis的启动流程。
并且在Configuration类中,可以看到非常多MyBatis框架的相关功能具体的实现类,从而方便查阅源码:
可以这样说Configuration描述了MyBatis框架的所有功能,以及具体实现入口,在看MyBatis源码的时候,只要找到关心的属性进行set方法或者调用的地方debug就可以了。
解析Mapper.xml
上面我们分析了MyBatis的启动到使用过程,大体可以总结为:
1、读取配置信息(从mybatis-config.xml或者Spring通过MyBatisProperties属性注入,在MyBatisAutoConfiguration进行设置)
2、根据配置信息生成Configuration实例
3、将Configuration实例传给SqlSessionFactory,进行初始化数据库会话连接工厂
4、通过SqlSessionFactory的实例获取到session就可以进行操作
这里每一步,都有SpringBoot或者MyBatis帮我们实现了,日常开发中更多的工作还是定义Mapper接口,以及在对应的mapper.xml中编写sql,mapper.xml中对sql编写非常灵活,也支持一些非常强大的功能,那么还是非常有必要了解一下,MyBatis是如何解析这么复杂的xml,并且它们之间是如何交互的。
解析Mapper.xml流程
在前面我们知道,解析mapper.xml只是真个MyBatis流程中的一部分,但是也是非常重要的一部分,它发生在创建Configuration阶段,会根据mapper.xml的sql片段生成MappedStatement,这个MappedStatement在前面有具体介绍。
解析mapper.xml的入口
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
接下来我们针对常用的sql片段来进行分析MyBatis是如何实现复杂的mapper.xml解析的。在分析mapper.xml解析细节的的前提,一定要先知道一个类MapperBuilderAssistant这个类是解析mapper.xml的一个辅助类,很多对mapper.xml标签的解析,都是通过这个类来实现的。
cache-ref和cache
cache二级缓存这一块的解析没有上下文关联性,它的逻辑很简单,就是解析cache里面属性然后通过CacheBuilder创建一个Cache对象,并将其放入一个全局配置对象Configuration中,就完事。
cache-ref从Configuration中获取到Cache对象,维护一个引用对象,表示共享其它namespace的缓存对象。
// org.apache.ibatis.builder.MapperBuilderAssistant#useCacheRef
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 搭配下面的cache标签看
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
cache标签,主要是为当前mapper.xml生成一个Cache对象实例并放入Configuration中
核心源码为org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 通过解析到在cache标签的属性,生成一个Cache对象
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 放入Configuration实例中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
这一步解析mapper.xml中的cache只是生成对应的Cache对象和维护好cache-ref,等到真正使用的时候才会起作用。
parameterMap
<parameterMap id="" type=""></parameterMap>
parameterMap是和resultMap有着相似功能的标签,都是描述java实体和数据列之间的映射关系,不过一个是输入(parameterMap),一个是输出(resultMap)。
实际开发中还是很少使用parameterMap的,一般都是用parameterType,除非没有具体对数据库表映射实体(一般不推荐这么做),或者两边完全没有规律可言,如以下场景可以使用该标签:
User.java
public class User {
private String uName;
private String uSix;
}
而数据库中字段为name、six,此时实体和数据库列名称对不上,就需要手动映射了。
言归正传,我们来看解析parameterMap标签MyBatis做了什么:简单来说,创建了一个ParameterMap对象,存到了Configuration对象中,完事儿。
org.apache.ibatis.builder.xml.XMLMapperBuilder#parameterMapElement
// parameterMapElement(context.evalNodes("/mapper/parameterMap"));
private void parameterMapElement(List<XNode> list) {
// 解析mapper中的所有parameterMap,并生成ParameterMapping
for (XNode parameterMapNode : list) {
String id = parameterMapNode.getStringAttribute("id");
String type = parameterMapNode.getStringAttribute("type");
Class<?> parameterClass = resolveClass(type);
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
List<ParameterMapping> parameterMappings = new ArrayList<>();
for (XNode parameterNode : parameterNodes) {
String property = parameterNode.getStringAttribute("property");
String javaType = parameterNode.getStringAttribute("javaType");
String jdbcType = parameterNode.getStringAttribute("jdbcType");
String resultMap = parameterNode.getStringAttribute("resultMap");
String mode = parameterNode.getStringAttribute("mode");
String typeHandler = parameterNode.getStringAttribute("typeHandler");
Integer numericScale = parameterNode.getIntAttribute("numericScale");
ParameterMode modeEnum = resolveParameterMode(mode);
Class<?> javaTypeClass = resolveClass(javaType);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
parameterMappings.add(parameterMapping);
}
// 将ParamterMapping转换成ParameterMap添加到到Configuration中
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}
// org.apache.ibatis.builder.MapperBuilderAssistant#addParameterMap
public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
id = applyCurrentNamespace(id, false);
ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
configuration.addParameterMap(parameterMap);
return parameterMap;
}
当后面解析到DDM(select | insert | update | delete)语句的时候,解析到parameterMap属性的时候,就从Configuration中获取到对应的ParameterMap对象,根据映射关系设置值。
resultMap
resultMap常用,它跟parameterMap一样用来描述查询结果的返回列与java对象实体映射关系,它的解析流程跟paramterMap也差不多。
但是呢,由于parameterMap是根据已有实例进行映射转换,resultMap需要将查询结果转换成对象实例所以需要创建对象,因此resultMap的解析要稍微麻烦一点,涉及到constructor的解析,对象继承体系等的解析,但是最后都是一样的原理,就是生成了一个ResultMap对象,并添加到Configuration中
// resultMapElements(context.evalNodes("/mapper/resultMap"));
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
....
// 创建ResultMap,添加到Configuration
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
使用场景跟parameterMap也是一样的在解析DDM语句的时候,会通过id从Configuration实例中获取到对应ResultMap,并生成对象。
sql片段解析
标签也是属于声明式的标签,它的主要作用是,在其它地方引用,因此根据以上的经验,基本上可以确定对该片段的解析也就是,将片段的信息,存放到Configuration实例中,在需要引用的时候再获取使用。
// 将<sql>片段,存储sqlFragments中
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>片段
public XNode getSqlFragment(String refid) {
return sqlFragments.get(refid);
}
我们可以看到,在sql片段解析结果是存放到sqlFragments这个属性中的,它的声明如下:
private final Map<String, XNode> sqlFragments;
刚刚有说到不是将sql片段存放到Configuration实例对象中嘛,怎么放到sqlFragments属性中了呢?那么这个sqlFragments属性是从何而来的呢?
这个就要看XMLMapperBuilder的构造器
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
可以看到这个sqlFragments属性是通过构造器传入的,然后回到XMLConfigBuilder#mapperElement方法,可以看到创建XMLMapperBuilder的代码:
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
可以看到sqlFragments是通过configuration.getSqlFragments() 传入的,实际中也是在Configuration实例对象中,那么为什么要这样使用呢,而不是像parameterMap和resultMap直接通过configuration引用添加呢?这是因为的refid可以通过namespace引入其它的mapper文件的sql片段,不限于当前mapper.xml,这样可以进行全局添加。
DDM语句解析
前面分析了解析Mapper.xml的几个全局标签,接下来我们分析分析开发过程中最常用的标签,即DDM语句的标签<select>、<insert>、<update>、<delete> 。这些标签对应了mapper接口方法,它们的解析在全局标签之后,因为它们会引入或者引用全局标签。
它们入口同样位于:XMLMapperBuilder#configurationElement
// XMLMapperBuilder#configurationElement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> 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);
}
}
}
可以看到源码中,针对DDM语句,专门有一个类来进行解析,就是XMLStatementBuilder,调用它的statementParser.parseStatementNode(); 方法进行解析。
在statementParser.parseStatementNode(); 这个方法中我们会看到非常多熟悉的属性:
public void parseStatementNode() {
// 解析id和databaseId
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 如果不匹配当前数据库方言,则不做解析sql片段
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 是否刷新缓存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 是否使用缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 没用过
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析include标签,用sql标签替换include处的标签
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析parameterType
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 解析语言驱动,默认就是XMLLanguageDriver,用于决定解析动态sql节点,ddm语句下的所有子标签节点都是由它来确定解析的
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 解析selectKey标签,常用于生成id
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 通过上面的LanguageDriver去解析标签里面的原生,生成SqlSource。这个SqlSource包含了预编译sql,也就是说是这里解析标签并生成sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 下面就是对一些属性进行取值
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 通过MapperBuilderAssistant往Configuration里面添加一个MappedStatement对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
可以看到上面解析ddm语句的核心逻辑除了对一些属性取值外,最核心的就在于对标签解析和解析标签生成sql脚本:
// 解析include标签,用sql标签替换include处的标签
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 通过上面的LanguageDriver去解析标签里面的原生,生成SqlSource。这个SqlSource包含了预编译sql,也就是说是这里解析标签并生成sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
注意:这里的context对象就是对应<select|update|insert|delete>的xml节点对象
include标签解析
标签搭配着标签 一起使用,从而达到对sql语句的复用目的。
<sql id="selectDeptVo">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time
from sys_dept d
</sql>
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
<include refid="selectDeptVo"/>
where d.del_flag = '0'
</select>
日常中我们使用的也挺多,接下来我们分析分析MyBatis是如何解析include的,在分析include之前要先了解前面标签的解析,回顾一下,前面说道对标签解析,会将读取到xml中的节点放入Configuration实例中。 在include会根据refid从Configuration实例中获取到节点,并将其复制一份里面内容将其替换掉原来的节点,从而完成标签的解析处理。
接下来看源码解析:
// org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
// 在实际解析xml节点之前,先解析<include>标签
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 替换include
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if ("include".equals(source.getNodeName())) {
// 根据include的refid从Configuration中获取<sql>节点
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 处理<include>节点里面的<include>标签
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 执行替换,用sql节点替换掉include节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
// 最后移除<sql>节点
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 进入该分支,已经不是include第一层了
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
// 可能包含多个include子节点,继续解析
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
总的来说,对include标签的解析是一种替换操作,但是替换过程包含了属性解析和递归替换,看起来稍微复杂点,但是debug跟一下代码就可以了。
SQL语句解析
前面的所有节点解析,都是为了最后一步,因为搞那么多功能,最终也是为了生成一个完整的sql语句,实际从XML中分析生成一个预编译SQL语句的地方就是这一步完成的,也就是MyBatis中创建SqlSource。
具体生成SQL语句需要经历,标签解析->转换为SqlNode对象->根据参数调用SqlNode动态生成拼接SQL语句。
这个SqlSource是一个接口,会返回一个BoundSql对象,而BoundSql描述了一个sql以及参数信息。最后老规矩将SqlSource的实例包装到MappedStatement中,放入到Configuration实例中。
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
public class BoundSql {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}
SqlSource有如下的实现类:
分别是动态、注解、原生、静态的SqlSource。
对外暴露的一般就是DynamicSqlSource、ProviderSqlSource、RawSqlSource。StaticSqlSource其实和RawSqlSource是搭配使用的,这两个都是描述该sql是一个静态sql,不存在动态标签。
动态标签指的是if、where、foreach....这种标签
解析源码分析:
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 创建SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
由语言驱动进行创建,默认的语言驱动MyBatis默认是使用XMLLanguageDriver来解析的。
在XMLLanguageDriver中,用了XMLScriptBuilder来解析具体的sql语句:
public class XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
}
在XMLScriptBuilder中的parseScriptNode()方法:
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
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
可以看到最后返回了一个动态sqlSource和原生的sqlSource,具体返回哪一个是根据parseDynamicTags(context) 解析结果判断的。而这个方法就是解析动态节点的入口,也是核心,如if、where、set等这些标签都算是动态节点。
具体处理动态节点的处理类:
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());
}
从以上源码可以看到,每一个动态标签都对应了一个处理类,这些处理类都是NodeHandler接口的实现类
private interface NodeHandler {
/**
nodeToHandle 当前动态节点,如<if>....</if>
targetContents 将xml中node节点也就是参数nodeToHandle进行处理转换成SqlNode加入到这个集合中
这个集合描述了整个整个sql片段中的所有node节点
如<select>
select * from tab_name where
<where>
<if test="configId !=null">and config_id = #{configId}</if>
</where>
</select>
这一段select将全部被转为SqlNode的实例,放入到targetContents集合中
*/
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
调用NodeHandler将xml的节点转为SqlNode对象:
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
// 遍历子节点,如果是动态节点,则通过NodeHandler递归调用解析
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 文本节点,如 select * from tab_name where ,在w3c-dom中也会被解析为一个节点
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 {
// 纯静态文本节点,就是一个字符串,可以直接拼接的那种
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 非文本节点,也就是<where><if>这种节点则需要通过获取到对应的Handler来处理,这个Handler是由initNodeHandlerMap维护的
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 使用具体的handler来解析动态节点,在handler里面也是会调用这个方法的,因此才说使用了递归调用
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
parseDynamicTags方法的作用就是解析一个sql节点,将里面的所有内容都转换为SqlNode的实现类。这个转换操作是由具体的Handler来操作的,也就是前面的initNodeHandlerMap初始化映射实例化的Handler。
SqlNode实现类如下:
我们随便挑一个简单的SqlNode实现类来分析,如IfSqlNode,他负责解析<if test="orgId!=null">.....</if>这样的语句:
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 递归调用parseDynamicTags方法解析<if>下面的子节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 取出当前if的条件表达式,创建IfSqlNode实例,并将其添加到targetContents中
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
我们可能会有疑问,因为在通常xml节点定义的时候子节点是由先后和嵌套关系的,这种一股脑丢给targetContens最后解析的时候不是变得无序了嘛?这时候就不得不提一个辅助SqlNode就是MixedSqlNode在分析SqlNode实现类的时候,每个实现类都能找到标签,但是这个找不到,是因为这个MixedSqlNode是不对外使用的它就是为了维护SqlNode直接的嵌套层次顺序关系的。
简单来说这个MixedSqlNode就是一个复合型SqlNode,没有具体含义,单纯维护标签转换成SqlNode的层次关系,该节点在parseDynamicTags方法返回的时候创建:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
例如我们这样一段xml:
<select id="selectRoleDeptTree" parameterType="Long" resultType="String">
select concat(d.dept_id, d.dept_name) as dept_name
from sys_dept d
left join sys_role_dept rd on d.dept_id = rd.dept_id
<where>
d.del_flag = '0'
<if test="roleId!=null">
AND rd.role_id = #{roleId}
</if>
</where>
order by d.parent_id, d.order_num
</select>
最后生成的SqlNode结构为:
MixedSqlNode
[0]StaticTextSqlNode select concat(d.dept_id, d.dept_name) as dept_name from sys_dept d ....
[1]WhereSqlNode
StaticTextSqlNode d.del_flag = '0'
IfSqlNode 条件:roleId!=null
TextSqlNode AND rd.role_id = #{roleId}
[2]StaticTextSqlNode order by d.parent_id, d.order_num
以上描述了调用parseDynamicTags方法后,返回的MixedSqlNode的一个层次关系。
最后将解析的结果生成对应SqlSource,用于创建MappedStatement放入Configuration中。
注意:此时还没有生成sql语句,只是将xml中的sql标签转为了一个个SqlNode对象放入了Configuration对象中,等到真正执行的时候回调用这些SqlNode的实例进行sql语句拼接