我们知道,我们在启动项目的时候,spring就会帮我们实例化好bean,那我们使用mybatis-spring的时候,编
写的maper是怎么交给spring容器的呢?这就是今天要探讨的问题。
一、扫描阶段
我们在接入mybatis-spring的时候会在相应的配置类增加这样的注解
@MapperScan(basePackages = "com.test.**.mapper")
关于开始提到的问题,就从这里开始。 点开@MapperScan这个注解,发现有如下这个注解
@Import({MapperScannerRegistrar.class})
这个注解正是关键所在,spring在bean创建之前首先会执行AbstractApplicationContext#invokeBeanFactoryPostProcessors(beanFactory)方法,执行bean工厂的处理器,在执行这个方法的时候就会扫描出所有的BeanDefinition,其中要扫描的范围就包括了@Import这个注解。那就需要看看MapperScannerRegistrar这个类做了什么事。
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
public MapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
// 省略部分代码
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
List<String> basePackages = new ArrayList();
String[] var10 = annoAttrs.getStringArray("value");
int var11 = var10.length;
int var12;
String pkg;
for(var12 = 0; var12 < var11; ++var12) {
pkg = var10[var12];
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
var10 = annoAttrs.getStringArray("basePackages");
var11 = var10.length;
for(var12 = 0; var12 < var11; ++var12) {
pkg = var10[var12];
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(basePackages));
}
注:考虑到篇幅,上述源码省略了部分代码
上述代码主要做了以下几件事:
- 获取注解@MapperScan的value属性和basePackages得到要扫描的包路径;
- scanner.registerFilters();注册filters,其实就是扫描的规则,也就是说,basePackages包下面的哪些文件是需要被扫描的,哪些是不需要的。
- scanner.doScan(StringUtils.toStringArray(basePackages));开始扫描指定包下面的文件。
- scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); 这两行代码是判断注解有没有对应的属性,有的话就添加到ClassPathMapperScanne这个扫描类。
ClassPathMapperScanner#doScan
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
先调用父类(org.springframework.context.annotation.ClassPathBeanDefinitionScanner)扫描出符合条件的所有BeanDefinitionHolder并注册到spring容器中(beanDefinitionMap)。然后对每个BeanDefinition执行后置处理。
ClassPathMapperScanner#processBeanDefinitions
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
Iterator var3 = beanDefinitions.iterator();
while(var3.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(2);
}
}
}
- 1、这里对每个BeanDefinition进行后置处理。添加构造参数的值、设置beanClass为MapperFactoryBean类型。这样一来,我们编写的mapper就接口再spring容器中都对应着MapperFactoryBean。比如我们有OrderMapper这个接口,那在spring容器中存在的BeanDefinition的beanClass为MapperFactoryBean,当通过bename为orderMapper从springring容器中创建bean的是候其实先创建的是MapperFactoryBean,再根据MapperFactoryBean的getObject方法得到MapperProxy,最终自动注入OrderMapper的时候也是注入MapperProxy。 MapperFactoryBean#getObject的调用链如下:
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
SqlSessionTemplate#getMapper
public <T> T getMapper(Class<T> type) {
return this.getConfiguration().getMapper(type, this);
}
Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
注意,这里会从knownMappers这个map中拿出MapperProxyFactory然后通过mapperProxyFactory.newInstance(sqlSession)得到MapperProxy。 MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy)
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
- 2、为什么需要添加构造参数? 因为MapperFactoryBean中有个带参的构造方法
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
实例化MapperFactoryBean的时候会通过构造方法反射出来,所以需要为这个构造方法添加参数,但是这里直接添加一个字符串进去了,比如"com.test.orderMapper",在实例化的时候spring可以将这个字符串转换成OrderMapper.class。
- 3、definition.setAutowireMode(2);最后一处代码,如果条件满足,就把MapperFactoryBean的自动装配模型改为2(RootBeanDefinition.AUTOWIRE_BY_TYPE);
- (1)、 从源码可以不难看出,如果没有sqlSessionFactory和sqlSessionTemplate的时候就会条件满足。那为什么这里需要这么做呢?因为spring中,默认的自动装配模型是0(RootBeanDefinition.AUTOWIRE_NO),在自动装配模型是0的情况下,使用了@Autowired注解是可以完成属性的自动装配的。我们看到,在Mybatis中,没有使用到这样的注解,所以需要设置装配类型为根据类型自动装配,这样一来,就可以完成MapperFactoryBean中所需要的属性注入。
- (2)、那什么情况下条件不满足呢?回顾开始的代码
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
就是在这里完成的,满足条件的话,就在这里直接把需要的属性值添加进去,后期就不需要通过自动装配来完成了,自然也不需要更改默认的自动装配类型。
小结:
- 1、扫描出所有的mapper所对应的BeanDefinition;
- 2、把mapper变成FactoryBean;
- 3、为BeanDefinition添加构造方法的值,用于实例化。
二、初始化操作
下面的源码比较多,重要的部分用红色字体已经标出,可快速浏览。
MapperFactoryBean的类继承关系下,可以看到它不但是一个FactoryBean,还最终实现了InitializingBean接口,Mybstis的初始化动作正是这样来完成的。

1、DaoSupport#afterPropertiesSet
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// Let abstract subclasses check their configuration.
checkDaoConfig();
// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
这里直接调用子类的checkDaoConfig方法,所以看MapperFactoryBean中的即可(initDao没有任何实现方法,就是个空方法,不用管)
2、MapperFactoryBean#checkDaoConfig
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
首先调用父类的checkDaoConfig方法
protected void checkDaoConfig() {
Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
sqlSessionFactory和sqlSessionTemplate在上面提过,要么通过spring自动装配进去,要么自己添加进去。 执行完父类方法后就开始执行configuration.addMapper(this.mapperInterface);方法。 MapperRegistry#addMapper
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.put(type, new MapperProxyFactory<T>(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);
}
}
}
}
- 为每个mapper创建一个对应类型的MapperProxyFactory,然后put到map中去。前面说过,最终注入的是MapperProxy对象,而MapperProxy这个对象就是从MapperProxyFactory产生的,所以需要提前创建好。
- 解析mapper。
3、MapperAnnotationBuilder#parse
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
- 加载xml文件
- 设置当前的名称空间,就是Mapper的权限定名,如com.test.mapper.OrderMapper
- 解析Statement 在xml文件中的,比如OrderMapper.xml,这里就对OrderMapper.xml进行解析
4、MapperAnnotationBuilder#loadXmlResource
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
- 根据Mapper的全限定名,替换成对应的xml文件名,这也是为什么我们自己直接在同目录下写个同名的xml文件不用添加别的配置就能被解析到的原因。
- 加载xml文件,然后解析xml文件。
5、XMLMapperBuilder#parse
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
- parser.evalNode("/mapper"),顾名思义,接可以解析出xml的每个节点文件;
- resource:就是com/test/mapper/OrderMapper.xml这样的字符串。
6、XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
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. Cause: " + e, e);
}
}
此时context中就是xml文件中所有原生的标签形式的sql语句,诸如下面这样:
<mapper namespace="com.test.mapper.OrderMapper">
<select id="select" resultType="String">
select * from tableName where id='1'
</select>
</mapper>
- 这里的namespace就是com.test.mapper.OrderMapper。然后依次解析cache-ref标签、Cache标签、mapper标签中的parameterMap标签、mapper标签中的resultMap标签、mapper标签中的sql标签;select|insert|update|delete这几个标签。
- 通过buildStatementFromContext这个方法的调用链执行到XMLStatementBuilder#parseStatementNode这个方法
7、XMLStatementBuilder#parseStatementNode
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
-
boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect);
注意这三行代码,首先判断是不是select类型的sql语句,如果是的话,再看有没有填充flushCache和useCache的值,如果有就用填充的,没有就设置默认值。
-
这里整个方法来看就是拿到标签的属性值,解析出sql,最终通过addMappedStatement方法将这些结果包装成MappedStatement添加到mappedStatements这个map中。比如我们在OrderMapper.xml中一个id为select的方法,mappedStatements这个map的key就是com.test.mapper.select,value就是解析后包装好的MappedStatement。当使用的时候就是根据id在这个map中拿到MappedStatement,再拿出sql去数据库执行的
小结:
利用InitializingBean接口,完成mapper信息的初始化
三、执行sql
由于注入的是代理,所以当执行Mapper中的sql的时候会执行MapperProxy的invoke方法
1、MapperProxy#invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
首先通过cachedMapperMethod这个方法得到MapperMethod,开始先从缓存中获取,如果从缓存中获取不到就构建出一个MapperMethod,并把构建的MapperMethod加入到缓存,最后返回MapperMethod。cachedMapperMethod方法代码如下:
2、MapperProxy#cachedMapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
MapperMethod中只有SqlCommand和MethodSignature这两个属性,看名字也大概能知道是存放什么信息的。得到MapperMethod就执行它的execute方法。
3、MapperMethod#execute
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
这个方法里面就是判断是INSERT、UPDATE、DELETE还是SELECT语句,然后转换参数。由于逻辑比较单一,代码又多。由于我这边使用的是直接根据主键查询一条结果进来的,所以选择了其中两行代码来说明。
- 1、convertArgsToSqlCommandParam:顾名思义,这个方法就是进行参数转换
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasNamedParameters && paramCount == 1) {
return args[params.keySet().iterator().next().intValue()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey().intValue()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
要说明的是参数的名字和值会在param这个map中存在两份,一份是我们在写mapper中的sql时通过@Param注解执行的名字,一份是根据参数的顺序,生成param1、param2、param3……..这样的名字。param1这种名字的用法。就是我们在写mapper中可以不指定参数名字,然后再xml文件中写sql的参数引用时按照顺序写param1、param2、param3这样也能被正确的将对应的参数值赋进去。
-
2、selectOne:执行查询方法,会执行到SqlSessionInterceptor的invoke方法(SqlSessionInterceptor是SqlSessionTemplate的内部类,实现了InvocationHandler接口。)注意SqlSessionInterceptor的invoke方法里面又是对Mybatis(注意是Mybatis,不是Mybatis-spring)里面的查询方法进行的代理,所以这个invoke方法里面会使用method.invoke先来执行原生Mybatis下对应的方法(本案例对应的为DefaultSqlSession#selectOne(java.lang.String, java.lang.Object))。
关于SqlSessionInterceptor的invoke方法如下,后文还会提到这个方法 SqlSessionInterceptor#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
跟进DefaultSqlSession#selectOne的调用链,会执行到如下方法:
4、DefaultSqlSession#selectList
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- 首先根据statement从mappedStatements这个map中得到MappedStatement。statement就是方法的唯一id,比如:com.test.mapper.OrderMapper.select。这个MappedStatement就是在
7、XMLStatementBuilder#parseStatementNode加进去的; - 执行查询方法。
上述查询方法会执行到下面的方法
5、BaseExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
从上面代码很容易可以看出做了下面这几件事情:
- 1、这里的ms.isFlushCacheRequired()实际上就是我们在写xml中的sql时设定的flushCache属性的值(或者默认值)。下面的configuration.getLocalCacheScope()顾名思义就是本地缓存的作用于,可以看到,这两处成立的话都会使用clearLocalCache方法清除本地缓存,每次都会从数据库查询数据。不同的是ms.isFlushCacheRequired()是针对具体的MappedStatement的,而configuration.getLocalCacheScope()是针对整个mybatis配置的,也就是说这个是对所有的MappedStatement生效的。clearLocalCache方法如下:
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
关于clearLocalCache方法下文还会提到
- 2、首先从本地缓存localCache中取结果,如果缓存中没有再执行queryFromDatabase方法从数据库查结果,并把查询结果put到本地缓存localCache中。最后,将查询的结果一步步返回。
四、使用缓存还是从数据库查
其实上面已经提到了一部分关于缓存的问题,这节相当于上面的有效补充。
public void getById(String id){
orderMapper.select(id);
orderMapper.select(id);
}
假设有这样的方法,同一条sql执行两次(不要纠结这个方法是void类型的,这里只是说明执行同样的sql时是否缓存的问题)。 执行流程和上面讲的一样。现在再回过头看一下上文提到的 SqlSessionInterceptor#invoke 方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
通过 Object result = method.invoke(sqlSession, args);这行代码执行完后拿到结果,再根据条件判断是否执行commit方法,以及finally块的closeSqlSession方法,这两个方法都和清除缓存有关。 跟进commit方法的调用链,会经过上文提到的clearLocalCache方法清楚本地缓存,closeSqlSession也会通过DefaultSqlSession#close执行经过clearLocalCache。
SqlSessionUtils#closeSqlSession
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
Assert.notNull(session, "No SqlSession specified");
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
if (holder != null && holder.getSqlSession() == session) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
}
holder.released();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
}
session.close();
}
}
那如何能不通过这两处代码执行clearLocalCache方法使得第二次查询的时候使用缓存呢?那就得使用@Transactional注解
@Transactional
public void getById(String id){
orderMapper.select(id);
orderMapper.select(id);
}
那么,@Transactional注解是如何使得SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)为true呢?
@Transactional是如何使得Mybatis一级缓存生效的
回顾上文提到的***SqlSessionInterceptor#invoke***方法,这个方法首先会执行
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
SqlSessionUtils#getSqlSession
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
首先从TransactionSynchronizationManager.getResource中获取SqlSessionHolder。(跟进getResource方法其实就是从一个本地线程的map中取值,这里就不贴源码了。)第一次肯定获取不到,所以就会执行下面的逻辑registerSessionHolder来注册SqlSessionHolder(这个方法里面通过TransactionSynchronizationManager.bindResource(sessionFactory, holder);最终将SqlSessionHolder注册到map)
现在我们再次回到 SqlSessionInterceptor#invoke 方法中SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)这行代码
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
return (holder != null) && (holder.getSqlSession() == session);
}
由于已经在map中注册了SqlSessionHolder,所以这里是可以拿到的,这个SqlSessionHolder中的session就是DefaultSqlSession,这里会返回true。 所以外围的代码自然不会执行。
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
这里commit方法不会执行。本地缓存就不清理。 SqlSessionUtils#closeSqlSession 中同理也不会关闭连接。
以上不会清除本地缓存的前提是一级缓存用的默认的session级别,如果将一级缓存级别改为statement的话虽然不会通过commit方法和closeSqlSession方法清理本地缓存,但是会通过上文提到的5、BaseExecutor#query中的方法清理本地缓存
小结:
- 1、如果一级缓存的级别是statement,不管加不加事务注解都会清理本地缓存;
- 2、如果一级缓存的级别是session,在增加了@Transactional事务注解后,会给一个本地线程map中添加(DefaultSqlSessionFactory,SqlSessionHolder),这个SqlSessionHolder中的session的属性值就是DefaultSqlSession。