-
首先入口是SqlSessionFactoryBuilder,该类接受一个Reader来接收MyBatis的配置文件,如mybatis-config.xml,该文件的配置就不介绍了, 接收到配置文件后SqlSessionFactoryBuilder使用[XML ConfigBuilder][]解析该配置文件并返回一个Configuration对象,该对象保存了mybatis-config.xml中的所有配置和mybatis-config.xml文件中设置的Mapper文件的解析结果, Configuration的构造函数设置了所有MyBatis中的默认别名、默认的TypeHandler及其他默认设置,该对象也将贯穿整个执行过程,解析Mapper的过程就是设置Configuration的过程。
-
XMLConfigBuilder解析完成后SqlSessionFactoryBuilder创建一个SqlSessionFactory接口的实现类DefaultSqlSessionFactory,将解析到的Configuration传入该对象并返回, SqlSessionFactory的功能是以不同的形式创建一个SqlSession,下面是一个SqlSession的使用demo。
final SqlSession sqlSession = sqlSessionFactory.openSession(); try { final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class); final PrimitiveSubject subject = mapper.getSubject(1); Assert.assertNotNull(subject); } finally { sqlSession.close(); }
-
SqlSession对象具有执行数据库的增删改查操作、获取Mapper和管理事务的功能,上面的代码中sqlSessionFactory获取SqlSession对象的过程是:
- 获取Configuration中保存的由mybatis-config.xml中的environment元素解析而来的Environment对象,该对象包含了连接数据库所需的相关信息并指定了TransactionFactory (用于获取Transaction, 该类能够管理一个数据库连接的生命周期,包括创建、配置和commit/rollback)的实现类。
- 获取TransactionFactory并从中创建一个Transaction,之后再创建一个Executor,该对象代理了Transaction的执行并且实现了对缓存的支持。
- 最后返回DefaultSqlSession对象。
-
通过SqlSession对象获取所需的Mapper,而Mapper保存在Configuration对象的MapperRegistry对象中,该对象维护了一个Mapper接口和该接口的MapperProxyFactory的Map, MapperProxyFactory使用动态代理返回Mapper接口的代理对象MapperProxy,对Mapper接口的方法调用都会交由MapperProxy对象。
-
MapperProxy处理了可能存在的继承自Object类的方法调用及接口的default方法的调用后,创建一个MapperMethod来执行Mapper方法的调用, 该对象根据当前数据库操作的类型调用SqlSession对象的不同方法并对返回的数据进行必要的处理,如insert、delete、update影响的数据行。SqlSession对象又是通过Executor对象执行数据库操作, Executor对象创建StatementHandler对象来获取和执行java.sql.Statement类,如果是查询操作则在执行完后通过ResultSetHandler处理返回的结果,将结果转换成Mapper接口的返回值类型并返回。
-
从SqlSession获取Mapper接口的实例到最终执行数据库操作涉及到了多个对象,每个对象都有自己的功能:
- MapperProxy:执行对Object类上的方法和default方法的调用,其他的方法调用都是Mapper接口方法的调用,MapperProxy创建MapperMethod对象并将其他方法的调用交由该对象执行。
- MapperMethod:将传入到当前调用的Mapper接口上的方法的参数转换成Map(当存在Param注解指定了参数名或者参数数量大于1时使用注解指定的名称或arg0、param0、arg1、param1...作为参数名并以参数名为key、值为value)或直接返回参数值(不存在Param注解并且只有一个参数时), 根据执行的方法对应的数据库操作(insert|delete|update|select)调用SqlSession对象的对应方法,并在返回结果之前对结果进行处理如将结果添加到List中并返回。
- SqlSession:获取保存在Configuration中的当前的Mapper方法对应的MappedStatement (SQL语句的解析过程),处理传入的参数,这里对参数的处理不同于MapperMethod中的参数处理, MapperMethod处理的主要目的是确定参数的名称,MappedStatement中的处理是如果方法只有一个参数并且是集合或者数组时,根据类型为参数设置上collection、list或array等别名(个人认为这一步在MapperMethod做也可以, MapperMethod中使用的是ParamNameResolver解析的参数名,从类名上看得出这是专门用于解析参数名的类,这和MappedStatement中解析参数的功能类似, 如果再创建一个类包含了MappedStatement中解析参数功能,并且ParamNameResolver作为其成员变量使其也拥有了确定参数名的功能,则这样的一个类放到MapperMethod中取代现有的ParamNameResolver的调用我觉得也是可以的, 或者直接修改现有的ParamNameResolver确定参数名称的过程也可以,不确定MyBatis现在这种做法的好处是什么,当然现在这么做也没问题),之后调用Executor类执行数据库操作。
- Executor:实现了缓存、事务的
commit/rollback
操作和数据库操作,执行数据库操作时创建StatementHandler,使用该对象获取数据库连接和创建java.sql.Statement
对象并交由StatementHandler执行数据库操作 - StatementHandler:最终执行数据库操作的类,调用
java.sql.Statement
类的execute
方法并获取到结果后对结果进行处理并返回
- 下面是解析Configuration的顺序:
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);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
利用XPath解析XML配置文件,propertiesElement
方法解析XML中的properties节点,在创建对应的XNode时使用PropertyParser解析${value}和${value:defaultValue}形式的属性值。
loadCustomVfs
使用ClassLoader加载VFS,VFS用于加载指定路径下的jar文件或class文件。settingsElement
设置MyBatis中的各项参数如是否开启缓存,最主要的是mapperElement
方法,
该方法解析mapper文件或mapper接口,mapperElement
方法分两种情况,一种解析接口,一种解析XML:
-
接口解析:调用Configuration的
addMapper
或addMappers
方法添加单个接口或包路径下接口,Configuration利用MapperRegistry添加Mapper接口, MapperRegistry创建当前解析的接口的MapperProxyFactory对象并保存下来,之后创建MapperAnnotationBuilder对象解析当前接口,MapperAnnotationBuilder首先判断当前接口包路径下是否存在同名的Mapper的XML文件, 如果存在则使用XMLMapperBuilder解析该XML,解析过程中涉及到的尚未解析到的引用将会被添加到Configuration的incompleteXXX中,这些incompleteXXX是所有解析Mapper接口和XML文件共享的, 每个解析过程都会获取一次所有的incompleteXXX并尝试再次解析,所以能保证当前尚未解析到的incompleteXXX在完成所有的Mapper接口和XML文件解析过程之前肯定能完成解析。解析时以接口类的toString结果作为当前接口资源的namespace, 如interface org.apache.ibatis.autoconstructor.AutoConstructorMapper
,之后获取类上的CacheNamespace注解和CacheNamespaceRef注解,CacheNamespace配置了指定namespace的缓存属性, CacheNamespaceRef指定了引用哪个namespace的缓存。之后解析每个接口方法的注解,方法上可用的注解有Insert、Update、Delete、Select、InsertProvider、UpdateProvider、DeleteProvider、SelectProvider、Options、SelectKey、ConstructorArgs、TypeDiscriminator、Results、MapKey、ResultMap、ResultType、Flush
- Insert、Update、Delete、Select、InsertProvider、UpdateProvider、DeleteProvider、SelectProvider:指定当前方法的SQL语句,XXXProvider能够指定类名和返回在运行时执行的 SQL 语句的方法。
- Options:用于设置映射语句的属性如
useCache
,由于Java的注解不能设置值为null,所以Options
包含了很多默认值,使用时需要注意。 - SelectKey:用于设置生成某个属性值的SQL语句,如
@SelectKey(statement = "call identity()", keyProperty = "nameId", before = false, resultType = int.class)
,解析SelectKey
时以当前方法的id + "!selectKey"为SelectKey
的id,这样就能将SelectKey
和方法进行绑定,之后将SelectKey
中的SQL解析成MappedStatement添加到Configuration中,并根据该MappedStatement创建SelectKeyGenerator添加到Configuration, SelectKeyGenerator实现了KeyGenerator接口,用于在运行SQL时对SQL进行处理。 - ResultMap:为
Select
或SelectProvider
注解设置在XML中配置的ResultMap
的id - ConstructorArgs:设置返回结果的构造函数,如
@ConstructorArgs({ @Arg(property = "id", column = "cid", id = true), @Arg(property = "name", column = "name") })
-
XML解析:直接使用XMLMapperBuilder解析,解析过程和使用MapperAnnotationBuilder解析过程类似,只不过配置都写在了XML中,XML中的
namespace
就是该XML所配置的Mapper接口的全路径, 在XML解析完成会以namespace
作为类名添加到Configuration中。
-
SQL语句可以以注解的形式配置在接口方法上或以XML的形式配置某个接口的SQL,两种方法区别不大,以XML的形式配置更加灵活且支持的功能更多,下面分析从XML解析SQL语句的过程: 略过
cache
和resultMap
等解析过程,解析SQL语句时首先获取所有的SQL语句的XNode对象(XPath表达式:select|insert|update|delete),之后获取保存在Configuration中的databaseId
(如果有的话,databaseId
用于实现编写多数据库SQL)以便过滤不需要解析的其他数据库的SQL语句, 之后开始遍历获取到的所有XNode
,为每个XNode
创建一个XMLStatementBuilder对象并解析对应的XNode
对象。解析时获取了所有可能的设置,如果未设置则以null
或默认值表示,值得注意的是statementType
属性, 该属性用于指定最后将被创建出来的java.sql.Statement
对象的类型,可选的有STATEMENT, PREPARED, CALLABLE
,分别对应JDBC中的常规语句(General statement,没有任何参数的SLQ语句)、 预置语句(Prepared statement,编译优化一次即可多次运行,每次可以传递不同的参数,数据库的批量执行也是利用该对象,提交多个参数最后一次执行)、可调用语句(Callable statement,可以执行数据库中的存储过程),在解析SQL前首先解析当前SQL引入的SQL代码片段和SelectKey
(如果有的话),SQL代码片段如:<include refid="userColumns"><property name="alias" value="t1"/></include>
解析SQL代码片段时根据refid获取SQL代码片段(在解析SQL语句前SQL代码片段就已经备解析了),之后将include
中的property
保存下来,替换SQL代码片段中指定的属性如上述配置将会替换SQL代码片段中的alias为t1,之后将解析完成的SQL代码片段替换include
元素,SQL代码片段就解析完成了。
之后是解析SelectKey
,将SelectKey
和普通的SQL语句一样作为MappedStatement对象保存到Configuration中,再根据这个从SelectKey
创建出来的MappedStatement对象创建KeyGenerator对象保存到Configuration中并以以当前方法的id + "!selectKey"为SelectKey
的id便于之后能够找到某个方法的KeyGenerator,解析完后从SQL语句中删除SelectKey
部分。
最后由LanguageDriver创建SqlSource对象,将所有这些信息整合到MappedStatement对象中并添加到Configuration中,SQL语句也就解析完成了。
-
解析动态SQL时,如
SELECT * FROM BLOG WHERE state = ‘ACTIVE’ AND title like #{title} 当解析Mapper接口的Select注解指定的或Mapper接口对应的XML中配置的SQL语句时,使用LanguageDriver获取SqlSource对象,SqlSource能接受用户参数、处理动态语句的内容并返回BoundSql对象,BoundSql代表的就是最终需要执行的SQL语句,对动态语句的支持主要就是在SqlSource中。 LanguageDriver获取SqlSource的过程如下:
-
以默认的LanguageDriver的实现XMLLanguageDriver为例,创建XMLScriptBuilder解析传入的包含待执行的SQL语句的XNode对象,如上述的
<select>
将传入如下代码并开始解析。SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if>
-
XMLScriptBuilder首先获取传入的XNode对象的所有子元素并判断子元素的类型,如果是字符串则判断字符串是否是动态SQL(判断过程看XMLScriptBuilder类的注释),如果是则返回支持解析变量的TextSqlNode类,如果是
Node.ELEMENT_NODE
的则根据节点名如where
获取对应的NodeHandler, 不同的NodeHandler将创建不同的SqlNode对象,并统一添加到同一个作为根的MixedSqlNode对象,在遍历完所有子元素后,该根对象创建完成,XMLScriptBuilder将该对象作为参数传入DynamicSqlSource对象并返回DynamicSqlSource对象,SqlSource就获取完成了(具体过程可以看XMLScriptBuilder类的注释)。
实现动态SQL关键在于DynamicSqlSource在获取BoundSql对象时的处理,DynamicSqlSource对象能够接收包含了所有运行时指定的传入Mapper接口方法中的参数,并创建DynamicContext对象,此时先前传入的MixedSqlNode对象将接收该对象并分析动态SQL内容。 以
where
和foreach
这两种比较复杂的场景为例分析:- where:由[WhereSqlNode][]处理
where
的动态SQL,其继承自TrimSqlNode,所有的方法也都是在TrimSqlNode中实现的,而where
内都是包含其他若干子元素如if
,而这些子元素已经在[WhereHandler][]解析并以一个MixedSqlNode对象的形式组合在一起, 解析where
的过程实际上就是解析这些子元素的过程,子元素如if
在提取到条件如id > 0
后,使用Ognl解析该表达式并获取结果判断是否应用当前子元素中的SQL语句, 如果满足条件则使用DynamicContext记录当前子元素的SQL,在遍历完所有子元素后即可获取where
语句中符合条件的SQL组成的一条SQL,在返回最终结果前会在SQL语句前加上WHERE
字符串,所以使用<WHERE>
构建SQL时不需要自己手写WHERE
关键字。 - foreach:由ForeachSqlNode处理
foreach
的动态SQL,首先使用Ognl获取参数中由foreach
的collection
属性指定的集合属性的值, 之后开始遍历该集合的元素,遍历前添加open
属性指定的字符串,遍历元素时将会添加separator
属性指定的分隔符,每次遍历都会根据当前元素的索引或key(如果是Map类型的集合参数的话)更新item
和index
属性的值(和传入方法的参数一样保存在DynamicContext的bindings
中), 再根据这些值解析动态SQL,将指定的属性值替换成对应的别名形式,如#{__frch_item_0}或#{__frch_index_0},遍历完成后添加close
属性指定的结束符号。
在解析完成动态属性后DynamicSqlSource使用SqlSourceBuilder再次解析返回的SQL语句,该类将所有#{}包围的变量以?替代以满足JDBC语法要求,再将所有解析到的参数(保存在DynamicContext的
bindings
的包括了遍历出来的__frch_item_0或__frch_index_0形式的参数和传入Mapper方法的参数)添加到最终返回的StaticSqlSource中便于之后执行该SQL时使用。 -
- 一级缓存:在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。也就是说一级缓存是在一个SqlSession内的缓存(默认情况下一级缓存在一个SqlSession内共享,可以设置缓存级别为
STATEMENT
)。 一级缓存是Executor实现的,BaseExecutor类实现了该接口的大部分功能包括一级缓存功能,BaseExecutor使用PerpetualCache类作为缓存的支持, PerpetualCache实现了Cache接口,该接口提供了和缓存相关的最基本的操作,PerpetualCache实现缓存的功能很简单,将缓存维护在一个HashMap
中。 每个SqlSession都有一个Executor对象,SqlSession对象对外提供数据库操作,而具体的数据库操作又是委托给了Executor,Executor在执行查询功能时会为当前查询请求创建一个CacheKey对象, 该对象用来作为是否是相同的查询请求的标示,Executor对象构建CacheKey时以Statement Id + Offset + Limmit + Sql + Params
作为参数,如果这5项都相等的两个查询请求则会被认为是相同的请求,对于相同的请求缓存才会生效。 创建CacheKey后Executor在执行查询操作时首先判断缓存中是否已存在当前请求的数据,如果已存在则以缓存中的数据作为结果返回,在返回之前还会判断一级缓存的级别是否为STATEMENT
的,如果是则清空缓存,所以STATEMENT
级别的一级缓存是在一个Statement
内的。 Executor中的update
方法用于处理update、insert、delete
操作,代码如下
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
在执行前会清空缓存,这就能保证在一个SqlSession内不会出现脏数据(不能避免SqlSession间的脏数据,因为一级缓存在SqlSession内,SqlSession间的缓存互不影响,某个SqlSession更新了数据库只会情况自己的缓存而不会影响其他SqlSession的缓存,可以设置一级缓存的缓存级别为STATEMENT
避免这个问题,STATEMENT
级别的缓存在每次查询之后都会清空缓存,这就相当于禁用了一级缓存)。
- 二级缓存:由CachingExecutor实现,每个SqlSession都有一个Executor对象,而该对象是DefaultSqlSessionFactory在创建SqlSession是使用Configuration对象的
newExecutor
方法创建的,代码如下
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
如果开启了二级缓存则cacheEnabled
为true
,返回的Executor就是CachingExecutor对象,代理了已有的executor的执行。CachingExecutor会在执行查询操作前获取当前MappedStatement对象的Cache,如果为空则直接调用被代理的Executor对象的查询方法,
这样就和一级缓存是一样的了。MappedStatement对象的Cache是在分析Mapper接口的XML文件时添加的,如果需要开启某个Mapper的二级缓存,需要在XML中添加<cache/>
,这样就是为MappedStatement对象添加一个默认的Cache,可以配置该Cache的属性:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
创建Cache的代码如下:
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;
可以看到默认实现方式还是PerpetualCache,并且默认添加了一个LruCache,该对象将会代理PerpetualCache并实现了最近最少使用算法以保证缓存的数据量不会超过某一个值(默认1024)。在CacheBuilder内部还添加了若干个Cache,都是以装饰器模式组合的,
最终的调用链将是SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
,功能如下:
- SynchronizedCache: 同步Cache,实现比较简单,直接使用synchronized修饰方法。
- LoggingCache: 日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
- SerializedCache: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
- LruCache: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。
- PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
Lru的实现是使用LinkedHashMap
,LinkedHashMap
支持按照添加的顺序存储,也可以按照访问的顺序存储,这里的实现方式就是利用了安装访问的顺序存储的特性,LruCache
声明LinkedHashMap
的代码如下:
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
removeEldestEntry
方法表示是否删除最老的数据,这里覆盖了LinkedHashMap
的默认行为,在操作设置的尺寸后删除最老的数据,删除数据时LruCache的keyMap
中的数据将会自动删除,
但是被代理的Cache需要手动调用删除,代码如下,每次putObject
时才有可能触发删除操作:
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
缓存的实现就是如上描述,下面来看CachingExecutor如何使用缓存以支持二级缓存的。 CachingExecutor查询代码如下:
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.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Cache对象是从MappedStatement对象中获取的,而MappedStatement对象是保存在Configuration中的,所以正常情况下全局只有一个,MappedStatement由namespace
作为唯一表示,所以二级缓存是在namespace
内的(如果缓存被其他[MapperStatement][]引用则是在这些namespace
之间)。CachingExecutor实现二级缓存的方式是,
首先检查是否需要刷新缓存,默认情况下之后insert、update、delete
会刷新缓存,可以在查询语句中添加flushCache="true"
来强制调用某个查询语句时刷新缓存。
之后将会从tcm
中获取缓存数据,tcm
是TransactionalCacheManager对象,该对象维护了一个Map:private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
,
Map的值TransactionalCache实现了Cache接口,CachingExecutor使用他包装Cache,该类的作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
具体的实现方式是,当第一次执行查询语句时没有缓存数据,此时从被代理的Executor中获取缓存数据并添加到TransactionalCache中,TransactionalCache添加缓存数据的代码如下:
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
将数据保存在了Map
中,所以在未commit
之前缓存数据是不会被保存到缓存中的,调用SqlSession的commit
方法后会调用CachingExecutor的commit
方法,该方法实现如下:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
此时才会将数据添加到缓存中,需要注意的是,获取SqlSession时即使设置了autocommit
为true
也不会自动调用SqlSession的commit
方法,
autocommit
是数据库自身支持的,所以为了二级缓存能够工作,需要手动在查询后调用commit
方法。
另外上述代码的entriesMissedInCache
作用是保存了在事务提交之前所有未查询到的CacheKey,这是在缓存的blocking
为true
时,缓存的调用将为BlockingCache -> SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
时用的,
BlockingCache会阻塞在第一个查询开始之后且保存查询到的数据到缓存之前的其他所有相同的查询请求直到缓存中存在数据。这一功能的实现方式是使用ReentrantLock
实现的,ReentrantLock
的使用方式是获取锁和释放锁要成对出现,查看BlockingCache的实现可知,getObject
时获取锁,putObject
时才会释放锁,正常情况下,
entriesMissedInCache
中的元素肯定在entriesToAddOnCommit
中,因为查询操作在缓存未命中的情况下总是在查询之后伴随着put数据到缓存中的过程,这会使得即使未命中某个CacheKey,
使得该CacheKey被添加到entriesMissedInCache
中(TransactionalCache的getObject
方法),这个CacheKey对应的数据也将在之后的put操作中和它对应的从数据库中查询到的数据一块添加到entriesToAddOnCommit
中,
但是如果在查询时出现异常了,导致没有执行查询之后的put缓存操作,这会使得BlockingCache的锁操作只有获取锁而没有释放锁,也就会导致这个异常查询的后续查询都处于阻塞状态,
这些存在阻塞的CacheKey就保存在entriesMissedInCache
中,所以在commit/rollback
时需要释放这些阻塞请求,这只需要调用putObject
就可以了.
MyBatis
使用Interceptor接口表示拦截器,拦截器能够作用的时间点有:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
上述类的相应的方法可以被拦截器作用,实现的原理看下面的测试代码就可以理解了:
@Test
public void mapPluginShouldInterceptGet() {
Map map = new HashMap();
map = (Map) new AlwaysMapPlugin().plugin(map);
assertEquals("Always", map.get("Anything"));
}
@Test
public void shouldNotInterceptToString() {
Map map = new HashMap();
map = (Map) new AlwaysMapPlugin().plugin(map);
assertFalse("Always".equals(map.toString()));
}
@Intercepts({
@Signature(type = Map.class, method = "get", args = {Object.class})})
public static class AlwaysMapPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return "Always";
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
上面的代码和MyBatis
的拦截器还没有关系,但是MyBatis
中拦截器的声明方式是通用的,关键之处在于plugin
方法的实现,该方法使用Mybatis
实现的代理类Plugin,返回一个动态代理对象,wrap
方法实现如下:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
首先根据注解配置返回一个被代理类和该类被代理方法的Map
(根据Signature注解的type找到类,再根据method和args找到方法,最后以类为key,方法为value),之后获取被拦截的对象即target的所有接口中存在于signatureMap
的接口,并以这些接口为参数创建动态代理对象,代理对象是new Plugin(target, interceptor, signatureMap)
该对象实现了InvocationHandler
接口,invoke
方法实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
首先判断当前调用的方法是否是需要拦截的方法,如果是则创建一个Invocation并调用拦截器的intercept
方法,Invocation维护了被代理的对象、被代理的方法和方法参数。MyBatis
的拦截器实现原理就是简单的利用Java的动态代理,而拦截器对目标类的拦截是在创建目标类的地方做的,如创建Executor时:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
类似executor = (Executor) interceptorChain.pluginAll(executor)
存在于所有支持拦截的类的创建方法中,在解析mybatis-config.xml
时已经将所有的拦截器添加到了interceptorChain
中,interceptorChain
是InterceptorChain类,其pluginAll
方法如下:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
通过循环应用所有的拦截器,所以拦截器拦截的对象也有可能是一个拦截器,如果想要实现同时运行多个拦截器,则拦截器中需要调用Invocation的proceed
方法(最后一个拦截器如果想返回一个固定的值而忽略数据库操作的值可以不调用该方法),该方法代码如下:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
如果某个拦截器拦截的是另一个拦截器,则需要在处理完后调用Invocation的proceed
方法将调用传递到后面的拦截器,如某个实现了更新时对密码加密,查询时解密的拦截器实现:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})})
public class DBEncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
Object parameter = invocation.getArgs()[1];
if (parameter != null && methodName.equals("update")) { //如果是更新则将参数加密
invocation.getArgs()[1] = encrypt(parameter);
}
Object returnValue = invocation.proceed(); //获取操作结果并对结果解密,结果可能是update方法的结果也可能是select
if (parameter != null && methodName.equals("select")) { //update操作的结果没必要解密
if (returnValue instanceof ArrayList<?>) {
List<?> list = (ArrayList<?>) returnValue;
for (Object val : list) {
decrypt(val);
}
} else {
decrypt(returnValue);
}
}
return returnValue;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
MyBatis的事务管理分为两种形式:
- 使用JDBC的事务管理机制:即利用
java.sql.Connection
对象完成对事务的提交(commit)、回滚(rollback)、关闭(close)等 - 使用MANAGED的事务管理机制:这种机制
MyBatis
自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理
mybatis-config.xml
中的environment
的子元素transactionManager
配置了使用哪种形式的事务管理:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
在解析mybatis-config.xml
文件,创建Environment对象时会根据type创建不同的TransactionFactory,分别创建不同的Transaction,Executor在执行数据库操作时获取的数据库连接就是从Transaction对象创建而来的,下面分析两种Transaction的实现:
-
JdbcTransaction:
@Override public void commit() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Committing JDBC Connection [" + connection + "]"); } connection.commit(); } } @Override public void rollback() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Rolling back JDBC Connection [" + connection + "]"); } connection.rollback(); } }
-
ManagedTransaction:
@Override public void commit() throws SQLException { // Does nothing } @Override public void rollback() throws SQLException { // Does nothing }
JdbcTransaction将commit/rollback
交给了数据库自己处理,ManagedTransaction不实现commit/rollback
方法,将commit/rollback
交给了容器处理,
所以如果使用MyBatis
构建本地程序,而不是WEB程序,若将type
设置成MANAGED
,那么执行的任何update
操作,即使最后执行了commit
操作,数据也不会保留,不会对数据库造成任何影响。因为将MyBatis
配置成了MANAGED
,即MyBatis
自己不管理事务,而我们又是运行的本地程序,没有事务管理功能,所以对数据库的update
操作都是无效的。
如果是JDBC的事务管理机制,则MyBatis的commit/rollback
过程是,除了select方法外,每次DefaultSqlSession
执行insert、update、delete
等方法时设置标志位dirty
为true
,代码如下:
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
SqlSession
的使用模式如下:
final SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// do something
} finally {
sqlSession.close();
}
每次SqlSession
的获取和使用都伴随着一个close,代码如下:
public void close() {
try {
executor.close(isCommitOrRollbackRequired(false));
closeCursors();
dirty = false;
} finally {
ErrorContext.instance().reset();
}
}
executor.close()
方法代码如下:
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
rollback
方法执行缓存的清空工作,并根据forceRollback
判断是否需要执行rollback,forceRollback
的值是isCommitOrRollbackRequired
方法返回的,
isCommitOrRollbackRequired
方法代码如下:
private boolean isCommitOrRollbackRequired(boolean force) {
return (!autoCommit && dirty) || force;
}
autoCommit
是在sqlSessionFactory
调用openSession
创建SqlSession
时传入的,如果不传该参数则默认为false,而dirty
在执行任何数据库的update
操作时都会被设置成true,所以在默认情况下,如果不执行
SqlSession
的commit()
方法则在该SqlSession
上执行的数据库更新操作都不会生效,因为在调用SqlSession
的close
方法时会执行rollback操作。而如果在使用SqlSession
期间执行发生异常需要rollback时需要手动调用SqlSession
的rollback
方法,
SelSession
的rollback
方法代码如下:
try {
executor.rollback(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
同样是根据isCommitOrRollbackRequired
方法判断是否需要执行rollback
,而在autoCommit
为true
的情况下,事务是自动提交的,每次操作都会自动提交,所以回滚也没有意义了,isCommitOrRollbackRequired
方法返回的将是false
,所以在创建可能需要rollback
的SqlSession
时不应该设置autoCommit
为true,
使用时应该自己在数据库操作完成后调用commit
,并在发生异常时调用rollback