相关文章
Mybatis 数据源和数据库连接池源码解析(DataSource)
Mybatis Mapper 接口源码解析(binding包)
Mybatis Mapper.xml 配置文件中 resultMap 节点的源码解析
前言
在使用 Mybatis 的时候,我们在 mapper.xml 配置文件中书写 SQL;文件中还配置了对应的dao,SQL 中还可以使用一些诸如for循环,if判断之类的高级特性,当数据库列和JavaBean属性不一致时定义的 resultMap等,接下来就来看下 Mybatis 是如何从配置文件中解析出 SQL 并把用户传的参数进行绑定;在 Mybatis 解析 SQL 的时候,可以分为两部分来看,一是从 Mapper.xml 配置文件中解析SQL,二是把 SQL 解析成为数据库能够执行的原始 SQL,把占位符替换为 ? 等。这篇文章先来看下第一部分,Mybatis 如如何从 Mapper.xml 配置文件中解析出 SQL 的。
配置文件的解析使用了大量的建造者模式(builder)
mybatis-config.xml 解析
Mybatis 有两个配置文件,mybaits-config.xml 配置的是 mybatis 的一些全局配置信息,而 mapper.xml 配置的是 SQL 信息,在 Mybatis 初始化的时候,会对这两个文件进行解析,mybatis-config.xml 配置文件的解析比较简单,不再细说,使用的 XMLConfigBuilder 类来对 mybatis-config.xml 文件进行解析。
public Configuration parse() {
// 如果已经解析过,则抛异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 解析 mybatis-config.xml 文件下的所有节点
private void parseConfiguration(XNode root) {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
// .... 其他的节点........
// 解析 mapper.xml 文件
mapperElement(root.evalNode("mappers"));
}
// 解析 mapper.xml 文件
private void mapperElement(XNode parent) throws Exception {
// ......
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
configuration.getSqlFragments());
mapperParser.parse();
}
从上述代码可以看到,解析 Mapper,.xml 配置文件是通过 XMLMapperBuilder 来解析的。接下来看下该类的实现
XMLMapperBuilder
XMLMapperBuilder 类是用来解析 Mapper.xml 文件的,它继承了 BaseBuilder ,BaseBuilder 类一个建造者基类,其中包含了 Mybatis 全局的配置信息 Configuration ,别名处理器,类型处理器等,如下所示:
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
关于 TypeAliasRegistry, TypeHandlerRegistry 可以参考 Mybatis 类型转换源码分析 。
接下来看下 XMLMapperBuilder 类的属性定义:
public class XMLMapperBuilder extends BaseBuilder {
// xpath 包装类
private XPathParser parser;
// MapperBuilder 构建助手
private MapperBuilderAssistant builderAssistant;
// 用来存放sql片段的哈希表
private Map<String, XNode> sqlFragments;
// 对应的 mapper 文件
private String resource;
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;
}
// 解析文件
public void parse() {
// 判断是否已经加载过该配置文件
if (!configuration.isResourceLoaded(resource)) {
// 解析 mapper 节点
configurationElement(parser.evalNode("/mapper"));
// 将 resource 添加到 configuration 的 addLoadedResource 集合中保存,该集合中记录了已经加载过的配置文件
configuration.addLoadedResource(resource);
// 注册 Mapper 接口
bindMapperForNamespace();
}
// 处理解析失败的 <resultMap> 节点
parsePendingResultMaps();
// 处理解析失败的 <cache-ref> 节点
parsePendingChacheRefs();
// 处理解析失败的 SQL 节点
parsePendingStatements();
}
从上面的代码中,使用到了 MapperBuilderAssistant 辅助类,该类中有许多的辅助方法,其中有个 currentNamespace 属性用来表示当前的 Mapper.xml 配置文件的命名空间,在解析完成 Mapper.xml 配置文件的时候,会调用 bindMapperForNamespace 进行注册 Mapper接口,表示该配置文件对应的 Mapper接口,关于 Mapper 的注册可以参考 Mybatis Mapper 接口源码解析(binding包):
private void bindMapperForNamespace() {
// 获取当前的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = Resources.classForName(namespace);
if (boundType != null) {
// 如果还没有注册过该 Mapper 接口,则注册
if (!configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
// 注册
configuration.addMapper(boundType);
}
}
}
现在就来解析 Mapper.xml 文件的每个节点,每个节点的解析都封装成一个方法,很好理解:
private void configurationElement(XNode context) {
// 命名空间
String namespace = context.getStringAttribute("namespace");
// 设置命名空间
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref namespace=""/> 节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache /> 节点
cacheElement(context.evalNode("cache"));
// 已废弃,忽略
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap /> 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql> 节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 select|insert|update|delete 这几个节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
解析 <cache> 节点
Mybatis 默认情况下是没有开启二级缓存的,除了局部的 session 缓存。如果要为某个命名空间开启二级缓存,则需要在 SQL 映射文件中添加<cache> 标签来告诉 Mybatis 需要开启二级缓存,先来看看 <cache> 标签的使用说明:
<cache eviction="LRU" flushInterval="1000" size="1024" readOnly="true" type="MyCache" blocking="true"/>
<cache> 一共有 6 个属性,可以用来改变 Mybatis 缓存的默认行为:
1. eviction: 缓存的过期策略,可以取 4 个值:
- LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认)
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
2. flushInterval: 刷新缓存的时间间隔,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
3. size: 缓存大小
4. readOnly: 是否是只读
5. type : 自定义缓存的实现
6. blocking:是否是阻塞
该类中主要使用 cacheElement 方法来解析 <cache> 节点:
// 解析 <cache> 节点
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// 获取 type 属性,默认为 PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取过期策略 eviction 属性
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取 <cache> 节点下的子节点,将用于初始化二级缓存
Properties props = context.getChildrenAsProperties();
// 创建 Cache 对象,并添加到 configuration.caches 集合中保存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
接下来看下 MapperBuilderAssistant 辅助类如何创建缓存,并添加到 configuration.caches 集合中去:
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass,
Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
// 创建缓存,使用构造者模式设置对应的属性
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.addCache(cache);
// 当前缓存
currentCache = cache;
return cache;
}
再来看下 CacheBuilder 是个什么东西,它是 Cache 的建造者,如下所示:
public class CacheBuilder {
// Cache 对象的唯一标识,对应配置文件中的 namespace
private String id;
// Cache 的实现类
private Class<? extends Cache> implementation;
// 装饰器集合
private List<Class<? extends Cache>> decorators;
private Integer size;
private Long clearInterval;
private boolean readWrite;
// 其他配置信息
private Properties properties;
// 是否阻塞
private boolean blocking;
// 创建 Cache 对象
public Cache build() {
// 设置 implementation 的默认值为 PerpetualCache ,decorators 的默认值为 LruCache
setDefaultImplementations();
// 创建 Cache
Cache cache = newBaseCacheInstance(implementation, id);
// 设置 <properties> 节点信息
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
}
解析 <cache-ref> 节点
在使用了 <cache> 配置了对应的指定缓存后,多个 namespace 可以引用同一个缓存,使用 <cache-ref> 进行指定
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
cacheRefElement(context.evalNode("cache-ref"));
解析的源码如下,比较简单:
private void cacheRefElement(XNode context) {
// 当前文件的namespace
String currentNamespace = builderAssistant.getCurrentNamespace();
// ref 属性所指向引用的 namespace
String refNamespace = context.getStringAttribute("namespace");
// 会存入到 configuration 的一个 map 中, cacheRefMap.put(namespace, referencedNamespace);
configuration.addCacheRef(currentNamespace , refNamespace );
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, refNamespace);
// 实际上调用 构建助手 builderAssistant 的 useCacheRef 方法进行解析
cacheRefResolver.resolveCacheRef();
}
}
构建助手 builderAssistant 的 useCacheRef 方法:
public Cache useCacheRef(String namespace) {
// 标识未成功解析的 Cache 引用
unresolvedCacheRef = true;
// 根据 namespace 中 configuration 的缓存集合中获取缓存
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("....");
}
// 当前使用的缓存
currentCache = cache;
// 已成功解析 Cache 引用
unresolvedCacheRef = false;
return cache;
}
解析 <resultMap> 节点
resultMap 节点很强大,也很复杂,会单独另写一篇。参考:Mybatis Mapper.xml 配置文件中 resultMap 节点的源码解析
解析 <sql> 节点
<sql> 节点可以用来重用SQ片段,
<sql id="commSQL" databaseId="" lang="">
id, name, job, age
</sql>
sqlElement(context.evalNodes("/mapper/sql"));
sqlElement 方法如下,一个 Mapper.xml 文件可以有多个 sql 节点:
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
// 遍历,处理每个 sql 节点
for (XNode context : list) {
// 数据库ID
String databaseId = context.getStringAttribute("databaseId");
// 获取 id 属性
String id = context.getStringAttribute("id");
// 为 id 加上 namespace 前缀,如原来 id 为 commSQL,加上前缀就变为了 com.aa.bb.cc.commSQL
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 如果 SQL 片段匹配对应的数据库,则把该节点加入到缓存中,是一个 map
// Map<String, XNode> sqlFragments
sqlFragments.put(id, context);
}
}
}
为 ID 加上namespace前缀的方法如下:
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
// 是否已经包含 namespace 了
if (isReference) {
if (base.contains(".")) {
return base;
}
} else {
// 是否是一 namespace. 开头
if (base.startsWith(currentNamespace + ".")) {
return base;
}
}
// 返回 namespace.id,即 com.aa.bb.cc.commSQL
return currentNamespace + "." + base;
}
insert | update | delete | select 节点的解析
关于这些与操作数据库的SQL的解析,主要是由 XMLStatementBuilder 类来进行解析。在 Mybatis 中使用 SqlSource 来表示 SQL语句,但是这些SQL 语句还不能直接在数据库中进行执行,可能还有动态SQL语句和占位符等。
接下来看下这类节点的解析
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) {
// 为 XMLStatementBuilder 对应的属性赋值
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
// 解析每个节点
statementParser.parseStatementNode();
}
可以看到 selelct | insert | update | delete 这类节点是使用 XMLStatementBuilder 类的 parseStatementNode() 方法来解析的,接下来看下该方法的实现:
public void parseStatementNode() {
// id 属性和数据库标识
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 如果数据库不匹配则不加载
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点的属性和对应属性的类型
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
// 从注册的类型里面查找参数类型
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 从注册的类型里面查找返回值类型
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取节点的名称
String nodeName = context.getNode().getNodeName();
// 根据节点的名称来获取节点的类型,枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 下面这三行代码,如果是select语句,则不会刷新缓存和需要使用缓存
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析 <include> 节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析 selectKey 节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 创建 sqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 处理 resultSets keyProperty keyColumn 属性
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
// 处理 keyGenerator
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))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
// 创建 MapperedStatement 对象,添加到 configuration 中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
该方法主要分为几个部分,1,解析属性,2,解析 include 节点,3,解析 selectKey 节点,4,创建 MapperedStatment对象并添加到configuration对应的集合中;解析属性比较简单,接下来看看后面几个部分:
解析include解析
解析include节点就是把其包含的SQL片段替换成 <sql> 节点定义的SQL片段,并将 ${xxx} 占位符替换成真实的参数。:
它是使用 XMLIncludeTransformer 类的 applyIncludes 方法来解析的:
public void applyIncludes(Node source) {
// 获取参数
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
if (configurationVariables != null) {
variablesContext.putAll(configurationVariables);
}
// 解析
applyIncludes(source, variablesContext, false);
}
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
// 这里是根据 ref 属性对应的值去 <sql> 节点对应的集合查找对应的SQL片段,在解析 <sql> 节点的时候,把它放到了一个map中,key为namespace+id,value为对应的节点,现在要拿 ref 属性去这个集合里面获取对应的SQL片段
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// 解析include的子节点<properties>
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 递归处理<include>节点
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 将 include 节点替换为 sql 节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 处理当前SQL节点的子节点
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// 绑定参数
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
selectKey 就是生成主键,可以不用看。
到这里,mapper.xml 配置文件中的节点已经解析完毕了 除了 resultMap 节点,在文章的开头部分,在解析节点的时候,有时候可能会出错,抛出异常,在解析每个解析抛出异常的时候,都会把该解析放入到对应的集合中再次进行解析,所以在解析完成后,还有如下三行代码:
// 处理解析失败的 <resultMap> 节点
parsePendingResultMaps();
// 处理解析失败的 <cache-ref> 节点
parsePendingChacheRefs();
// 处理解析失败的 SQL 节点
parsePendingStatements();
就是用来从新解析失败的那些节点的。
到这里,Mapper.xml 配置文件就解析完毕了。