Mybatis 源码:Mapper的解析工作

Posted 无虑的小猪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis 源码:Mapper的解析工作相关的知识,希望对你有一定的参考价值。

1、Mapper配置方式

1、package方式

 指定包路径:

<mappers> <package name="org.snails.mapper"/> </mappers>

2、resource方式

  指定mapper.xml文件的相对路径:

<mappers> <mapper resource="org/snails/mapper/SnailsMapper.xml"/> </mappers>

3、url方式

  指定mapper.xml文件的绝对路径:

<mappers> <mapper url="file:///opt/org/snails/mapper/SnailsMapper.xml"/> </mappers>

4、接口方式

  指定mapper接口:

<mappers> <mapper class="org.snails.inter.SnailsMapper"/> </mappers>

2、Mapper解析源码

  Mappers标签的解析,根据全局配置文件中不同的注册方式,有不同的扫描方式。最终都要做两件事,1、语句注册;2、接口注册。

  Mappers标签的解析,XMLConfigBuilder#mapperElement() 核心代码:

 

 // 映射器 mappers 标签解析
 private void mapperElement(XNode parent) throws Exception 
   if (parent != null) 
     // 处理mapper子节点
     for (XNode child : parent.getChildren()) 
       // package子节点
       if ("package".equals(child.getName())) 
         // 自动扫描包下所有映射器
         String mapperPackage = child.getStringAttribute("name");
         // 扫描指定的包,并向mapperRegistry注册mapper接口
         configuration.addMappers(mapperPackage);
        else 
         // 获取mapper节点的resource、url、class属性,三个属性互斥
         String resource = child.getStringAttribute("resource");
         String url = child.getStringAttribute("url");
         String mapperClass = child.getStringAttribute("class");
         // 如果mapper节点指定了resource或者url属性,则创建XmlMapperBuilder对象,并通过该对象解析resource或者url属性指定的mapper配置文件
         if (resource != null && url == null && mapperClass == null) 
           // 使用类路径
           ErrorContext.instance().resource(resource);
           // 创建XMLMapperBuilder对象,解析映射配置文件
           InputStream inputStream = Resources.getResourceAsStream(resource);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
           // Mapper解析器解析
           mapperParser.parse();
          else if (resource == null && url != null && mapperClass == null) 
           // 使用绝对url路径
           ErrorContext.instance().resource(url);
           InputStream inputStream = Resources.getUrlAsStream(url);
           // 创建XMLMapperBuilder对象,解析映射配置文件
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
           mapperParser.parse();
          else if (resource == null && url == null && mapperClass != null) 
           // 如果mapper节点指定了class属性,则向MapperRegistry注册该mapper接口
           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.");
         
       
     
   
 

 

  上述四种配置方式中,package方式与接口方式使用MapperAnnotationBuilder作为解析入口,;resource方式和url方式使用XMLMapperBuilder作为解析入口。

1、XMLMapperBuilder作为解析入口的解析

  XMLMapperBuilder继承自BaseBuilder抽象类,在上面已经提到主要用于解析Mapper映射器。

  XMLMapperBuilder#parse() 核心代码:

 // 解析SQL的Mapper映射文件
 public void parse() 
   // 判断是否已经加载过该映射文件
   if (!configuration.isResourceLoaded(resource)) 
     // 1、语句注册,具体的增删改查接口标签解析<insert> <update> <delete> <select>。一个标签一个MappedStatement对象。
     configurationElement(parser.evalNode("/mapper"));
     // 2、接口注册,把namespace(接口类型)和工厂类绑定起来,放到一个map。一个namespace 一个 MapperProxyFactory
     // 将resource添加到Configuration.loadedResources集合中保存,hashset类型的集合,其中记录了已经加载过的映射文件
     configuration.addLoadedResource(resource);
     // 绑定映射器到namespace
     bindMapperForNamespace();
   
 
   // 处理ConfigurationElement方法中解析失败的resultMap节点
   parsePendingResultMaps();
   // 处理ConfigurationElement方法中解析失败的cache-ref节点
   parsePendingCacheRefs();
   // 处理ConfigurationElement方法中解析失败的SQL语句节点
   parsePendingStatements();
 

主要完成两件事情:

  1、configurationElement():解析Mapper.xml所有的子标签,最终获得MappedStatement对象,并注册到配置类configuration中。

  2、bindMapperForNamespace():把namespace(接口类型)和工厂类MapperProxyFactory绑定起来,将Mapper接口与接口代理工厂映射关系设置在configuration中mapperRegistry属性对象的knownMappers缓存属性中。

1.1、configurationElement()

  解析Mapper.xml配置文件中的标签信息,比如namespace、cache、parameterMap、resultMap、sql和select|insert|update|delete等。

  XMLMapperBuilder#configurationElement(),核心代码:

 // 解析<Mapper标签>
 private void configurationElement(XNode context) 
   try 
     // 获取mapper节点的namespace属性
     String namespace = context.getStringAttribute("namespace");
     if (namespace == null || namespace.equals("")) 
       throw new BuilderException("Mapper\'s namespace cannot be empty");
     
     // 设置MapperBuilderAssistant的currentNamespace字段,记录当前命名空间
     builderAssistant.setCurrentNamespace(namespace);
     // 解析cache-ref节点
     cacheRefElement(context.evalNode("cache-ref"));
     // 解析cache节点
     cacheElement(context.evalNode("cache"));
     // 解析parameterMap节点
     parameterMapElement(context.evalNodes("/mapper/parameterMap"));
     // 解析resultMap节点
     resultMapElements(context.evalNodes("/mapper/resultMap"));
     // 解析sql节点
     sqlElement(context.evalNodes("/mapper/sql"));
     // 解析select、update、insert、delete等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);
   
 

1、解析resultMap节点

  resultMap的解析通过ResultMapResolver解析器中的assistant属性完成的。并将创建的ResultMap对象设置进配置类configuration的resultMaps属性中。

  MapperBuilderAssistant#resultMapElement() 核心代码段:

 // 创建 resultMap解析器
 ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
 // 创建ResultMap对象,并添加到resultMap集合中,该集合是StrictMap类型
 return resultMapResolver.resolve();

  MapperBuilderAssistant#addResultMap() 核心代码段:

 // 创建ResultMap对象,并添加到configuration.resultMaps集合中保存
 ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
       .discriminator(discriminator)
       .build();
 // 将resultMap添加进配置类configuration的resultMap属性中
 configuration.addResultMap(resultMap);

2、生成mappedStatement对象并添加至configuration中,解析select|insert|update|delete节点

  XMLMapperBuilder#buildStatementFromContext() 的核心代码:

 // 构建语句
 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) 
   for (XNode context : list) 
     // 构建所有语句,一个mapper下可以有很多select
     // 语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder
     final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
     try 
       // 核心XMLStatementBuilder.parseStatementNode
       statementParser.parseStatementNode();
      catch (IncompleteElementException e) 
       // 如果出现SQL语句不完整,把它记下来,塞到configuration去
       configuration.addIncompleteStatement(statementParser);
     
   
 

  在buildStatementFromContext()方法中,创建了用来解析增删改查标签的XMLStatementBuilder,并且把创建的MappedStatement添加到mappedStatements中。Mybatis通过MapperBuilderAssistant将MappedStatement对象设置到configuration配置类中。

  MapperBuilderAssistant#addMappedStatement() 核心代码段:

 // 创建MappedStatement对象
 MappedStatement statement = statementBuilder.build();
 // 将MappedStatement对象添加进配置类configuration的mappedStatements属性中
 configuration.addMappedStatement(statement);

注意:在addMappedStatement方法中,有个参数sqlCommandType,代表sql命令的类型,Mybtais通过sqlCommandType完成对SQL语句增删改查的判断,Mybatis解析Mapper.xml中的SQL的标签来获取sqlCommandType。

  XMLStatementBuilder#parseStatementNode() 核心代码段:

// 根据SQL节点的名称决定其SqlCommandType
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

  MappedStatement对象中的很多属性都是在XMLStatementBuilder#parseStatementNode()方法中创建的。

1.2、bindMapperForNamespace()

  构建映射的SQL语句,XMLMapperBuilder#bindMapperForNamespace() 核心代码:

 private void bindMapperForNamespace() 
   // 获取映射配置文件的命名空间
   String namespace = builderAssistant.getCurrentNamespace();
   if (namespace != null) 
     Class<?> boundType = null;
     try 
       // 解析命名空间对应的类型
       boundType = Resources.classForName(namespace);
      catch (ClassNotFoundException e) 
       //ignore, bound type is not required
     
     if (boundType != null) 
       // 是否已经加载了boundType接口
       if (!configuration.hasMapper(boundType)) 
         // 追加namespace前缀,并添加到loadedResources集合中保存
         configuration.addLoadedResource("namespace:" + namespace);
         // 调用MapperRegistry.addMapper方法,注册boundType接口
         configuration.addMapper(boundType);
       
     
   
 

  主要是调用了configuration#addMapper()。addMapper()方法中,把接口类型注册到MapperRegistry中:实际上是为接口创建一个对应的MapperProxyFactory(用于为这个type提供工厂类,创建MapperProxy)。

添加接口Class对象与MapperProxyFactory的映射,同时解析配置文件中SQL语句。

  MapperRegistry#addMapper() 核心代码:

 // 记录了Mapper接口与对应MapperProxyFactory之间的关系
 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
 
 // 添加Mapper映射器
 public <T> void addMapper(Class<T> type) 
   // 检测type是否为接口
   if (type.isInterface()) 
     // knownMappers集合中,抛异常
     if (hasMapper(type)) 
       throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
     
     // 加载完成标识
     boolean loadCompleted = false;
     try 
       // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合
        // Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系
       knownMappers.put(type, new MapperProxyFactory<>(type));
       // 创建MapperAnnotationBuilder对象用于解析
       MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
       // 根据接口,开始解析所有方法上的注解,例如 @Select、@Insert...等注解
       parser.parse();
       loadCompleted = true;
      finally 
       // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除
       if (!loadCompleted) 
         knownMappers.remove(type);
       
     
   
 

  Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合,MapperProxyFactory主要用于Mapper接口代理对象的创建,后续会详细介绍到。

  解析SQL并获取MappedStatement对象并设置进configuration配置类中,MapperAnnotationBuilder#parse() 核心代码:

 public void parse() 
   String resource = type.toString();
   // 检测是否已经加载过该接口
   if (!configuration.isResourceLoaded(resource)) 
     // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件
     loadXmlResource();
     configuration.addLoadedResource(resource);
     assistant.setCurrentNamespace(type.getName());
     // 解析@CacheNamespace注解
     parseCache();
     // 解析@CacheNamespaceRef注解
     parseCacheRef();
     Method[] methods = type.getMethods();
     for (Method method : methods) 
       try 
         if (!method.isBridge()) 
           // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象,添加进configuration中的mappedStatements属性中
           parseStatement(method);
         
        catch (IncompleteElementException e) 
         // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存
         configuration.addIncompleteMethod(new MethodResolver(this, method));
       
     
   
   parsePendingMethods();
 

  mappedStatement通过MapperBuilderAssistant对象完成MappedStatement对象添加进配置类configuration中的mappedStatements属性。

  增加映射语句,MapperBuilderAssistant#addMappedStatement() 核心代码段:

// 创建MappedStatement对象
MappedStatement statement = statementBuilder.build();
// 将MappedStatement对象添加进配置类configuration的mappedStatements属性中
configuration.addMappedStatement(statement);

  注意:在MapperBuilderAssistant#addMappedStatement()中创建MappedStatement的内部类Builder时,默认的将statementType设置为PREPARED,resultSetType设置为DEFAULT,该属性会在后续创建具体的StatementHandler对象是用到。

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)

 

2、MapperAnnotationBuilder作为解析入口的解析

  其实XMLMapperBuilder作为解析入口与MapperAnnotationBuilder作为解析入口核心流程是一样的,最终都是将MappedStatement对象添加进配置类configuration中的mappedStatements属性中。只不过代码执行的顺序不同。

  MapperRegistry#addMapper() 核心代码:

 // 记录了Mapper接口与对应MapperProxyFactory之间的关系
 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
 
 // 添加Mapper映射器
 public <T> void addMapper(Class<T> type) 
   // 检测type是否为接口
   if (type.isInterface()) 
     // knownMappers集合中,抛异常
     if (hasMapper(type)) 
       throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
     
     // 加载完成表示
     boolean loadCompleted = false;
     try 
       // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合
       knownMappers.put(type, new MapperProxyFactory<>(type));
       // 创建MapperAnnotationBuilder对象用于解析
       MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
       // 解析
       parser.parse();
       loadCompleted = true;
      finally 
       // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除
       if (!loadCompleted) 
         knownMappers.remove(type);
       
     
   
 

  优先将接口的Class类型与映射代理工厂MapperProxyFactory的映射关系设置在MapperRegistry的knownMappers属性中,映射器代理工厂MapperProxyFactory在后续会详细介绍到,此处只需知道MapperProxyFactory使用来生成Mapper接口代理对象的。

  通过MapperAnnotationBuilder完成解析,创建MappedStatement对象同时设置进configuration对象中。

  MapperAnnotationBuilder#parse() 核心代码:

 // 解析mapper
 public void parse() 
   String resource = type.toString();
   // 检测是否已经加载过该接口
   if (!configuration.isResourceLoaded(resource)) 
     // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件
     loadXmlResource();
     configuration.addLoadedResource(resource);
     assistant.setCurrentNamespace(type.getName());
     // 解析@CacheNamespace注解
     parseCache();
     // 解析@CacheNamespaceRef注解
     parseCacheRef();
     Method[] methods = type.getMethods();
     for (Method method : methods) 
       try 
         // issue #237
         if (!method.isBridge()) 
           // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象
           parseStatement(method);
         
        catch (IncompleteElementException e) 
         // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存
         configuration.addIncompleteMethod(new MethodResolver(this, method));
       
     
   
   parsePendingMethods();
 

  上述代码与xml配置文件中的解析方式相同,只不过是此处在loadXmlResource()中不会被过滤掉。

// 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件
loadXmlResource()

  loadXmlResource()最终会调用XMLMapperBuilder#parse()方法,加载Mapper.xml映射文件中的SQL映射语句,解析为MappedStatement对象并注册到配置类configuration中,然后将Mapper接口与映射代理工程MapperProxyFactory做绑定,将Mapper接口中被@Select等被注解修饰的方法也解析为MappedStatement对象并完成注册。

3、总结

对Mappers标签的解析工作,主要完成如下两件事情:

  1、优先解析*Mapper.xml配置文件,将配置文件信息解析成MappedStatement对象。注册到configuration对象中的mappedStatements属性。

  2、若Mapper接口未注册到配置类configuration中,把namespace(接口类型)和工厂类MapperProxyFactory绑定起来,将映射关系设置在configuration中mapperRegistry属性对象的knownMappers缓存属性中。同时将带有@Select等注解的方法解析成MappedStatement对象,注册到到configuration对象中的mappedStatements属性。

 

以上是关于Mybatis 源码:Mapper的解析工作的主要内容,如果未能解决你的问题,请参考以下文章

mybatis源码-解析配置文件之配置文件Mapper解析

Mybatis源码解析MyBatis解析Mapper.xml(以动态sql为例)

mybatis通用mapper源码解析

Mybatis之Mapper调用源码分析

mybatis源码解析 - mapper代理对象的生成

mybatis源码解析 - mapper代理对象的生成