文章目录
前言
本文将从源码角度对 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
是配置文件的文件流,environment
和 properties
是可选的参数。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
感兴趣可关注本专栏其他文章。