【Mybatis源码阅读】初始化过程

mybatis专栏 https://blog.csdn.net/worn_xiao/category_6530299.html?spm=1001.2014.3001.5482

第一步 先看一个初始化代码

public static void setup() throws Exception {
    createBlogDataSource();
    final String resource = "org/apache/ibatis/builder/MapperConfig.xml";
    final Reader reader = Resources.getResourceAsReader(resource);
    sqlMapper = new SqlSessionFactoryBuilder().build(reader);
  }

      如上图所示是构建SqlSessionFactory的全过程,那么具体是怎么执行的呢,首先我们大致可以看出代码中的意思无非就是读取配置文件生成SqlSessionFactory工厂,这里先给出一个比较全的配置文件格式代码。

<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

  <properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/>

  <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="false"/>
    <setting name="multipleResultSetsEnabled" value="true"/>
    <setting name="useColumnLabel" value="true"/>
    <setting name="useGeneratedKeys" value="false"/>
    <setting name="defaultExecutorType" value="SIMPLE"/>
    <setting name="defaultStatementTimeout" value="25"/>
  </settings>

  <typeAliases>
    <typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author"/>
    <typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog"/>
    <typeAlias alias="Comment" type="org.apache.ibatis.domain.blog.Comment"/>
    <typeAlias alias="Post" type="org.apache.ibatis.domain.blog.Post"/>
    <typeAlias alias="Section" type="org.apache.ibatis.domain.blog.Section"/>
    <typeAlias alias="Tag" type="org.apache.ibatis.domain.blog.Tag"/>
  </typeAliases>

  <typeHandlers>
    <typeHandler javaType="String" jdbcType="VARCHAR" handler="org.apache.ibatis.builder.CustomStringTypeHandler"/>
  </typeHandlers>

  <objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory">
    <property name="objectFactoryProperty" value="100"/>
  </objectFactory>

  <plugins>
    <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
      <property name="pluginProperty" value="100"/>
    </plugin>
  </plugins>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC">
        <property name="" value=""/>
      </transactionManager>
      <dataSource type="UNPOOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="org/apache/ibatis/builder/AuthorMapper.xml"/>
    <mapper resource="org/apache/ibatis/builder/BlogMapper.xml"/>
    <mapper resource="org/apache/ibatis/builder/CachedAuthorMapper.xml"/>
    <mapper resource="org/apache/ibatis/builder/PostMapper.xml"/>
    <mapper resource="org/apache/ibatis/builder/NestedBlogMapper.xml"/>
  </mappers>

</configuration>

 如下图所示是xml文件加载成为字符流的过程

首先是Resource组合了ClassLoaderWrapper通过获取各种类加载器,把配置文件加载成为字节流,然后再通过流转换,把字节流转换成为字符流返回,配置文件的流文件

具体过程

public static Reader getResourceAsReader(String resource) throws IOException {
  Reader reader;
  if (charset == null) {
    reader = new InputStreamReader(getResourceAsStream(resource));
  } else {
    reader = new InputStreamReader(getResourceAsStream(resource), charset);
  }
  return reader;
}


//Resource中获取Reader的核心代码
public class Resources {
  private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
   //可以看到Resource中组合了classloaderWrapper的代码
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
  return new ClassLoader[]{
      classLoader,
      defaultClassLoader,
      Thread.currentThread().getContextClassLoader(),
      getClass().getClassLoader(),
      systemClassLoader};
}

      可以看到这是ClassLoaderWrapper类获取各种类加载器的代码,还可以传入参数指定类的加载器。

       接下来可以看到就是jdk读取资源路径下的资源文件转化成为字节流的过程了。再回到入库出就是把字节流转化成为字符流的初始化过程了。这就是整个流程件的加载过程。接下来我们分析一下SqlSessionFactory的构建过程。有了字符流文件,那么自然我们想到的就是吧字符流文件转化成对象做成一个配置类,提供后面自行调用。好的那我们接下来梳理一下流程

如上图所示,大致的流程就是解析字符流,转化为Configuration然后构建成一个工厂类

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
  for (ClassLoader cl : classLoader) {
    if (null != cl) {
      InputStream returnValue = cl.getResourceAsStream(resource);
      if (null == returnValue) {
        returnValue = cl.getResourceAsStream("/" + resource);
      }
      if (null != returnValue) {
        return returnValue;
      }
    }
  }
  return null;
}

如上所示一般会给三个参数一个是流文件

<properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/>
<environments default="development">
    <environment id="development">
      <transactionManager type="JDBC">
        <property name="" value=""/>
      </transactionManager>
      <dataSource type="UNPOOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>

所以有另外两个参数,主要是方便我们自定是数据源与数据源连接地址,当然在源码中也是做了控制的,如果有自定义的就会装配自定义的。
 

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      reader.close();
    } catch (IOException e) {

      // Intentionally ignore. Prefer previous error.

    }
  }
}

2接下来我们看XMLConfigBuilder中的核心代码

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {

  super(new Configuration());

  ErrorContext.instance().resource("SQL Mapper Configuration");

  this.configuration.setVariables(props);

  this.parsed = false;

  this.environment = environment;

  this.parser = parser;

}



public Configuration parse() {

  if (parsed) {

    throw new BuilderException("Each XMLConfigBuilder can only be used once.");

  }

  parsed = true;

  parseConfiguration(parser.evalNode("/configuration"));

  return configuration;

}

从上面的代码很容易看出实际上是通过XPathParser中的parser把xml的字符流文件解析成Configuration类的过程那么XPathParser是如何工作的呢。

3 XPathParser核心代码如下。

public XPathParser(Reader reader) {

  commonConstructor(false, null, null);

  this.document = createDocument(new InputSource(reader));

}


private Document createDocument(InputSource inputSource) { 

  try {

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

    factory.setValidating(validation);

    factory.setNamespaceAware(false);

    factory.setIgnoringComments(true);

    factory.setIgnoringElementContentWhitespace(false);

    factory.setCoalescing(false);

    factory.setExpandEntityReferences(true);

    DocumentBuilder builder = factory.newDocumentBuilder();

    builder.setEntityResolver(entityResolver);

    builder.setErrorHandler(new ErrorHandler() {

      @Override

      public void error(SAXParseException exception) throws SAXException {

        throw exception;

      }



      @Override

      public void fatalError(SAXParseException exception) throws SAXException {

        throw exception;

      }

      @Override

      public void warning(SAXParseException exception) throws SAXException {

      }

    });

    return builder.parse(inputSource);

  } catch (Exception e) {

    throw new BuilderException("Error creating document instance.  Cause: " + e, e);

  }

}

把流作为输入源通过jdk的DocumentBuilderFactory解析输入源生成Document的文档对象。private final Document document;

实际上不难看出还可以把字节流和字符串作为输入源进行解析。通过条用parser.evalNode("/configuration") 把xml的文档对象解析返回Configuration类实际上是调用jdk的Xpath类按路径进行解析的。
3 XMLConfigBuilder把xml转化成Configuration的核心代码
private void parseConfiguration(XNode root) {

  try {

    //issue #117 read properties first

    propertiesElement(root.evalNode("properties"));

    Properties settings = settingsAsProperties(root.evalNode("settings"));

    loadCustomVfs(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);

  }

}
如上代码可以看到就是把配置文件中的各个xml节点转化成Configuration的过程。那么我们来看四个properties, environments, mappers,plugins 这四个配置项直接指导了我们的配置。分别是我们的数据源配置文件,mapper.xml配置文件,还有插件的配置文件。

正如我们前面代码所预料的那样,在我们配置了自定义的数据源与配置文件的时候,就不走默认的配置.接下来是插件的代码

如上图所示遍历上下文的插件注册到Configuration当中。接下来我们看看我们比较关心的mapper.xml文件的读取过程

如上图所示通过读取pachage包中的映射,通过XMLMapperBuilder读取xml文件中的内容放入Configuration中就可以了,但是我们也可以看到这里有三个判断一个是有resource路径,一个是有url,另一个是有class.那么mapper这里到底怎么加载的呢。实际上你可以看到url,和resource的都是通过xmlMapperBuilder进行parse的,然而mapperClass则是通过mapperInterface进行加载mapper文件的,那么问题就在这里了,有什么区别呢.接下来我们来看看XmlMapperBuider做了什么

一目了然就是解析了mapper文件然后把想入的节点映射再组装到一个类中。最后把相应的类组装进Configuration中。给到DefaultSqlSessionFactory进行保存。解析/mapper就是我们.xml文件的根节点。

 再往里面走就是解析各个mapper.xml文件里面的片段了,这里包括sql,parameterMap,cahe,chache-refd,dcl语句等等的,那么我们可以来看看做了什么

 cache-ref实际是往configutation中添加cacheRef.其他的也是类似的都是往Configuration中注册相应的字段映射。

或者类是select|insert等注解对应对象的构造器都是生成特定的对象,然后在那个在公共的配置类中生成映射表

如上代码片段是Configuration中的类的映射信息,xml文件中类似insert|delete等的每个操作片段映射成一个mappedStatements包括parameterMap和resultMap等等的 。

好了说完了url和resource映射接下来改说一下class映射了吧。这里是重点,我先给大家画一下,为什么呢,因为在不久的将来,随着springboot的新起,你会发现xml的配置方式,将会被注解彻底的取代,那么Mybaties的注解,自然就来了,让我们看一下通过class获取mapper的接口干了什么吧

如上代码所示,大家可以看到一个是给出包名的package,一个是单纯的一个类接口,那么好无疑问给包的干了什么呢,一眼就可以

看出来就是获取到包路径然后addMappers

 再来看addMappers的源码,可以看到就是获取包西面接口的整个集合,遍历添加到mapper中,那么Configuration.addMapper()都做了哪些事情呢,好吧让我们继续看源代码

其实你一看就可以发现无非就是把mapper的类型做为key,映射一个mapper的代理,这个代理具体有什么用呢,请大家关注我一片Mybaties的博客,讲解Mybatis中注解式的mapper的执行过程。我们可以看到MapperAnnotationBuilder类那么我们自然关注的就是parse()方法了。

可以看到先是加载xml,然后在parseStatement(method),问题来了,集成可以通过类型对应的xml加载配置信息,那么不就完了么,parseStatement还要来干什么呢,让我们一步步的走进去


  void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {//要以注解的形式加载的前提是 mapper的方法上要有相应的注解 所以如果用了xml文件自然mapper上也不会有注解 sqlSource自然也就为null 也就不会装配注解
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = "id";
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(//所以 一个mapperStatementId 实际是一个mapper的具体方法
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

这段代码似乎有点长可以看到sqlSource那里有个判断,那么我们看看SqlSource做了什么

 实际上这个方法就干了一件事,解析mapper方法上面的注解,可以看到为什么平时我们可以在Mapper接口上写@Select或者MapperProvider的注解,实际上这里就是根据注解,获取里面的sql语句生成SQL数据源。再回到原来的代码与问题分析,为什么Mybaties的配置通常人们要么配置XML要么用注解呢,这里就说明了 如果用注解那么SqlSource 就不为空就会执行注解,如果用xml注解非空就会通过注解解析各种parameterMap,或者resultMap放入到放入到Configuration中,所以不会重复。好的,整个Mybaties的初始化过程我已经讲解完成了。实际上我们通过Spring配置package的包路径以后,为什么能够生效。实际上你应该明白了,无非就是扫描包下面所有的class信息,加载xml或者mapper上的注解,把相关的配置,集成映射到Configuration中 请看下回分解!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值