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