源码敲门砖-mybatis源码-mapper详解和底层如何解析sql


上一篇只是简单的对mybatis的基本流程进行了,探查。
这次准备深入看一下上篇的 使用XMLConfigBuilder进行解析, mapper到底如何配置,注解的sql如何获取的?

(一)配置mapper的方式:

根据官方文档,可以找到一共有四种方式。
官方文档链接
那么这几种方式的优先级和配置如何解析和加载的?

(二)如何解析:

上片文章中已经提到,

2.1 解析主配置文件的主要方法:

栈调用:

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
org.apache.ibatis.builder.xml.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);
  }
}

2.2 解析mappers

2.2.1、源码:

org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
         //通过反射获取接口,将接口放到configuration 
	Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

2.2.2 mappers中子标签的优先级和配置规则

根据代码可知,首先会判断mappers标签下,子元素是package还是mapper。
Package与mapper是不可同时存在的。下图可知:
在这里插入图片描述

Package子标签:

Package指定这个包下所有的接口都是持久层接口,或者就是使用指定一个个mapper。

Mapper子标签:

Mapper标签下:三种类型的路径指定属性(resource,class,url),且每种只能使用一个属性。
Resource:xml相对于本配置文件的相对路径
Class:接口的包名全路径
url:绝对路径(file://+xml文件的全路径)
规则:
三种属性不能配置在同一个标签内:
<mapper resource="mapper/BlogMapper.xml" class="com.study.mapper.MyMapper"/>
就会导致报错:

A mapper element may only specify a url, resource or class, but not more than one.

同一个文件,只能配置一次mapper,否则也会报错:
Mapped Statements collection already contains value for org.apache.ibatis.domain.blog.mappers.BlogMapper.selectOne. please check mapper/BlogMapper.xml and file:///F:\IDEAR\IdeaProjects\mybatis3\mybatis-3\src\test\resources\mapper\BlogMapper.xml
正确使用示例:

<mappers>
  <!--<package name="com.study.mapper"/>-->
  <mapper class="com.study.mapper.MyMapper"/>
  <!--<mapper resource="mapper/BlogMapper.xml" />-->
  <mapper url="file:///F:\IDEAR\IdeaProjects\mybatis3\mybatis-3\src\test\resources\mapper\BlogMapper.xml"/>
</mappers>
2.2.3 跟踪源码探查根源:

根据2.2.1的源码可知,

Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
解析mapper时,会将mapper接口放到configuration。

DEBUG 在addMapper打断点:
对应的栈调用:

parse:126, MapperAnnotationBuilder (org.apache.ibatis.builder.annotation)
addMapper:72, MapperRegistry (org.apache.ibatis.binding)
addMapper:810, Configuration (org.apache.ibatis.session)
mapperElement:383, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parseConfiguration:120, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parse:99, XMLConfigBuilder (org.apache.ibatis.builder.xml)
build:78, SqlSessionFactoryBuilder (org.apache.ibatis.session)
build:64, SqlSessionFactoryBuilder (org.apache.ibatis.session)
main:42, MybatisMain (com.study.anno)

addMapper源码:

configuration的addmapper:mapperRegistry.addMapper(type);

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {首先会判断是否已存在,如果有了就不会再往里放了
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {//放入mapper的动态代理工厂
      knownMappers.put(type, new MapperProxyFactory<>(type));
    /*解析前先将mapper的类型type(我这里是myMapper)
    配置给解析器parser,这样会直接用这个类型去解析mapper接口。
    否则会尝试去找对应的类型然后再解析。   */
 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

跟踪parse(下面2.2.4会对parse方法的每一步进行分析:)

MapperAnnotationBuilder源码
这个构建者类,以静态代码块来初始化这些属性:

static {
  SQL_ANNOTATION_TYPES.add(Select.class);
  SQL_ANNOTATION_TYPES.add(Insert.class);
  SQL_ANNOTATION_TYPES.add(Update.class);
  SQL_ANNOTATION_TYPES.add(Delete.class);

  SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
  SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
  SQL_PROVIDER_ANNOTAION_TYPES.add(UpdateProvider.class);
  SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
}

parse方法:

public void parse() {
  String resource = type.toString();//interface com.study.mapper.MyMapper

  if (!configuration.isResourceLoaded(resource)) {
    loadXmlResource();//先尝试通过找到接口对应的映射文件xml,并解析
    //将resource放到configuration 的loadedResources(HashSet <String>类型)
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      if (getSqlCommandType(method) == SqlCommandType.SELECT && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

2.2.4 parse中的核心方法:
① 首先执行的是loadXmlResource
这个方法:尝试接口名和接口包名路径来获取xml文件:
private void loadXmlResource() {
  // Spring may not know the real resource name so we check a flag
  // to prevent loading again a resource twice
  // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
他会先根据接口路径com.study.mapper. MyMapper去拼接一个xml路径出来,去获取这个mapper的xml
com/study/mapper/MyMapper.xml。也就是说,你配置成接口注解的方式,他也会先去查有没有xml方式的接口映射文件。
String xmlResource = type.getName().replace('.', '/') + ".xml";
    // #1347
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
//如果根据包名找不到,那就试着到主配置文件同级找
      // Search XML mapper that is not in the module but in the classpath.
      try {
//通过项目路径没有获取对应的xml,再试着到resources下获取接口的xml
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e2) {
        // ignore, resource is not required
      }
    }
//无论上面的那种方式,只要找到就用xml方式:
    if (inputStream != null) {
//最终都会构造XMLMapperBuilder
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}

结论:
如果找不到xml才会去找注解,所以在mybatis中xml的优先级是高于注解方式的。
因此如果之前用的是xml,后来改成注解了,要将相应的mapper的映射xml文件删除或者让注解所在的mapper接口路径与xml路径不一致,否则会导致在接口上的注解时效。
在这里插入图片描述这种结构或者Mymapper.xml在resources下:都会导致–>只要在主配置文件的mapper配置上了MyMapper的路径,无论主配置文件有没有配置,在Mymapper接口方法上有没有加了注解,都会导致xml有效,注解失效。

② configuration.addLoadedResource(resource);

对应的Configuration源码:
将上一步获取到的需要加载的mapper放到待加载集合loadedResources 中

protected final Set<String> loadedResources = new HashSet<>();
public void addLoadedResource(String resource) {
    loadedResources.add(resource);
  }
③ 查缓存

1、设置当前mapper的Namespace
assistant.setCurrentNamespace(type.getName());
type:interface com.study.mapper.MyMapper
2、获取CacheNamespace :

private void parseCache() {
//根据获取的这个Mapper,来获取对应的命名空间下的缓存
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }

3、获取CacheNamespaceRef 缓存:

private void parseCacheRef() {
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
    if (cacheDomainRef != null) {
      Class<?> refType = cacheDomainRef.value();
      String refName = cacheDomainRef.name();
      if (refType == void.class && refName.isEmpty()) {
        throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
      }
      if (refType != void.class && !refName.isEmpty()) {
        throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
      }
      String namespace = (refType != void.class) ? refType.getName() : refName;
      try {
        assistant.useCacheRef(namespace);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
      }
    }
  }

对上面缓存类CacheNamespace和CacheNamespaceRef进行一下总结:
mybatis的缓存:
(1)为每一个Mapper分配一个Cache缓存对象(使用节点配置或者 @CacheNamespace注解 );
(2)多个Mapper共用一个Cache缓存对象(使用节点配置或者本文所提到的@CacheNamespaceRef注解);

④解析方法和sql
④-1、mapper方法解析和结果集解析源码:
//获取指定type对应的mapper的所有方法。
for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
        //将桥连方法和default方法直接跳过
  //桥连方法https://blog.csdn.net/mhmyqn/article/details/47342577
          continue;
        }
        //判断这个方法对应的注解是不是select,如果是就解析查询结果集
        if (getSqlCommandType(method) == SqlCommandType.SELECT && method.getAnnotation(ResultMap.class) == null) {
          //下面单独详解
          parseResultMap(method);
        }
        try {
          //下面单独详解
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
//parseResultMap
private String parseResultMap(Method method) {
	//这里我的返回对象是 com.study.bean.MyBean
    Class<?> returnType = getReturnType(method);
    Arg[] args = method.getAnnotationsByType(Arg.class);
    Result[] results = method.getAnnotationsByType(Result.class);
    TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
    //resultMapId为 com.study.mapper.MyMapper.selectOne-int
    String resultMapId = generateResultMapName(method);
    //这里是将上面的结果集应用上,即赋值到Configuration的Result Maps collection
    applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
    return resultMapId;
  }

拼接处每个方法的resultMapId ,作为key放到Configuration的resultMaps(是Result Maps collection)中。

④-2、sql解析与处理(${}、#{})

第一步 parseStatement(method);

void parseStatement(Method method) {
	//获取方法的参数类型集合
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    /*获取注解类型(本次是SELECT)和对应的sql(select * from MyBean where id = #{id}),并将sql中的${}#{}处理掉,后面我会单独写一下*/
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
     .....
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;
      ......
      if (isSelect) {
        ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
        if (resultMapAnnotation != null) {
          resultMapId = String.join(",", resultMapAnnotation.value());
        } else {
          resultMapId = generateResultMapName(method);
        }
      }
      assistant.addMappedStatement(
         ......
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

第二步 从注解获取sql并解析(${})
getSqlSourceFromAnnotations:

private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      //获取注解的类型 这里我的是SELECT
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
        if (sqlProviderAnnotationType != null) {
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        /*sqlAnnotation.getClass().getMethod("value")
        对应的值就是sql: select * from MyBean where id = #{id}*/
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
      }
      return null;
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
  }
 // buildSqlSourceFromStrings
   private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    //sql转成字符串作为参数,创建sqlSource
    return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
  }

第三步 languageDriver.createSqlSource远程调用的是XMLLanguageDriver。

public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127   script:select * from MyBean where id = #{id}
      //configuration.getVariables()获取数据源信息(url,username,driver,password)
      //解析sql,将#{}和${}分别使用标记器进行处理替换成?  先替换的${},再替换#{} 下面第四步详解
      script = PropertyParser.parse(script, configuration.getVariables());

      TextSqlNode textSqlNode = new TextSqlNode(script);
      //下面第五步详解
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

第四步 PropertyParser.parse(script, configuration.getVariables());

public static String parse(String string, Properties variables) {
    //标记处理器 在这里会初始化多种标记处理器
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //如果sql中有${}标记,就进行处理解析
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

标记处理解析 GenericTokenParser

public String parse(String text) {//如果有${ ,就去${和}之间的内容返回。如果没有${ 直接将sql返回。
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token ${
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // openToken被转义了. 删除反斜杠再继续.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();//sql的openToken的最后一位索引
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

get到一个点:
里面使用的append(char[] str, int offset, int len)是一种高效的方法,以后工作中替代substring方法。记录一下,写一个简单的例子:

StringBuffer buff = new StringBuffer("qwer");
      // char array
      char[] str = new char[]
      {'a','s','d','f','g','h','j','k','l'};
      buff.append(str, 3, 5);
      //就是在buff的后面追加一个一些字符,字符从str数组中取,从索引为3开始取5个字符,追加到buff后面。
      //结果是qwerfghjk。

org.apache.ibatis.builder.SqlSourceBuilder#parse
第五步 动态拼接sql:

if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }

textSqlNode.isDynamic():还是调用前面的标记处理解析器 ,替换调${}为?

new RawSqlSource(configuration, script, parameterType);

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    //对#{}标记进行处理
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

SqlSourceBuilder.parse:

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //定义一个#{}标记处理器
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //调用前面的处理器
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这里的parse也是调用的标记处理解析器
到整理@select注解和对应的sql全部获取到了。
下次再研究如何获取执行器并进行执行的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值