【Mybatis源码探索】 --- Mybatis配置文件解析核心源码解读


1 源码阅读入口

入口为读取配置文件mybatis-config.xml,并拿着读取到的配置文件流通过SqlSessionFactoryBuilder去创建sqlSessionFactory,可参考上篇文章《【Mybatis源码探索】 — 开篇 • 搭建一个最简单的Mybatis框架》。代码如下:

 @Before
 public void init() throws IOException {
     String resource = "mybatis-config.xml";
     InputStream inputStream = Resources.getResourceAsStream(resource);
     // 1.读取mybatis配置文件创SqlSessionFactory
     sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
     inputStream.close();
 }

2 配置文件解析核心源码解读

2.1 SqlSessionFactoryBuilder — 大骨架

SqlSessionFactoryBuilder其实相对比较简单,如若build(…)方法的参数为InputStream的话,其所走的源码如下:
所在类:SqlSessionFactoryBuilder

//拿着读取到的InputStream调用下面的build(...)方法创建SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
    	//获取解析器 --- 这里注意一下:由上面的build(InputStream inputStream)可知environment 和properties都为null
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        //build方法在下面,可以看到它的参数是Configuration
        //也就是说parser.parse()这个方法就是获取Configuration对象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}
public SqlSessionFactory build(Configuration config) {
  //拿着获取的Configuration对象new一个SqlSessionFactory对象
  return new DefaultSqlSessionFactory(config);
}

从上面抽离出来的源码其实可以很清楚地看到SqlSessionFactoryBuilder主要的工作是:
(1)将读取到的mybatis-config.xml流文件解析成一个Configuration对象
(2)根据获取到的Configuration对象创建SqlSessionFactory对象


这里先不对Configuration对象和SqlSessionFactory对象进行具体的解析。


2.2 XMLConfigBuilder 和 parser.parse() — 模板模式

2.2.1 XMLConfigBuilder构造函数及BaseBuild的引出

首先来看一下new XMLConfigBuilder(inputStream, environment, properties)这句话到底做了什么,其源码如下:
所在类:XMLConfigBuilder

//该方法只是一个简单的有参构造函数,并且它实际上会调用到下面第二个方法来对属性进行赋值
 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  //将InputStream封装到XPathParser里,并调用下面的private方法
   this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
 }
//真正的对属性赋值
 private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
   //这里其实应该可以想到,2.1中的parser.parse()方法的目的就是要获取一个Configuration对象,而且该方法是一个空参方法,
   //因此其实它获取到的Configuration对象就是这里new出来的Configuration对象
   //并且这里需要提示的一点是:Configuration对象其实是一个单例对象,它只会在这里初始化一次,
   //其他用到Configuration对象的地方如DefaultSqlSessionFactory,都是通过构造函数等方式将此处new出来的对象传递进去的
   super(new Configuration());
   ErrorContext.instance().resource("SQL Mapper Configuration");
   this.configuration.setVariables(props);
   this.parsed = false;
   this.environment = environment;
   this.parser = parser;
 }

除了我在上面源码中注释的内容外,不知道大家会不会有这样的疑惑 :

看2.1中的代码可以知道,它先new了一个XMLConfigBuilder对象,然后调用该对象的parse()方法将mybatis-config.xml流文件解析成一个Configuration对象,其实Configuration对象完全可以作为XMLConfigBuilder对象的一个属性啊,为啥它还用了个super(new Configuration())呢?

其实答案很简单,要我设计我应该也会做类似的设计,它就是用到了模版方法。XMLConfigBuilder的父类为BaseBuild,它里面真正封装了Configuration属性,并封装了一些解析文件的通用方法。BaseBuild的类继承关系图如下:
在这里插入图片描述
通过该图应该可以很容易的猜到,BaseBuild的这几个子类其实都是在解析配置文件,并将解析结果封装到BaseBuild的属性中接下来会对其进行验证


2.2.2 parser.parse()方法 — mybatis-config.xml配置文件解析模版

接着来看一下parser.parse()方法,其源码如下:
所在类:XMLConfigBuilder

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  //parser.evalNode("/configuration")的实际值就是mybatis-config.xml中<configuration>节点内的xml
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
//真正解析mybatis-config.xml中配置的内容
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first 
    propertiesElement(root.evalNode("properties")); //解析properties标签
    Properties settings = settingsAsProperties(root.evalNode("settings"));//解析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
    //解析environments标签 --- >数据源在environments标签里
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    //解析mappers标签
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

从上面的源码可以看出:parse()方法就是拿着mybatis-config.xml中< configuration >节点内的xml内容,去调用parseConfiguration(XNode root) 方法去真正解析每一个xml标签里的内容。

联系2.1应该可以猜到,parseConfiguration这些方法肯定是将解析的内容,set到Configuration对象里 —>接下来会对其进行验证。


2.3 配置文件解析具体流程 — 仅介绍environments和mappers标签

由2.2.2中的源码可以看出其实mybatis-config.xml可配的标签还是挺多的,本篇文章只对environments标签和mappers的解析流程做一下具体的分析。


2.3.1 解析environments标签源码探究 — 数据源的获取

environmentsElement(…)方法的具体源码如下:
所在类:XMLConfigBuilder


  private void environmentsElement(XNode context) throws Exception {
  	//这里的context就是mybatis-config.xml中<environments>节点内的xml
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      //child就是<environments>标签下的每一个<environment>标签的内容
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          //拿着<transactionManager>标签的内容去获取事务工厂
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //拿着<dataSource>标签里的内容去获取数据源工厂
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          //获取到数据源
          DataSource dataSource = dsFactory.getDataSource();
          //利用事务工厂+数据源构建Environment对象
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          //这里其实就可以验证前面所说的内容了 --- 即将解析到的mybatis-config.xml内容set到Configuration对象里
          //将Environment对象set到Configuration对象里
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

简单来看一下DataSourceFactory的构建过程(其实TransactionFactory的构建过程和它基本一致):
所在类:XMLConfigBuilder

  private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      //获取到配置的数据源信息
      Properties props = context.getChildrenAsProperties();
      //拿着获取的type通过反射获取到数据源工厂
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      //将数据源信息set到数据源工厂里
      factory.setProperties(props);
      return factory; //返回数据源工厂
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

2.3.2 解析mappers标签源码探究 — 获取sql相应内容及当前mapper.xml对应的Mapper接口

2.3.2.1 mapperElement(…)方法 — 对四种mapper配置方式解析的模版

接着来看一下 mapperElement(…)方法,其源码如下:
所在类:XMLConfigBuilder

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
      	//如果使用package标签进行扫描mapper.xml
        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");
          //如果使用resource标签进行扫描mapper.xml
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            //获取到mapper.xml的流文件
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //拿着mapper.xml的流文件、Configuration对象、mapper.xml的路径和Configuration对象中的sql碎片对象
            //去构建XMLMapperBuilder 对象
            //这里的XMLMapperBuilder在2.2.1中的图片里就已经见到过了,它就是专门用来解析mapper的Builder类
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            //解析mapper.xml文件 --- 可以看到这里其实也是一个无参方法
            mapperParser.parse();
            //如果使用url标签进行扫描mapper.xml
          } 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();
            //如果指定直接使用class方式进行扫描
          } else if (resource == null && url == null && mapperClass != null) {
            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.");
          }
        }
      }
    }
  }

可以看出上面的源码就是对四种mapper配置方式解析的模版,本文只就resource方式进行分析(因为上篇文章我配置的是resource☺☺☺),其实一种明白了,其他三种也就可以很轻松的理解了。


2.3.2.2 mapper标签对应的mapper.xml文件的具体解析流程 — 以resource方式为例

mapperParser.parse()方法的源码如下

所在类:XMLMapperBuilder

 public void parse() {
   if (!configuration.isResourceLoaded(resource)) {
   	//解析mapper.xml--- 注意这里的parser.evalNode("/mapper")其实就是mapper.xml的具体内容了
     configurationElement(parser.evalNode("/mapper"));
     configuration.addLoadedResource(resource); //标识该标签已经解析完成
     bindMapperForNamespace(); //构建当前mapper.xml对应的Mapper接口
   }
   //下面的代码不细究了
   parsePendingResultMaps();
   parsePendingCacheRefs();
   parsePendingStatements();
 }

可以看到这里的解析其实分了五步:分别为解析当前mapper.xml、解析获得当前mapper.xml对应的Mapper接口、解析待处理的结果、解析待处理的缓存引用、解析待处理的语句,本文接下来主要分析一下前两步。


2.3.2.2.1 configurationElement方法 — 解析当前mapper.xml文件,获取sql相应内容

(1)XMLMapperBuilder中解析mapper.xml的核心源码主要如下:

所在类:XMLMapperBuilder

 //这里定义了解析mapper文件的模版
 private void configurationElement(XNode context) {
   try {
   	 //获取到namespace ,如:com.nrsc.mybatis.mapper.TUserMapper
     String namespace = context.getStringAttribute("namespace");
     if (namespace == null || namespace.equals("")) {
       throw new BuilderException("Mapper's namespace cannot be empty");
     }
     //这里留意一下builderAssistant,它也在2.2.1中的图片里就已经见过了,它其实是用来帮助解析mapper.xml文件的
     builderAssistant.setCurrentNamespace(namespace);
     cacheRefElement(context.evalNode("cache-ref"));//解析<cache-ref>标签
     cacheElement(context.evalNode("cache"));//解析缓存标签
     parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析parameterMap标签
     resultMapElements(context.evalNodes("/mapper/resultMap")); //解析resultMap
     sqlElement(context.evalNodes("/mapper/sql")); //解析sql片段
     buildStatementFromContext(context.evalNodes("select|insert|update|delete")); //真正解析sql语句
   } catch (Exception e) {
     throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
   }
 }
 //这里的list其实就是当前mapper.xml文件里的一个个select、insert、update、delete标签的内容
 private void buildStatementFromContext(List<XNode> list) {
   if (configuration.getDatabaseId() != null) {
     buildStatementFromContext(list, configuration.getDatabaseId());
   }
   //解析select、insert、update、delete标签
   buildStatementFromContext(list, null);
 }
 //循环遍历select、insert、update、delete标签组成的list集合,解析每一个标签
 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
   for (XNode context : list) {
   	//构建XMLStatementBuilder对象,用于解析select、insert、update、delete标签 
   	//这里注意一下:XMLStatementBuilder也在2.2.1中的图片里就已经见过了,它是真正解析sql语句的一个builder
     final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
     try {
  	   //真正去解析select、insert、update、delete标签
       statementParser.parseStatementNode();
     } catch (IncompleteElementException e) {
       configuration.addIncompleteStatement(statementParser);
     }
   }
 }

由上面的源码可以知道XMLMapperBuilder是用来解析当前mapper.xml的,但是select、insert、update、delete标签是在XMLStatementBuilder类里进行解析的。


(2)跟一下 XMLStatementBuilder中的parseStatementNode方法,其核心源码最主要如下:

所在类:XMLStatementBuilder

public void parseStatementNode() {
	//此处省略n行,主要逻辑就是解析当前mapper.xml中的select、insert、update、delete标签,并将解析内容封装为一个个的对象
	
	//拿着解析到的内容利用builderAssistant类将解析结果进行封装
	//这里应该可以想到,还会将封装结果放到Configuration对象里
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

(3)再跟一下MapperBuilderAssistant的builderAssistant方法

builderAssistant其实有两个作用:
(1)将select、 insert、update、delete标签解析结果进行封装
(2)将封装后的对象set到Configuration对象里
代码不具体跟了,将断点跟踪过程用如下图进行展示一下:
在这里插入图片描述
这里要注意:

(1)select、 insert、update、delete一个个的标签被解析后封装的对象为MappedStatement对象
(2)这些MappedStatement对象都会被加到Configuration对象的一个map里 — mappedStatements,该map的key其实就是namespace.方法名或者说类的全额限定名.方法名。如: com.nrsc.mybatis.mapper.TUserMapper.selectByPrimaryKey
(3)其实每一个MappedStatement对象里也都包含了Configuration对象 ,这一点也要格外注意。


2.3.2.2.2 bindMapperForNamespace方法 — 解析获得当前mapper.xml对应的Mapper接口

(1)bindMapperForNamespace方法的源码如下:

所在类:XMLMapperBuilder

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {//获取当前mapper.xml绑定的Mapper接口的类型
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        configuration.addLoadedResource("namespace:" + namespace);
        configuration.addMapper(boundType); //解析当前mapper.xml对应的Mapper接口,并将其放入Configuration对象
      }
    }
  }
}

(2)跟一下configuration.addMapper(boundType)方法,源码如下:

所在类:Configuration

public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}

注意: 这里的mapperRegistry其实是Configuration对象的一个属性,它在Configuration对象的初始化语句如下,

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

由此可知,其实MapperRegistry这个属性里也包含了当前的Configuration对象。
其实我觉得在这里一定要理清一个关系,就是Configuration对象里有MapperRegistry属性,MapperRegistry也有Configuration属性 --- 而且Configuration对象里的MapperRegistry是加了final修饰符的,Configuration对象在spring容器里是单例的。

这样的话,只要MapperRegistry对象的属性发生变化,通过Configuration对象都能获取到最新的变化结果。


(3) 接着跟一下mapperRegistry.addMapper(type)方法的源码:
所在类:MapperRegistry

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 {
    	//knownMappers是MapperRegistry对象里的一个Map对象,用来保存所有的mapper.xml对应的Mapper接口的封装类
	//这句话,其实就把当前mapper.xml对应的Mapper接口的封装类加入到Configuration对象了
      knownMappers.put(type, new MapperProxyFactory<>(type));
      //下面的代码不细究了
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

在这里看一下knownMappers属性,它在MapperRegistry对象里的声明源码如下:
所在类:MapperRegistry

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

由此可知,其实knownMappers的key并不是String,而是一个class类型,其实在Mybatis源码中,它就是一个个Mapper接口的class类型。而Value是MapperProxyFactory对象 —> 即mapper代理工厂对像 —> 该对象非常重要!!!
其源码如下:
所在类:MapperProxyFactory

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
  //唯一的构造函数,需要传进来一个class类型 ---> 即需要将Mapper接口的class类型传递进来
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }
 //拿着接口类型创建代理对象
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
 //拿着接口类型创建代理对象
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

该对象会在我接下来的几篇文章里都会提到。

3 总结

本篇文章主要介绍了Mybatis解析mybatis-config.xml文件的主要流程。着重分析了解析environments标签和mappers标签获取数据源、sql、Mapper接口的封装类 — MapperProxyFactory的流程。现将其总结如下:
在这里插入图片描述

发布了189 篇原创文章 · 获赞 187 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/103833623