博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Mybatis 解析 SQL 源码分析一
阅读量:6226 次
发布时间:2019-06-21

本文共 17626 字,大约阅读时间需要 58 分钟。

hot3.png

相关文章

前言

在使用 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 可以参考  。

接下来看下 XMLMapperBuilder 类的属性定义:

public class XMLMapperBuilder extends BaseBuilder {  // xpath 包装类  private XPathParser parser;  // MapperBuilder 构建助手  private MapperBuilderAssistant builderAssistant;  // 用来存放sql片段的哈希表  private Map
sqlFragments; // 对应的 mapper 文件 private String resource; private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map
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(); } // 处理解析失败的
节点 parsePendingResultMaps(); // 处理解析失败的
节点 parsePendingChacheRefs(); // 处理解析失败的 SQL 节点 parsePendingStatements(); }

从上面的代码中,使用到了 MapperBuilderAssistant 辅助类,该类中有许多的辅助方法,其中有个  currentNamespace 属性用来表示当前的 Mapper.xml 配置文件的命名空间,在解析完成 Mapper.xml 配置文件的时候,会调用 bindMapperForNamespace 进行注册 Mapper接口,表示该配置文件对应的 Mapper接口,关于 Mapper 的注册可以参考 :

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);      // 解析 
节点 cacheRefElement(context.evalNode("cache-ref")); // 解析
节点 cacheElement(context.evalNode("cache")); // 已废弃,忽略 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析
节点 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析
节点 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> 节点:

// 解析 
节点 private void cacheElement(XNode context) throws Exception { if (context != null) { // 获取 type 属性,默认为 PERPETUAL String type = context.getStringAttribute("type", "PERPETUAL"); Class
typeClass = typeAliasRegistry.resolveAlias(type); // 获取过期策略 eviction 属性 String eviction = context.getStringAttribute("eviction", "LRU"); Class
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); // 获取
节点下的子节点,将用于初始化二级缓存 Properties props = context.getChildrenAsProperties(); // 创建 Cache 对象,并添加到 configuration.caches 集合中保存 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }

 接下来看下 MapperBuilderAssistant 辅助类如何创建缓存,并添加到 configuration.caches 集合中去:

public Cache useNewCache(Class
typeClass, Class
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
implementation; // 装饰器集合 private List
> 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); // 设置
节点信息 setCacheProperties(cache); if (PerpetualCache.class.equals(cache.getClass())) { for (Class
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> 进行指定

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 节点很强大,也很复杂,会单独另写一篇。参考:

解析 <sql> 节点

<sql> 节点可以用来重用SQ片段,

id, name, job, age
sqlElement(context.evalNodes("/mapper/sql"));

sqlElement 方法如下,一个 Mapper.xml 文件可以有多个 sql 节点:

private void sqlElement(List
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
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
list) {// 匹配对应的数据库if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId());}buildStatementFromContext(list, null);}private void buildStatementFromContext(List
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); // 解析
节点 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片段,在解析
节点的时候,把它放到了一个map中,key为namespace+id,value为对应的节点,现在要拿 ref 属性去这个集合里面获取对应的SQL片段 Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); // 解析include的子节点
Properties toIncludeContext = getVariablesContext(source, variablesContext); // 递归处理
节点 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 节点,在文章的开头部分,在解析节点的时候,有时候可能会出错,抛出异常,在解析每个解析抛出异常的时候,都会把该解析放入到对应的集合中再次进行解析,所以在解析完成后,还有如下三行代码:

// 处理解析失败的 
节点 parsePendingResultMaps(); // 处理解析失败的
节点 parsePendingChacheRefs(); // 处理解析失败的 SQL 节点 parsePendingStatements();

就是用来从新解析失败的那些节点的。

到这里,Mapper.xml 配置文件就解析完毕了。

转载于:https://my.oschina.net/mengyuankan/blog/2874776

你可能感兴趣的文章
Java课堂 动手动脑5
查看>>
Python实战之字符串的详细简单练习
查看>>
SSM框架快速整合实例——学生查询
查看>>
p标签中的文字垂直居中
查看>>
小程序(将Solaris下的换行符转化为windows下的换行符)
查看>>
MY-IMX6 Linux-3.14 测试手册(Qt版)
查看>>
js客户端UI框架
查看>>
【转】四元数(Quaternion)和旋转
查看>>
使用vue.js常见错误之一
查看>>
centos7配置openldap服务器
查看>>
bzoj 1500 修改区间 splay
查看>>
组合数打表法(1587: 爬楼梯)
查看>>
Symmetric Tree
查看>>
Oracle用户管理
查看>>
关于网络爬取(爬虫)01
查看>>
python re模块findall()详解
查看>>
MSTest
查看>>
java 给任务传递参数
查看>>
oracle之 反向键索引
查看>>
mysql+keepalived 双主热备高可用
查看>>