Mybatis源码分析06-MyBatis总结
前言
前面我们分析了mybatis的源码以及手写了两个版本的微型mybatis,相信大家对mybatis源码一定有了更深入的了解了。
分享一句名言:学而不思则罔,思而不学则殆!
这节我们来思考一些问题。以及对mybatis集成spring的jar包做一个分析!
mybatis源码回顾
分析mybatis源码的过程中,我拿出几个问题,不妨一起思考一下?
- org.apache.ibatis.binding.MapperProxy#invoke 这个类的53行什么时候执行?
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return ((method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
&& method.getDeclaringClass().isInterface();
}
判断一下当前类是接口,并且,类的修饰符可以是abstract或public或static~
- TestMapper 作者为什么要设计这样的形式来做?为什么不是一个class而是一个interface?
public interface TestMapper {
Test selectByPrimaryKey(Integer userId);
}
因为这个类要来做代理,只需要获取其中的方法名称,方法参数即可,不需要实现类来做多余的事,并且在spring注入的时候也是接口。
- org.apache.ibatis.executor.BaseExecutor#queryFromDatabase 322行这行代码的意义
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);//322
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
查找一下这个值在哪里put的?
ctrl+h搜索一下EXECUTION_PLACEHOLDER:
看canload()在哪里引用的?
这里还是调用,我们再看重写的这个方法deferLoad
看看CachingExecutor:
既然这里没有put,我们看一下DefaultResultSetHandler:
找到了!
这里是判断了一下是否是有缓存,有缓存,则调用deferLoad方法赋值
回到322行源码,我们再看看这里的逻辑,key不能为空,并且,当前的key对象的value不能是这个占位符,才可以load():
占位符:
什么情况下存在占位符呢?就是doQuery()执行的过程中,比如执行了100s,在这个过程中,告诉别人说,当前这个key的状态是: i’m basy! 这样就有效的防止并发~
怎么理解这种解决并发的策略呢?举个栗子:
双十一,大家在抢单同一样商品,如果这件商品没有做热备(热加载)的话,所有的并发请求都要直接走数据库了,数据库毫无疑问会崩溃!
所以,我们可以采取占位符的方式:
比如用户一最先访问到product1:
那么我们在Cache里put(pid=1,placeholder),表示product1正在被操作
如果此时有用户二访问product1:
则首先判断get(pid=1) ==placeholder
有占位符则表示此商品正在被操作,其他用户必须排队等待了!
通过占位符防止高并发的这种方式可以说是简单粗暴!
如果有10万的dps,那么通过此举可以阻止99999的请求,只允许一个请求访问数据库!
题外:限流策略还有很多,比如查询到占位符,sleep(100)或可以使用布隆过滤
- lazy loading是怎么做到的?
不妨看源码org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject():
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
final List<Object> constructorArgs = new ArrayList<Object>();
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = (resultObject != null && !constructorArgTypes.isEmpty()); // set current mapping result
return resultObject;
}
懒加载:如果当前的ResultMapping的下一个id不为空,并且是懒加载,那么使用代理创建的Object。
之前章节我们讲到过这个配置。两种代理模式,一种是cglib代理,另一种是jdk代理。
- plugin功能如何实现?
整个过程四个重要的类我们需要关注:
- InterceptorChain
- Plugin
- Invocation
- Interceptor
简单的实现Plugin:
a) 先实现Interceptor 接口
@Intercepts(value={
@Signature(
type = Executor.class, // 只能是: StatementHandler | ParameterHandler | ResultSetHandler | Executor 类或者子类
method = "query", // 表示:拦截Executor的query方法
args = { // query 有很多的重载方法,需要通过方法签名来指定具体拦截的是那个方法
MappedStatement.class,
Object.class,
RowBounds.class,
ResultHandler.class
}
/**
* type:标记需要拦截的类
* method: 标记是拦截类的那个方法
* args: 标记拦截类方法的具体那个引用(尤其是重载时)
*/
)})
public class TestPlugin implements Interceptor {
/**
* 具体拦截的实现逻辑
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("----------- intercept query start.... ---------");
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
System.out.println(String.format("plugin output sql = %s , params = %s",boundSql.getSql(),boundSql));
// 调用方法,实际上就是拦截的方法
Object result = invocation.proceed();
System.out.println("----------- intercept query end.... ---------");
return invocation.proceed();
}
/**
* 插入插件
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
// 调用Plugin工具类,创建当前的类的代理类
return Plugin.wrap(target, this);
}
/**
* 设置插件属性
* @param properties
*/
@Override
public void setProperties(Properties properties) {
}
}
b) 然后在配置文件里引入即可:
看官网上plugins的介绍:
只有在初始化到以上四个类的时候会plugin ,这也就确定了@Signature的type字段。
我们在debug一下之前我们访问通过mybatis访问数据库的demo,发现容器启动,就加载plugins:
然后是pluginAll(处理多个plugin):
org.apache.ibatis.plugin#Plugin:
在Incocation被invoke的同时又被invoke:
整个过程类似于aop的动态代理:
mybatis-spring.jar
spring与mybatis整合(通过@Configuration将mybatis的类注册到spring)
思路:用mybatis programming(编程式)->managed(集成式)
怎么集成?
- xml
- annotation
好的,我们依照惯例,打开源码:
这里annotation包就是以注解的形式整合
这里的mapper包就是以编程式形式整合
我们先看annotation形式:
MapperScannerRegistrar实现于ImportBeanDefinitionRegistrar接口
ImportBeanDefinitionRegistrar是spring对暴露对外的框架,整合以注解形式的所有bean。
我们看代码的59行,就是先解析到org.mybatis.spring.annotation#MapperScan
然后将MapperScan的所有注解扫描,进行匹配
下图是spring的ImportBeanDefinitionRegistrar接口:
通过以上我们了解:
spring容器在处理@Configuration的时候会去调用第三方的实现类MapperScannerRegistrar调用registerBeanDefinitions去把mybatis的所有需要的类注册进来。
那么,我们同样可以自定义这个实现类!
题外话:
注册bean的时候,可以注册接口,spring就可以自动将所有的实现类全部注入!
题外话结束~!
回到正题:
mybatis的dao层的多个mapper被扫描到,怎么知道当前的要扫描的类就是Mapper.java?
org.mybatis.spring.mapper#isCandidateComponent
这里就是判断是否为接口,是否是独立的类
我们先看xml形式,实际上与annotation的形式类似:
那么是sqlsession集成方式?
我们说编程式与当前spring-mybatis集成最大的区别就在于:sqlsession的实现方式不同,一个是SqlSessionTemplate,另一个是DefaultSqlSession!
我们看debug,从这里开始不同,直接进入SqlSessionTemplate
SqlSessionTemplate这里做了一个代理:
代理做了什么事情呢?
我们看invoke方法:
在这个SqlSessionTemplate里,我们看这里是获取了sqlsession,而不是我们在demo里手动调用,这也就是为什么我们使用ssm框架,直接用mapper的方法就可以了,sqlsession其实是在双层代理的invoke里实现的!
再看getSqlsession:
看一下getSqlSession()方法的实现,在template下面,这个util里有三个方法,一个是获取sqlsession,一个是关闭sqlsession,另一个是注册SessionHolder:
这里注意一个问题:
之前我们总结Mapper的生命周期是method,但是现在为什么是单例(容器级别)呢?
mapper的作用是什么?
找sql
总结
代码地址:本节github代码地址