SqlSessionFactoryBean
在Mybatis中, 通過SqlSessionFactory創建SqlSession來進行數據持久化操作, 在Spring整合Mybatis中也存在類似對象: SqlSessionFactoryBean
SqlSessionFactoryBean實現了三個接口: FactoryBean, InitializingBean, ApplicationListener
InitializingBean接口: 實現了這個接口, 那麼當bean屬性注入後調用初始化方法前, 會調用該接口的實現類的afterPropertiesSet方法
FactoryBean接口: 實現了該接口的類,在調getBean的時候會返回該工廠返回的實例對象,也就是再調一次getObject方法返回工廠的實例
ApplicationListener接口: 實現了該接口,如果註冊了該監聽的話,那麼就可以了監聽到Spring的一些事件,然後做相應的處理
SqlSessionFactory的初始化是在SqlSessionFactoryBean調用初始化方法前執行afterPropertiesSet()方法時進行創建的, 這個也是要實現InitializingBean接口的原因
SqlSessionFactoryBean#afterPropertiesSet實現:
@Override
public void afterPropertiesSet() throws Exception {
// 屬性校驗
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");
/* 構建SqlSessionFactory */
this.sqlSessionFactory = buildSqlSessionFactory();
}
分析:
在buildSqlSessionFactory()方法中與Mybatis很相似, 都是創建Configuration實例, 然後將相關配置信息封裝到Configuration中, 最後調用SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)方法生成DefaultSqlSessionFactory實例並返回; SqlSessionFactoryBean中維護着DefaultSqlSessionFactory屬性; 在調用Application的getBean()方法時, 由於SqlSessionFactoryBean實現了FactoryBean接口, 所以會調用getObject()返回DefaultSqlSessionFactory實例對象
SqlSessionFactoryBean#getObject實現:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
MapperScannerConfigurer
實現了BeanDefinitionRegistryPostProcessor,負責掃描指定包下的映射接口並向容器中註冊對應的bean。
註冊過程中有一些細節需要提一下,註冊的bean的beanClass並不是映射接口本身,而統一是MapperFactoryBean。同時MapperScannerConfigurer創建時傳入的sqlSessionFactoryBeanName所代表的SqlSessionFactory會設置到這些bean中去。
MapperFactoryBean
一個FactoryBean,負責創建對應映射接口的實現類對象,這個實現類負責完成映射接口的方法和XML定義的SQL語句的映射關係。
Mybatis通過SqlSession接口執行SQL語句,所以MapperFactoryBean會在初始化時通過持有的SqlSessionFactory對象創建一個SqlSessionTemplate(它實現了SqlSession)對象。這個SqlSessionTemplate是mybatis-spring的核心,它給常規的SqlSession賦予了更多的功能,特別是迎合Spring的功能,後面會詳細描述。
我們來看一下MapperFactoryBean是如何創建映射接口的實現類對象的。
既然是FactoryBean,就是通過getObject創建需要的bean對象。跟蹤方法調用,發現最終委託給了Configuration對象中MapperRegistry屬性。上面簡述XML解析過程時已知,MapperRegistry對象的knownMappers屬性保存了映射接口的類對象和一個MapperProxyFactory對象組成的鍵值對。
MapperProxyFactory就是一個代理工廠類,它創建實現類對象的方式就是創建以映射接口爲實現接口、MapperProxy爲InvocationHandler的JDK動態代理。代理的邏輯都在MapperProxy#invoke方法中:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
可以看到,我們想要實現的方法(即排除Object方法和接口的默認方法),都委託給了對應的MapperMethod去實現。方法第一次調用時,新建MapperMethod,然後放入緩存。MapperMethod包含了兩個內部類屬性:
- SqlCommand:負責關聯SQL命令。根據接口名和方法名從Configuration對象的mappedStatements中檢查並獲取方法對應的SQL語句解析成的MappedStatement對象,保存它的id和SQL命令類型。
- MethodSignature:負責解析和保存方法簽名信息。解析方法的參數和返回類型,保存解析後的信息。
獲取MapperMethod後就是調用它的execute方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
方法根據SQL命令類型的不同進行不同的操作,一樣的地方是都會先把方法參數轉化爲SQL參數形式,然後執行傳進execute方法的SqlSession對象(即MapperFactoryBean對象持有的SqlSessionTemplate對象)的對應的方法。
總結下MapperScannerConfigurer和MapperFactoryBean的作用:MapperScannerConfigurer負責把配置路徑下的映射接口註冊爲Spring容器的MapperFactoryBean類型的bean。這個工廠bean通過代理方式創建對應映射接口的實現類對象。實現類攔截映射接口的自定義方法,讓SqlSessionTemplate去處理方法對應的SQL解析成的MappedStatement。
SqlSessionTemplate
實現了SqlSession,但和SqlSession默認實現類DefaultSqlSession不同的是,它是線程安全的,這意味着一個SqlSessionTemplate實例可以在多個Dao之間共享;它和Spring的事務管理緊密關聯,可以實現多線程下各個事務之間的相互隔離;另外,它會把Mybatis返回的異常轉化爲Spring的DataAccessException。下面我們來探究它是如何做到這幾點的。
SqlSessionTemplate在初始化時會通過JDK動態代理的方式創建一個實現SqlSession、以SqlSessionInterceptor爲InvocationHandler的代理對象,SqlSessionTemplate的大多數方法調用都轉發給這個代理。攔截的邏輯在SqlSessionInterceptor#invoke中:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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);
}
}
}
}
首先獲取真正用來工作的SqlSession,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;
}
這裏包含了與Spring事務關聯的邏輯。先嚐試從事務同步管理類中獲取傳入的SqlSessionFactory對象在當前線程綁定的SqlSessionHolder對象,如果存在就直接返回SqlSessionHolder對象持有的SqlSession對象,否則就用SqlSessionFactory創建一個新的SqlSession,調用DefaultSqlSessionFactory#openSessionFromDataSource,level默認是null,autoCommit默認false:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
可以看到最終創建了一個DefaultSqlSession對象,這裏需要注意的一點是,這裏創建了Transaction和Executor,在繼續往底層探索時會再提及到。
創建完之後,會根據當前線程是否存在Spring事務而選擇是否封裝成SqlSessionHolder放入事務同步管理類,這樣以來,同線程同事務下對映射接口的調用,實際工作的都是同一個SqlSession。
我們回到SqlSessionInterceptor,獲取到實際工作的DefaultSqlSession會去執行當前攔截的方法(具體我們稍後探究),如果拋出Mybatis的PersistenceException異常,初始化時設置的PersistenceExceptionTranslator對象(默認是MyBatisExceptionTranslator對象)會對異常進行轉化爲DataAccessException。
總結下SqlSessionTemplate的作用,它通過動態代理對方法進行攔截,然後根據當前Spring事務狀態獲取或創建SqlSession來進行實際的工作。
DefaultSqlSession
我們現在知道SqlSessionTemplate最終還是依賴一個DefaultSqlSession對象去處理映射接口方法對應的MappedStatement。下面我們以selectList方法爲例探究具體的處理過程:
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();
}
}
首先從configuration中獲取到MappedStatement對象,然後讓Executor對象調用query方法。
Executor
Executor是Mybatis的執行器,負責SQL語句的生成和查詢緩存的維護。
前面在創建DefaultSqlSession的時候,會先讓configuration創建一個Executor,根據配置的ExecutorType選擇具體的Executor實現,默認是SimpleExecutor,然後如果配置緩存開啓(默認開啓),則還要封裝成CachingExecutor。
CachingExecutor的query方法會先從MappedStatement對象動態生成sql語句,和參數一起封裝在BoundSql對象中;再根據sql、參數和返回映射等信息創建一個緩存鍵;然後檢查XML裏有沒有配置二級緩存,有的話就用緩存鍵去查找,否則就執行它代理的Executor對象的query方法,先用緩存鍵去一級緩存也叫本地緩存中去查找,如果沒有的話就執行doQuery方法。不同Executor實現的doQuery有所不同,但核心都是創建一個StatementHandler,然後通過它對底層JDBC Statement進行操作,最後對查詢的結果集進行轉化。
限於篇幅,就不繼續探究StatementHandler及更底層的操作了,就再看下Mybatis是怎麼管理數據庫連接的。
Transaction
先回顧下這個Transaction對象是怎麼來的:前面創建實際工作的DefaultSqlSession時會讓TransactionFactory對象創建一個Transactio對象作爲Executor對象的屬性。而這個TransactionFactory對象,如何沒有指定的話,默認是SpringManagedTransactionFactory對象。它接受一個DataSource創建SpringManagedTransaction,可以看到這裏把事務隔離級別和是否自動提交兩個參數都忽略了,那是因爲mybatis-spring把事務都交給Spring去管理了。
Executor在執行doQuery方法,創建JDBC Statement對象時需要先獲取到數據庫連接:
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
繼續看到SpringManagedTransaction,它的Connection是通過DataSourceUtils調用getConnection方法獲取的,核心邏輯在doGetConnection方法中:
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(con, dataSource);
throw ex;
}
}
return con;
}
可以看到,Spring的事務管理器不僅保存了事務環境下當前線程的SqlSession,還以dataSource爲鍵保存了Connection。如果從事務管理器沒有獲取到,就需要通過從SpringManagedTransaction傳遞過來的dataSource獲取Connection對象,獲取到之後判斷當前是否在事務環境,是的話就把Connection對象封裝成ConnectionHolder保存在事務管理器中,這樣的話就能保證一個事務中的數據庫連接是同一個。
spring整合mybatis後,mybatis一級緩存失效的原因參考播客: spring整合mybatis後,mybatis一級緩存失效的原因