從大學開始接觸mybatis到如今差很少快3年了吧,最近尋思着使用3年了,我卻還不清楚其內部實現細節,好比:html
它是如何加載各類mybatis相關的xml?面試
它是如何僅僅經過一個Mapper接口 + Mappe.xml實現數據庫操做的(儘管不少人可能都清楚是經過代理實現,但面試時一旦深刻詢問:好比Mapper的代理類名是什麼?是經過JDK仍是cglib實現?)?spring
在同一個方法中,Mybatis屢次請求數據庫,是否要建立多個SqlSession會話?sql
它與Spring是如何適配(整合)的?數據庫
在Spring中是如何保障SqlSession的生命週期的?安全
等等一系列的問題。。。bash
若是以上問題你自認爲沒法回答,或者說了解一些,那麼就從如今開始,咱們來一一揭開這層面紗。session
相信只要用過Mybatis的同窗看到下面的代碼必定不會陌生,若是不清楚的能夠看下官網文檔mybatis
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 一、目前流行方式
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(101);
}
// 二、之前流行方式
try (SqlSession session = sqlSessionFactory.openSession()) {
User user = sqlSession.selectOne("xxx.UserMapper.selectById", "101");;
}
複製代碼
示列代碼演示了Mybatis進行一次數據庫操做的過程,大體分爲(針對目前流行方式,其實之前的使用和目前流行的使用方式實現原理同樣):app
一、 經過 SqlSessionFactoryBuilder 將讀取到的配置資源 build 生成 SqlSessionFactory
二、 經過 SqlSessionFactory 的 openSession() 獲取到 SqlSession
三、 經過 SqlSession 獲取到 Mapper的代理對象(MapperProxy)
四、 經過 Mapper 進行 數據庫請求操做
SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder
咱們能夠輕易的發現每次去請求數據庫操做都須要經過 SqlSessionFactory 去獲取到 SqlSession,而 SqlSessionFactory 是經過 SqlSessionFactoryBuilder 構造出來的, 而且最後請求操做完成後都關閉了SqlSession。所以,不可貴出:
針對這3個類以及mapper的做用域(Scope)和生命週期的描述,我的以爲官方文檔寫得很清楚:
SqlSessionFactoryBuilder
這個類能夠被實例化、使用和丟棄,一旦建立了 SqlSessionFactory,就再也不須要它了。 所以 SqlSessionFactoryBuilder 實例的最佳做用域是方法做用域(也就是局部方法變量)。 你能夠重用 SqlSessionFactoryBuilder 來建立多個 SqlSessionFactory 實例,可是最好仍是不要讓其一直存在,以保證全部的 XML 解析資源能夠被釋放給更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被建立就應該在應用的運行期間一直存在,沒有任何理由丟棄它或從新建立另外一個實例。 使用 SqlSessionFactory 的最佳實踐是在應用運行期間不要重複建立屢次,屢次重建 SqlSessionFactory 被視爲一種代碼「壞味道(bad smell)」。所以 SqlSessionFactory 的最佳做用域是應用做用域。 有不少方法能夠作到,最簡單的就是使用單例模式或者靜態單例模式。
SqlSession
每一個線程都應該有它本身的 SqlSession 實例。SqlSession 的實例不是線程安全的,所以是不能被共享的,因此它的最佳的做用域是請求或方法做用域。 絕對不能將 SqlSession 實例的引用放在一個類的靜態域,甚至一個類的實例變量也不行。 也毫不能將 SqlSession 實例的引用放在任何類型的託管做用域中,好比 Servlet 框架中的 HttpSession。 若是你如今正在使用一種 Web 框架,要考慮 SqlSession 放在一個和 HTTP 請求對象類似的做用域中。 換句話說,每次收到的 HTTP 請求,就能夠打開一個 SqlSession,返回一個響應,就關閉它。 這個關閉操做是很重要的,你應該把這個關閉操做放到 finally 塊中以確保每次都能執行關閉。 依賴注入框架能夠建立線程安全的、基於事務的 SqlSession 和映射器,並將它們直接注入到你的 bean 中,所以能夠直接忽略它們的生命週期。 若是對如何經過依賴注入框架來使用 MyBatis 感興趣,能夠研究一下 MyBatis-Spring 或 MyBatis-Guice 兩個子項目。
映射器實例(Mapper實例) 映射器是一些由你建立的、綁定你映射的語句的接口。映射器接口的實例是從 SqlSession 中得到的。所以從技術層面講,任何映射器實例的最大做用域是和請求它們的 SqlSession 相同的。儘管如此,映射器實例的最佳做用域是方法做用域。 也就是說,映射器實例應該在調用它們的方法中被請求,用過以後便可丟棄。 並不須要顯式地關閉映射器實例,儘管在整個請求做用域保持映射器實例也不會有什麼問題,可是你很快會發現,像 SqlSession 同樣,在這個做用域上管理太多的資源的話會難於控制。 爲了不這種複雜性,最好把映射器放在方法做用域內。就像示列代碼同樣。 若是SqlSession是注入的,那麼映射器實例也可經過依賴注入,而且可忽略其生命週期。
前面是學習mybatis常看到的一種代碼,但缺點也很明顯: 每次請求都得建立SqlSession,而且Mapper的代理類是經過SqlSession獲取(說明耦合度很高),也就意味着每次請求都得建立一個新的Mapper代理類。爲了整合Spring,而且解決前面問題,因此Mybatis-Spring 子項目來襲。
MyBatis-Spring 會幫助你將 MyBatis 代碼無縫地整合到 Spring 中。它將容許 MyBatis 參與到 Spring 的事務管理之中,建立映射器 mapper 和 SqlSession 並注入到 bean中。
上面是 Mybatis-Spring的官方介紹,其中 容許 MyBatis 參與到 Spring 的事務管理之中,建立映射器 mapper 和 SqlSession 並注入到 bean中 是咱們本次解析的關鍵點。那麼開始分析吧!
SqlSessionFactoryBean 、 MapperScannerConfigurer
在Spring項目中應用了Mybatis都會有下面的2個bean配置,這2個配置就是實現xml加載、mapper和SqlSession注入的起始配置。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"></property>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- DAO接口所在包名,Spring會自動查找其下的類 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="xxx.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
複製代碼
從配置中咱們能夠看的 SqlSessionFactoryBean 配置了數據源、mapper的xml路徑、mybatis-config的xml路徑。所以,不難想象,SqlSessionFactoryBean 內部實現了xml配置文件的加載及SqlSessionFactory對象的建立。咱們來看下 SqlSessionFactoryBean繼承關係圖形:
在繼承關係圖中,咱們發現了 InitializingBean、FactoryBean 的身影,可能清楚這個的同窗,大概已經猜到了確定有 afterPropertiesSet() 來建立 SqlSessionFactory 對象 和 getObject() 來獲取 SqlSessionFactory 對象 。 話很少說,先看下getObject()實現:
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
複製代碼
getObject()相對簡單,咱們都知道FactoryBean子類都是經過getObject()來獲取到實際的Bean對象,這裏也就是SqlSessionFactory。從源碼中咱們看到當 sqlSessionFactory爲null會去調用 afterPropertiesSet(),因此 SqlSessionFactory 確定是由 afterPropertiesSet() 來實現建立的。繼續看afterPropertiesSet()實現:
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
this.sqlSessionFactory = buildSqlSessionFactory();
}
複製代碼
afterPropertiesSet() 內部首先 驗證了 dataSource 和 sqlSessionFactoryBuilder 部位null,最後調用 buildSqlSessionFactory()方法獲取到 SqlSessionFactory 對象,並賦值到類字段屬性 sqlSessionFactory 。 繼續查看buildSqlSessionFactory()源碼:
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
// 省略了 SqlSessionFactoryBean 的屬性(好比:ObjectFactory )賦值到 Configuration 對象中的操做
// 1 Configuration : Mybatis的核心類之一,主要存放讀取到的xml數據,包括mapper.xml
Configuration configuration;
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configLocation != null) {
// 2 建立 xmlConfigBuilder 對象 : 用於解析 mybatis-config.xml 數據
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
if (logger.isDebugEnabled()) {
logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
configuration.setVariables(this.configurationProperties);
}
if (xmlConfigBuilder != null) {
try {
// 3 XmlConfigBuilder 解析方法執行
xmlConfigBuilder.parse();
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
// 4 建立 XMLMapperBuilder 對象 : 用於解析 mapper.xml 數據
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
}
}
// 5 經過 SqlSessionFactoryBuilder bulid SqlSessionFactory 對象
return this.sqlSessionFactoryBuilder.build(configuration);
}
複製代碼
整個 buildSqlSessionFactory() 源碼主要有如下幾個重要的點:
一、 XMLConfigBuilder ,經過調用其 parse() 方法來 解析 mybatis-config.xml 配置(若是 配置有 mapper.xml ,其會經過 XMLMapperBuilder 進行解析加載),並將解析的數據賦值到 Configuration(Mybatis的核心類之一,主要存放讀取到的xml數據,包括mapper.xml,該類貫穿整個mybatis,足以見得其重要性)
二、 XMLMapperBuilder : 經過調用其 parse() 方法來 解析 mapper.xml 配置, 並將解析的數據賦值到 Configuration
三、 將存放有解析數據的 Configuration 做爲 sqlSessionFactoryBuilder.build() 參數,建立 sqlSessionFactory 對象。
至此
MapperScannerConfigurer 是 mybatis-spring 項目中爲了實現方便加載Mapper接口,以及將 Mapper 偷樑換柱成 MapperFactoryBean。查看 MapperScannerConfigurer 源碼,先看下其繼承關係圖:
從中咱們其繼承了 BeanDefinitionRegistryPostProcessor 接口,熟悉Spring 的同窗應該 已經大體想到了 其如何將 Mapper 偷樑換柱成 MapperFactoryBean 了。話很少說,咱們來看看 MapperScannerConfigurer 是如何實現 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
複製代碼
咱們能夠發現整個方法內部其實就是經過 ClassPathMapperScanner 的 scan() 方法,查看 scan() 實現,發現其內部調用了關鍵方法 doScan(),那麼咱們來看下 doScan() 方法實現:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 一、調用父類 ClassPathBeanDefinitionScanner的 doScan方法 加載路徑下全部的mapper接口生成對應的 BeanDefinition
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
for (BeanDefinitionHolder holder : beanDefinitions) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 二、 設置 被代理的 Bean(也就是Mapper) 的class信息
definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
// 三、 偷樑換柱成 MapperFactoryBean
definition.setBeanClass(MapperFactoryBean.class);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
// 四、 設置 sqlSessionFactory
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;
}
// 五、 設置 sqlSessionTemplate
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
return beanDefinitions;
}
複製代碼
整個方法分爲3個部分:
一、 調用父類 ClassPathBeanDefinitionScanner的 doScan()方法 加載路徑下全部的mapper接口生成對應的 BeanDefinition
二、 經過definition.setBeanClass(MapperFactoryBean.class) 偷樑換柱成 MapperFactoryBean
三、 經過 definition.getPropertyValues().add() 添加 MapperFactoryBean 所需的 字段或者方法參數信息 : sqlSessionFactory 、 mapperInterface等
至此 MapperScannerConfigurer 的使命已經完成, 至於 MapperFactoryBean 的建立就徹底交給Spring來完成了。
咱們知道在mybatis中,Mapper是經過 SqlSession建立的,而SqlSession的生命週期僅僅在一次會話中,那麼按照這種設計,每一次會話都要去建立SqlSession,而後再經過SqlSession去建立Mapper。咱們知道Mapper其實沒有必要每次都去建立,它更加適合做爲一個單列對象。那麼怎麼將SqlSession和Mapper解耦呢? 在mybatis-spring項目中經過 MapperFactoryBean 、SqlSessionTemplate 來實現的。接下來咱們就來解析它們。
MapperFactoryBean
正如前面咱們所看到的同樣,MapperFactoryBean 其實能夠理解爲 Mapper的代理工廠Bean,咱們能夠經過 MapperFactoryBean 的方法獲取到 Mapper的代理對象。先來看下 MapperFactoryBean繼承關係 :
咱們能夠看到 MapperFactoryBean 實現了 FactoryBean, 那麼 確定經過 實現 getObject() 獲取到 Mapper的代理對象,查看源碼以下:
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
複製代碼
其內部就是咱們熟悉的 getSqlSession().getMapper() 建立Mapper代理對象的方法。熟悉Spring 的同窗都知道 在Bean加載的過程當中若是發現當前Bean對象是 FactoryBean 會去 調用getObject() 獲取真正的Bean對象。不熟悉的同窗能夠去看下 AbstractBeanFactory 的 getBean() 方法。
可是彷佛仍是沒有吧SqlSession和Mapper解耦的跡象呢?不着急,咱們繼續看下 getSqlSession(), 發現其是 父類 SqlSessionDaoSupport 實現,咱們看下SqlSessionDaoSupport源碼:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
// 建立 SqlSession子類 SqlSessionTemplate
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
public SqlSession getSqlSession() {
return this.sqlSession;
}
....
}
複製代碼
咱們發現咱們獲取到的SqlSession實際上是其子類SqlSessionTemplate, 咱們查看其構造方法源碼:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 維護了一個 SqlSession的代理對象
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
複製代碼
咱們能夠清楚的發現,其內部維護了一個 SqlSession的字段 sqlSessionProxy ,其賦值的是代理對象 SqlSessionInterceptor。 咱們再來看下 SqlSessionInterceptor 的源碼:
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 經過getSqlSession() 獲取一個 SqlSession
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
複製代碼
咱們發現其代理實現時,經過getSqlSession() 獲取一個 全新的SqlSession。也就是說建立Mapper的SqlSession和會話請求的SqlSession不是同一個。這裏就完美的解耦了Mapper和SqlSession,而且保障了每次會話SqlSession的生命週期範圍。
這裏超前提下: getSqlSession().getMapper() 其實 是經過 configuration.getMapper() 來獲取的,那麼就意味着 configuration內部必須添加了Mapper信息,那麼configuration是什麼時候添加的呢? 能夠看下 MapperFactoryBean的checkDaoConfig()方法,源碼以下:
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Throwable t) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
throw new IllegalArgumentException(t);
} finally {
ErrorContext.instance().reset();
}
}
}
複製代碼
因爲父類實現了 InitializingBean 接口,而且其afterPropertiesSet() 調用了 checkDaoConfig() 方法 ,因此,至少在初始化建立MapperFactoryBean 時,就已經向 configuration內部必須添加了Mapper信息。
本文解析了Mybatis與Spring是如何整合的,其中的關鍵對象包括:
SqlSessionFactoryBuilder: 用於建立 SqlSessionFactory
SqlSessionFactory: 用於建立 SqlSession
SqlSession: Mybatis工做的最頂層API會話接口,全部訪問數據庫的操做都是經過SqlSession來的
Configuration: 存放有全部的mybatis配置信息,包括mapper.xml、 mybatis-config.xml等
XMLConfigBuilder: 解析 mybatis-config.xml 配置並存放到Configuration中
XMLMapperBuilder: 解析 mapper.xml 配置並存放到Configuration中
SqlSessionFactoryBean: mybatis整合Spring時的 生成 SqlSessionFactory 的FactoryBean
MapperScannerConfigurer: mybatis整合Spring時的 實現方便加載Mapper接口,以及將 Mapper 偷樑換柱成 MapperFactoryBean
MapperFactoryBean: 生成 Mapper 代理對象的FactoryBean
SqlSessionTemplate: 內部維護有 SqlSession 的代理對象,解耦Mapper和SqlSession的關鍵對象。
若是您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!
本文由博客一文多發平臺 OpenWrite 發佈!