从源码角度分析 Mybatis 的 SqlSession 以及 二级缓存的创建过程

291 阅读7分钟

前言

本文将从源码角度对 SqlSession 对象的创建过程以及二级缓存的实现进行讲解。

构建 SqlSession

Mybatis 构建阶段的调用入口类是 SqlSessionFactoryBuilder,在得到初始化的 configuration对象后用其构建 SqlSessionFactory,而 SqlSessionFactory 是生产 SqlSession 对象的工厂,SqlSession 是 Mybatis 执行阶段的关键入口类。

构建入口 SqlSessionFactoryBuilder

下面从 SqlSessionFactoryBuilder 类源码开始说起。

SqlSessionFactoryBuilder 中有很多重载的 build()方法,但核心方法有两种:

  • SqlSessionFactory#build(InputStream inputStream, String environment, Properties properties)
  • SqlSessionFactory#build(Configuration config)

(1) 我们先看第一个build(),参数 inputStream 是配置文件的文件流,environmentproperties 是可选的参数。build 方法首先生成 XMLConfigBuilder对象,然后调用 parse() 方法将配置文件的文件流解析成 configuration对象,在利用configuration对象调用第二个 build()方法。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 初始化解析器
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 将构建好的config回传给build()
    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.
    }
  }
}

2)SqlSessionFactory#build(Configuration config) 是在 configuration 对象解析完成后使用 configuration 对象构建 DefaultSqlSessionFactory 对象。

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

配置(Configuration)和配置构造器(XmlConfigBuilder)

Mybatis 支持 XML 形式的 Configuration 配置,XpathParser 是XML解析的工具类,具体解析过程如下:

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

XPathParser 类在初始化时调用两个方法:commonConstructor()createDocument()

public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
  commonConstructor(validation, variables, entityResolver);
  this.document = createDocument(new InputSource(inputStream));
}

创建完 XPathParser 对象后会返回到 XMLConfigBuilder 构造中,将创建的 XPathParser 对象作为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;
}

私有的构造用于初始化 configuration 对象及赋值核心属性。XMLConfigBuilder构建一个 configuration 对象,然后调用父类 BaseBuilder的构造,将 properties 变量赋值到 configuration 对象。

简单总结一下,我们要创建 SqlSessionFactory 对象首先要创建 XMLConfigBuilder 对象,而 创建 XMLConfigBuilder 对象就要创建 XathParser对象,XpathParser 用来解析 XML 配置文件,创建 XMLConfigBuilder 对象成功后,调用 parse() 方法将其解析成 Configuration 对象,再根据 Configuration 对象创建 DefaultSqlSessionFactory对象实例。

构建 SqlSession 实例

创建了 DefaultSqlSessionFactory实例后,就可以创建 SqlSession 对象了,DefaultSqlSessionFactory 类中包含了很多 openSession() 重载方法。

@Override
public SqlSession openSession() {...}

@Override
public SqlSession openSession(boolean autoCommit) {...}

@Override
public SqlSession openSession(ExecutorType execType){...}

@Override
public SqlSession openSession(TransactionIsolationLevel level){...}

二级缓存

Mybatis 使用了两种缓存:本地缓存和二级缓存。

每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

创建cache

下面根据源码分析二级缓存的实现过程

SqlSessionFactoryBuilder 类的 build() 方法中有对生成的 configuration对象进行解析,进入到 parser.parse() 方法内部。

parser.parse()内部有个 parseConfiguration(parser.evalNode("/configuration"));方法用来解析Mybatis全局配置文件 mybatis-config.xml 文件中的配置。

我们先只关注其中解析 mapper标签的方法 mapperElement(root.evalNode("mappers"));

我们都知道在全局配置文件中配置映射的mapper文件有四种方式:

  • package 标签,指定映射器所在包名
  • resource 属性,使用相对于类路径的资源引用
  • url 属性,使用完全限定资源定位符URL
  • class 属性,使用映射器接口实现类的完全限定类名

看源码解析 mapper 标签的逻辑如下:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 使用 package 标签配置映射器
      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 属性配置映射器
        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();
        // 使用 url 属性配置映射器
        } 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.");
        }
      }
    }
  }
}

其中使用XMLMapperBuilder 对象调用 parse() 方法对每一个映射文件进行解析。

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

进到 configurationElement(parser.evalNode("/mapper"));方法,此方法创建了二级缓存。

private void configurationElement(XNode context) {
  try {
    // 获取顶级标签mapper的namespace 属性
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    // 解析 cache-ref 标签
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析 cache 标签
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/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);
  }
}

上述有解析 cache-ref 标签和 cache 标签的方法,我们先看cacheRefElement()

private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

再来看一下 cacheElement() 方法

private void cacheElement(XNode context) {
  if (context != null) {
    //  获取cache标签type属性值,如无指定默认值为 PERPETUAL
    String type = context.getStringAttribute("type", "PERPETUAL");
    // 解析 type 属性值别名
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    // 获取 eviction 属性值,默认值为 LRU
    String eviction = context.getStringAttribute("eviction", "LRU");
    // 解析 eviction 属性别名
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    // 获取 flushInterval 属性值
    Long flushInterval = context.getLongAttribute("flushInterval");
    // 获取 size 属性值
    Integer size = context.getIntAttribute("size");
    // 获取 readOnly 属性值
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    // 获取 blocking 属性值
    boolean blocking = context.getBooleanAttribute("blocking", false);
    //  获取内部的 property 标签属性
    Properties props = context.getChildrenAsProperties();
    // 新建缓存
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

进入到 useNewCache(...) 方法

public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> 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;
}

可以看到此方法中根据 <cache> 标签的属性值构建了 cache 对象。我们进入到真正构建的 build() 方法

public Cache build() {
  // 设置 type 和 eviction 两个属性的默认值
  setDefaultImplementations();
  // 创建 cache 实例
  Cache cache = newBaseCacheInstance(implementation, id);
  // 给cache对象设置属性
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
  if (PerpetualCache.class.equals(cache.getClass())) {
    // 使用装饰器模式给cache对象添加属性
    for (Class<? extends Cache> 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 的属性添加至 cache 对象,点进 newBaseCacheInstance() 方法还可以发现创建缓存使用的反射机制进行创建的。

创建完 cache对象后,将 cache 对象添加到二级缓存 map 中。

protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");

上面描述的是二级缓存的创建过程,那缓存是怎么使用的?

使用 cache

使用 cache 的地方在 CachingExecutor 类中,来看一下做了什么工作,以查询为例。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 获取缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 根据需要刷新缓存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 从缓存中取结果集
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 缓存中没有时,查询数据库
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 结果集存到缓存中
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托模式,交给 SimpleExecutor 等实现类去实现方法
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

首先,从 MapperStatement 中取出缓存,isUseCache方法判断是否使用缓存,并且确保方法没有 out 类型的参数,Mybatis 不支持存储过程的缓存,所以如果是存储过程,这里就会报错。

private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if (parameterMapping.getMode() != ParameterMode.IN) {
        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
      }
    }
  }
}

再从 TransactionalCacheManager 中根据 key 值取出缓存,如果能在缓存中查出结果集,则返回;如没有缓存,就会执行查询,并且将结果集放到缓存中并返回结果。

小结

本文主要从源码角度对 SqlSession的创建过程以及二级缓存进行了分析讲解,如对 Mybatis 感兴趣可关注本专栏其他文章。