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代碼地址