開博也快三年還沒正經的寫過一篇文章,總是隨便的記些隨筆。今天抽空就將最近解決的mybatis多數據源自動切換記錄一下思路。有不合理的還望各位大蝦多多指正。
最近公司將以前在一個庫中的N多表,分庫存儲。每個庫由專門負責該塊業務的研發人員負責維護讀取。這下可就害苦了我這個搞測試的了。自動化腳本都是在一個流程中要訪問多個表的,這樣分了我一個流程要切換幾個庫,才能訪問到這些表。
既然問題出來了,那么接下來就是怎么解決了,苦思冥想后,各種度娘。
首先發現的解決方案一:通過在配置中配置需要的多個DataSource,並繼承AbstractRoutingDataSource,實現手動切換。相關的具體實現網上很多。可參考:http://www.cnblogs.com/lzrabbit/p/3750803.html,嘗試下來后發現同一個線程未執行過數據調用時切換有效,一旦在執行過相應數據調用后在切換到別的數據源,此時切換就無效。實驗代碼如下:

切換到qa時能成功,但在切換回dev時無效。因此該方案無效。
接下來繼續各種度娘,猛然發現mybatis-plugins,居然可以給mybatis添加攔截器,那是不是可以在mybatis執行連接數據庫前攔截下來修改datasource。mybatis-plugins只提供了對四個接口類的攔截,分別是
- Executor (update, query, flushStatements, commit, rollback,
getTransaction, close, isClosed) - ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler(prepare, parameterize, batch, update, query)
一個一個看下來,最有機會的就是StatementHandler的prepare進行攔截,查看該prepare方法的源碼發現它接收的是connect ,既然connect都生成了,那我修改DataSource還有什么用呢,這個也不行。mybatis-plugins可參考:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins
接下來只能繼續度娘了,突然靈光一閃,spring不是有aop嗎,我能不能對SqlSessionFactory進行aop,修改opensession不就完美了。說着就干,各種配置,完了已測試,發現aop完全沒進去。后來一想,aop是spring對bean才能有效,SqlSessionFactory不是通過bean產生的,怎么可能有效呢。無奈放棄。
繼續度娘海里掏針的尋覓,就在我回眸的一剎那,這篇博客http://blog.csdn.net/zl3450341/article/details/20150687讓我恍然大悟。立馬翻看我的配置,配置如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:qacontext/framework/jdbc/ibaits/sqlmap-config.xml" /> <property name="mapperLocations" value="classpath:com/nonobank/mapping/**/*.xml" /> </bean> <!-- 配置掃描器 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 掃描me.gacl.dao這個包以及它的子包下的所有映射接口類 --> <property name="basePackage" value="com.nonobank.dao" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean>
我的里面沒有SqlSessionTemplate的配置啊,那我怎么重寫SqlSessionTemplate呢。沒有,那就只能看看MapperScannerConfigurer中能不能指定SqlSessionTemplate了,一看發現MapperScannerConfigurer中有個sqlSessionTemplateBeanName屬性,又是一番度娘,參考:http://www.cnblogs.com/ClassNotFoundException/p/6425558.html最后一句話。原來sqlSessionTemplateBeanName和sqlSessionFactoryBeanName差不多的,那還等什么,立即將配置sqlSessionFactoryBeanName替換掉。配置文件就變成了如下:
<bean id="sqlSessionFactoryQa" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 實例化sqlSessionFactory時需要使用上述配置好的數據源以及SQL映射文件 --> <property name="dataSource" ref="dataSourceQa" /> <!-- 自動掃描me/gacl/mapping/目錄下的所有SQL映射的xml文件, 省掉Configuration.xml里的手工配置 value="classpath:me/gacl/mapping/*.xml"指的是classpath(類路徑)下me.gacl.mapping包中的所有xml文件 UserMapper.xml位於me.gacl.mapping包下,這樣UserMapper.xml就可以被自動掃描 --> <property name="configLocation" value="classpath:qacontext/framework/jdbc/ibaits/sqlmap-config.xml" /> <property name="mapperLocations" value="classpath:com/nonobank/mapping/**/*.xml" /> </bean> <bean id="sqlSessionFactoryDev" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSourceDev" /> <property name="configLocation" value="classpath:qacontext/framework/jdbc/ibaits/sqlmap-config.xml" /> <property name="mapperLocations" value="classpath:com/nonobank/mapping/**/*.xml" /> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> <property name="targetSqlSessionFactory"> <map> <entry value-ref="sqlSessionFactoryQa" key="qa" /> <entry value-ref="sqlSessionFactoryDev" key="dev" /> </map> </property> </bean> <!-- 配置掃描器 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 掃描me.gacl.dao這個包以及它的子包下的所有映射接口類 --> <property name="basePackage" value="com.nonobank.dao" /> <!--<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> --> <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate" /> </bean>
現在終於有SqlSessionTemplate可以給我重寫了。重寫完后的代碼如下:
package com.yinting.mybatis; /** * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import static java.lang.reflect.Proxy.newProxyInstance; import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable; import static org.mybatis.spring.SqlSessionUtils.closeSqlSession; import static org.mybatis.spring.SqlSessionUtils.getSqlSession; import static org.mybatis.spring.SqlSessionUtils.isSqlSessionTransactional; import static org.springframework.util.Assert.notNull; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Properties; import javax.sql.DataSource; import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.exceptions.PersistenceException; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.MyBatisExceptionTranslator; import org.springframework.beans.factory.DisposableBean; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.yinting.jdbc.DbHandller; import com.zaxxer.hikari.HikariDataSource; /** * Thread safe, Spring managed, {@code SqlSession} that works with Spring * transaction management to ensure that that the actual SqlSession used is the * one associated with the current Spring transaction. In addition, it manages * the session life-cycle, including closing, committing or rolling back the * session as necessary based on the Spring transaction configuration. * <p> * The template needs a SqlSessionFactory to create SqlSessions, passed as a * constructor argument. It also can be constructed indicating the executor type * to be used, if not, the default executor type, defined in the session factory * will be used. * <p> * This template converts MyBatis PersistenceExceptions into unchecked * DataAccessExceptions, using, by default, a * {@code MyBatisExceptionTranslator}. * <p> * Because SqlSessionTemplate is thread safe, a single instance can be shared by * all DAOs; there should also be a small memory savings by doing this. This * pattern can be used in Spring configuration files as follows: * * <pre class="code"> * {@code * <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> * <constructor-arg ref="sqlSessionFactory" /> * </bean> * } * </pre> * * @author Putthibong Boonbong * @author Hunter Presnall * @author Eduardo Macarron * * @see SqlSessionFactory * @see MyBatisExceptionTranslator */ public class SqlSessionTemplate extends org.mybatis.spring.SqlSessionTemplate { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();//保存當前線程上一次切換DataSource關鍵字 private final SqlSessionFactory sqlSessionFactory; private final ExecutorType executorType; private final SqlSession sqlSessionProxy; private final PersistenceExceptionTranslator exceptionTranslator; private Map<Object, SqlSessionFactoryBean> targetSqlSessionFactory; public void setTargetSqlSessionFactory(Map<Object, SqlSessionFactoryBean> targetSqlSessionFactory) { this.targetSqlSessionFactory = targetSqlSessionFactory; } /** * Constructs a Spring managed SqlSession with the {@code SqlSessionFactory} * provided as an argument. * * @param sqlSessionFactory */ public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); } /** * Constructs a Spring managed SqlSession with the {@code SqlSessionFactory} * provided as an argument and the given {@code ExecutorType} * {@code ExecutorType} cannot be changed once the * {@code SqlSessionTemplate} is constructed. * * @param sqlSessionFactory * @param executorType */ public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator( sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true)); } /** * Constructs a Spring managed {@code SqlSession} with the given * {@code SqlSessionFactory} and {@code ExecutorType}. A custom * {@code SQLExceptionTranslator} can be provided as an argument so any * {@code PersistenceException} thrown by MyBatis can be custom translated * to a {@code RuntimeException} The {@code SQLExceptionTranslator} can also * be null and thus no exception translation will be done and MyBatis * exceptions will be thrown * * @param sqlSessionFactory * @param executorType * @param exceptionTranslator */ public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { super(sqlSessionFactory, executorType, exceptionTranslator); notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); } private DataSource newDatasource(String url, String username, String password) { HikariDataSource dataSource = null; dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setConnectionTestQuery("SELECT 1"); dataSource.setConnectionTimeout(30000); dataSource.setIdleTimeout(60000); dataSource.setMaxLifetime(1800000); dataSource.setMaximumPoolSize(10); dataSource.setMinimumIdle(1); return dataSource; } private DataSource ruleTransform(String classInfo) { String env = System.getProperty("ENV"); String dataSourceKey = SqlSessionContextHolder.getDataSourceKey(); if (env.equals("dev")) { Properties pro = new Properties(); String[] claNames = classInfo.split("\\."); String claName = claNames[claNames.length - 2];// 類名 String key = claName.split("_")[0];// 獲取關鍵字 if (dataSourceKey != null) {//如果dataSourceKey不為空直接使用作為key值 key = dataSourceKey; } else { try {// key.properties 中存在該key對應的value時使用配置,不存在使用key本身 pro.load(new FileInputStream( new File("src/main/java/properties/framework/" + env + "/key.properties"))); String keyValue = pro.getProperty(key + ".key"); if (keyValue != null) { key = keyValue; } } catch (FileNotFoundException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } if(contextHolder.get()==null||!contextHolder.get().equals(key)){ try { pro.load(new FileInputStream("src/main/java/properties/framework/" + env + "/jdbc.properties")); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } String url = MessageFormat.format(pro.getProperty("hyper." + env + ".mysql.url"), key); String username = MessageFormat.format(pro.getProperty("hyper." + env + ".mysql.username"), key); String password = MessageFormat.format(pro.getProperty("hyper." + env + ".mysql.password"), key); contextHolder.set(key); return newDatasource(url, username, password); } } return null; } public SqlSessionFactory getSqlSessionFactory(String classInfo) { SqlSessionFactoryBean sqlSessionFactoryBean = this.targetSqlSessionFactory.get(System.getProperty("ENV"));// 獲取SqlSessionFactoryBean DataSource dataSource = ruleTransform(classInfo); if (dataSource != null) { sqlSessionFactoryBean.setDataSource(dataSource); } SqlSessionFactory targetSqlSessionFactory = null; try { targetSqlSessionFactory = sqlSessionFactoryBean.getSqlSessionFactory(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } if (targetSqlSessionFactory != null) { return targetSqlSessionFactory; } else if (this.sqlSessionFactory != null) { return this.sqlSessionFactory; } throw new IllegalArgumentException("sqlSessionFactory or targetSqlSessionFactory must set one at least"); } public ExecutorType getExecutorType() { return this.executorType; } public PersistenceExceptionTranslator getPersistenceExceptionTranslator() { return this.exceptionTranslator; } /** * {@inheritDoc} */ @Override public <T> T selectOne(String statement) { return this.sqlSessionProxy.<T>selectOne(statement); } /** * {@inheritDoc} */ @Override public <T> T selectOne(String statement, Object parameter) { return this.sqlSessionProxy.<T>selectOne(statement, parameter); } /** * {@inheritDoc} */ @Override public <K, V> Map<K, V> selectMap(String statement, String mapKey) { return this.sqlSessionProxy.<K, V>selectMap(statement, mapKey); } /** * {@inheritDoc} */ @Override public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) { return this.sqlSessionProxy.<K, V>selectMap(statement, parameter, mapKey); } /** * {@inheritDoc} */ @Override public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) { return this.sqlSessionProxy.<K, V>selectMap(statement, parameter, mapKey, rowBounds); } /** * {@inheritDoc} */ @Override public <T> Cursor<T> selectCursor(String statement) { return this.sqlSessionProxy.selectCursor(statement); } /** * {@inheritDoc} */ @Override public <T> Cursor<T> selectCursor(String statement, Object parameter) { return this.sqlSessionProxy.selectCursor(statement, parameter); } /** * {@inheritDoc} */ @Override public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) { return this.sqlSessionProxy.selectCursor(statement, parameter, rowBounds); } /** * {@inheritDoc} */ @Override public <E> List<E> selectList(String statement) { return this.sqlSessionProxy.<E>selectList(statement); } /** * {@inheritDoc} */ @Override public <E> List<E> selectList(String statement, Object parameter) { return this.sqlSessionProxy.<E>selectList(statement, parameter); } /** * {@inheritDoc} */ @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { return this.sqlSessionProxy.<E>selectList(statement, parameter, rowBounds); } /** * {@inheritDoc} */ @Override public void select(String statement, ResultHandler handler) { this.sqlSessionProxy.select(statement, handler); } /** * {@inheritDoc} */ @Override public void select(String statement, Object parameter, ResultHandler handler) { this.sqlSessionProxy.select(statement, parameter, handler); } /** * {@inheritDoc} */ @Override public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { this.sqlSessionProxy.select(statement, parameter, rowBounds, handler); } /** * {@inheritDoc} */ @Override public int insert(String statement) { return this.sqlSessionProxy.insert(statement); } /** * {@inheritDoc} */ @Override public int insert(String statement, Object parameter) { return this.sqlSessionProxy.insert(statement, parameter); } /** * {@inheritDoc} */ @Override public int update(String statement) { return this.sqlSessionProxy.update(statement); } /** * {@inheritDoc} */ @Override public int update(String statement, Object parameter) { return this.sqlSessionProxy.update(statement, parameter); } /** * {@inheritDoc} */ @Override public int delete(String statement) { return this.sqlSessionProxy.delete(statement); } /** * {@inheritDoc} */ @Override public int delete(String statement, Object parameter) { return this.sqlSessionProxy.delete(statement, parameter); } /** * {@inheritDoc} */ @Override public <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this); } /** * {@inheritDoc} */ @Override public void commit() { throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); } /** * {@inheritDoc} */ @Override public void commit(boolean force) { throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); } /** * {@inheritDoc} */ @Override public void rollback() { throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); } /** * {@inheritDoc} */ @Override public void rollback(boolean force) { throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); } /** * {@inheritDoc} */ @Override public void close() { throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession"); } /** * {@inheritDoc} */ @Override public void clearCache() { this.sqlSessionProxy.clearCache(); } /** * {@inheritDoc} * */ @Override public Configuration getConfiguration() { return this.sqlSessionFactory.getConfiguration(); } /** * {@inheritDoc} */ @Override public Connection getConnection() { return this.sqlSessionProxy.getConnection(); } /** * {@inheritDoc} * * @since 1.0.2 * */ @Override public List<BatchResult> flushStatements() { return this.sqlSessionProxy.flushStatements(); } /** * Allow gently dispose bean: * * <pre> * {@code * * <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> * <constructor-arg index="0" ref="sqlSessionFactory" /> * </bean> * } * </pre> * * The implementation of {@link DisposableBean} forces spring context to use * {@link DisposableBean#destroy()} method instead of * {@link SqlSessionTemplate#close()} to shutdown gently. * * @see SqlSessionTemplate#close() * @see org.springframework.beans.factory.support.DisposableBeanAdapter#inferDestroyMethodIfNecessary * @see org.springframework.beans.factory.support.DisposableBeanAdapter#CLOSE_METHOD_NAME */ @Override public void destroy() throws Exception { // This method forces spring disposer to avoid call of // SqlSessionTemplate.close() which gives UnsupportedOperationException } /** * Proxy needed to route MyBatis method calls to the proper SqlSession got * from Spring's Transaction Manager It also unwraps exceptions thrown by * {@code Method#invoke(Object, Object...)} to pass a * {@code PersistenceException} to the * {@code PersistenceExceptionTranslator}. */ private class SqlSessionInterceptor implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.getSqlSessionFactory(args[0].toString()), 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); } } } } }
標紅部分為修改部分,首先參考的博主是private Map<Object, SqlSessionFactory> targetSqlSessionFactory;而我的是private Map<Object, SqlSessionFactoryBean> targetSqlSessionFactory;,這是由於我是需要動態新增DataSource,而不是把所有DataSource先配好,然后去切換。而SqlSessionFactory並沒有可以修改DataSource的方法提供,但是在SqlSessionFactoryBean提供了setDataSource方法,因此我就可以自己生成DataSource並賦值進去。
其二SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.getSqlSessionFactory(args[0].toString()),參考博主里面並沒有給getSqlSessionFactory傳參,這個主要是我需要根據類名規則生成DataSource,因此需要獲取類名信息,因此args[0].toString()就是獲取類名信息並傳給getSqlSessionFactory。
其次新增了兩個方法ruleTransform,newDatasource,getSqlSessionFactory調用ruleTransform並傳遞類信息進入,ruleTransform將根據規則選擇生成何種DataSource。
一切准備就緒開始測試,一運行,一堆錯誤。有點蒙。具體什么錯誤但是沒保存,就不展示,大體意思就是生成的sqlSessionFactory賦值給SqlSessionFactoryBean錯誤,格式化失敗。我的乖乖,我就改過private Map<Object, SqlSessionFactory> targetSqlSessionFactory; 這里的類型,肯定是這里有問題啦。
細細一想發現有點問題,我在配置中配置的是SqlSessionFactoryBean,為什么接收的是SqlSessionFactory。這個沒有道理啊,繼續開啟度娘模式。原因可參考:http://blog.csdn.net/wdyr321/article/details/11763979,簡單就是,spring匹配上bean后判斷bean繼承了FactoryBean,就會調用FactoryBean中的getObject獲取實例,賦值給代碼中的變量。而我配置的SqlSessionFactoryBean變成了SqlSessionFactory就是getObject 這個方法搞的鬼。怎么辦,那還等什么誰擋我我就重寫誰。重寫SqlSessionFactoryBean。代碼如下:
package com.yinting.mybatis; /** * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import static org.springframework.util.Assert.notNull; import static org.springframework.util.Assert.state; import static org.springframework.util.ObjectUtils.isEmpty; import static org.springframework.util.StringUtils.hasLength; import static org.springframework.util.StringUtils.tokenizeToStringArray; import java.io.IOException; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource; import org.apache.ibatis.builder.xml.XMLConfigBuilder; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.cache.Cache; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.io.VFS; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.TransactionFactory; import org.apache.ibatis.type.TypeHandler; import org.mybatis.spring.transaction.SpringManagedTransactionFactory; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.NestedIOException; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; /** * {@code FactoryBean} that creates an MyBatis {@code SqlSessionFactory}. * This is the usual way to set up a shared MyBatis {@code SqlSessionFactory} in a Spring application context; * the SqlSessionFactory can then be passed to MyBatis-based DAOs via dependency injection. * * Either {@code DataSourceTransactionManager} or {@code JtaTransactionManager} can be used for transaction * demarcation in combination with a {@code SqlSessionFactory}. JTA should be used for transactions * which span multiple databases or when container managed transactions (CMT) are being used. * * @author Putthibong Boonbong * @author Hunter Presnall * @author Eduardo Macarron * @author Eddú Meléndez * @author Kazuki Shimizu * * @see #setConfigLocation * @see #setDataSource */ public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactoryBean>, InitializingBean, ApplicationListener<ApplicationEvent>{ private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class); private Resource configLocation; private Configuration configuration; private Resource[] mapperLocations; private DataSource dataSource; private TransactionFactory transactionFactory; private Properties configurationProperties; private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); private SqlSessionFactory sqlSessionFactory; //EnvironmentAware requires spring 3.1 private String environment = SqlSessionFactoryBean.class.getSimpleName(); private boolean failFast; private Interceptor[] plugins; private TypeHandler<?>[] typeHandlers; private String typeHandlersPackage; private Class<?>[] typeAliases; private String typeAliasesPackage; private Class<?> typeAliasesSuperType; //issue #19. No default provider. private DatabaseIdProvider databaseIdProvider; private Class<? extends VFS> vfs; private Cache cache; private ObjectFactory objectFactory; private ObjectWrapperFactory objectWrapperFactory; /** * Sets the ObjectFactory. * * @since 1.1.2 * @param objectFactory */ public void setObjectFactory(ObjectFactory objectFactory) { this.objectFactory = objectFactory; } /** * Sets the ObjectWrapperFactory. * * @since 1.1.2 * @param objectWrapperFactory */ public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) { this.objectWrapperFactory = objectWrapperFactory; } /** * Gets the DatabaseIdProvider * * @since 1.1.0 * @return */ public DatabaseIdProvider getDatabaseIdProvider() { return databaseIdProvider; } /** * Sets the DatabaseIdProvider. * As of version 1.2.2 this variable is not initialized by default. * * @since 1.1.0 * @param databaseIdProvider */ public void setDatabaseIdProvider(DatabaseIdProvider databaseIdProvider) { this.databaseIdProvider = databaseIdProvider; } public Class<? extends VFS> getVfs() { return this.vfs; } public void setVfs(Class<? extends VFS> vfs) { this.vfs = vfs; } public Cache getCache() { return this.cache; } public void setCache(Cache cache) { this.cache = cache; } /** * Mybatis plugin list. * * @since 1.0.1 * * @param plugins list of plugins * */ public void setPlugins(Interceptor[] plugins) { this.plugins = plugins; } /** * Packages to search for type aliases. * * @since 1.0.1 * * @param typeAliasesPackage package to scan for domain objects * */ public void setTypeAliasesPackage(String typeAliasesPackage) { this.typeAliasesPackage = typeAliasesPackage; } /** * Super class which domain objects have to extend to have a type alias created. * No effect if there is no package to scan configured. * * @since 1.1.2 * * @param typeAliasesSuperType super class for domain objects * */ public void setTypeAliasesSuperType(Class<?> typeAliasesSuperType) { this.typeAliasesSuperType = typeAliasesSuperType; } /** * Packages to search for type handlers. * * @since 1.0.1 * * @param typeHandlersPackage package to scan for type handlers * */ public void setTypeHandlersPackage(String typeHandlersPackage) { this.typeHandlersPackage = typeHandlersPackage; } /** * Set type handlers. They must be annotated with {@code MappedTypes} and optionally with {@code MappedJdbcTypes} * * @since 1.0.1 * * @param typeHandlers Type handler list */ public void setTypeHandlers(TypeHandler<?>[] typeHandlers) { this.typeHandlers = typeHandlers; } /** * List of type aliases to register. They can be annotated with {@code Alias} * * @since 1.0.1 * * @param typeAliases Type aliases list */ public void setTypeAliases(Class<?>[] typeAliases) { this.typeAliases = typeAliases; } /** * If true, a final check is done on Configuration to assure that all mapped * statements are fully loaded and there is no one still pending to resolve * includes. Defaults to false. * * @since 1.0.1 * * @param failFast enable failFast */ public void setFailFast(boolean failFast) { this.failFast = failFast; } /** * Set the location of the MyBatis {@code SqlSessionFactory} config file. A typical value is * "WEB-INF/mybatis-configuration.xml". */ public void setConfigLocation(Resource configLocation) { this.configLocation = configLocation; } /** * Set a customized MyBatis configuration. * @param configuration MyBatis configuration * @since 1.3.0 */ public void setConfiguration(Configuration configuration) { this.configuration = configuration; } /** * Set locations of MyBatis mapper files that are going to be merged into the {@code SqlSessionFactory} * configuration at runtime. * * This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file. * This property being based on Spring's resource abstraction also allows for specifying * resource patterns here: e.g. "classpath*:sqlmap/*-mapper.xml". */ public void setMapperLocations(Resource[] mapperLocations) { this.mapperLocations = mapperLocations; } /** * Set optional properties to be passed into the SqlSession configuration, as alternative to a * {@code <properties>} tag in the configuration xml file. This will be used to * resolve placeholders in the config file. */ public void setConfigurationProperties(Properties sqlSessionFactoryProperties) { this.configurationProperties = sqlSessionFactoryProperties; } /** * Set the JDBC {@code DataSource} that this instance should manage transactions for. The {@code DataSource} * should match the one used by the {@code SqlSessionFactory}: for example, you could specify the same * JNDI DataSource for both. * * A transactional JDBC {@code Connection} for this {@code DataSource} will be provided to application code * accessing this {@code DataSource} directly via {@code DataSourceUtils} or {@code DataSourceTransactionManager}. * * The {@code DataSource} specified here should be the target {@code DataSource} to manage transactions for, not * a {@code TransactionAwareDataSourceProxy}. Only data access code may work with * {@code TransactionAwareDataSourceProxy}, while the transaction manager needs to work on the * underlying target {@code DataSource}. If there's nevertheless a {@code TransactionAwareDataSourceProxy} * passed in, it will be unwrapped to extract its target {@code DataSource}. * */ public void setDataSource(DataSource dataSource) { if (dataSource instanceof TransactionAwareDataSourceProxy) { // If we got a TransactionAwareDataSourceProxy, we need to perform // transactions for its underlying target DataSource, else data // access code won't see properly exposed transactions (i.e. // transactions for the target DataSource). this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); } else { this.dataSource = dataSource; } } /** * Sets the {@code SqlSessionFactoryBuilder} to use when creating the {@code SqlSessionFactory}. * * This is mainly meant for testing so that mock SqlSessionFactory classes can be injected. By * default, {@code SqlSessionFactoryBuilder} creates {@code DefaultSqlSessionFactory} instances. * */ public void setSqlSessionFactoryBuilder(SqlSessionFactoryBuilder sqlSessionFactoryBuilder) { this.sqlSessionFactoryBuilder = sqlSessionFactoryBuilder; } /** * Set the MyBatis TransactionFactory to use. Default is {@code SpringManagedTransactionFactory} * * The default {@code SpringManagedTransactionFactory} should be appropriate for all cases: * be it Spring transaction management, EJB CMT or plain JTA. If there is no active transaction, * SqlSession operations will execute SQL statements non-transactionally. * * <b>It is strongly recommended to use the default {@code TransactionFactory}.</b> If not used, any * attempt at getting an SqlSession through Spring's MyBatis framework will throw an exception if * a transaction is active. * * @see SpringManagedTransactionFactory * @param transactionFactory the MyBatis TransactionFactory */ public void setTransactionFactory(TransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; } /** * <b>NOTE:</b> This class <em>overrides</em> any {@code Environment} you have set in the MyBatis * config file. This is used only as a placeholder name. The default value is * {@code SqlSessionFactoryBean.class.getSimpleName()}. * * @param environment the environment name */ public void setEnvironment(String environment) { this.environment = environment; } /** * {@inheritDoc} */ 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"); this.sqlSessionFactory = buildSqlSessionFactory(); } /** * Build a {@code SqlSessionFactory} instance. * * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a * {@code SqlSessionFactory} instance based on an Reader. * Since 1.3.0, it can be specified a {@link Configuration} instance directly(without config file). * * @return SqlSessionFactory * @throws IOException if loading the config file failed */ protected SqlSessionFactory buildSqlSessionFactory() throws IOException { Configuration configuration; XMLConfigBuilder xmlConfigBuilder = null; if (this.configuration != null) { configuration = this.configuration; if (configuration.getVariables() == null) { configuration.setVariables(this.configurationProperties); } else if (this.configurationProperties != null) { configuration.getVariables().putAll(this.configurationProperties); } } else if (this.configLocation != null) { xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); configuration = xmlConfigBuilder.getConfiguration(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration"); } configuration = new Configuration(); if (this.configurationProperties != null) { configuration.setVariables(this.configurationProperties); } } if (this.objectFactory != null) { configuration.setObjectFactory(this.objectFactory); } if (this.objectWrapperFactory != null) { configuration.setObjectWrapperFactory(this.objectWrapperFactory); } if (this.vfs != null) { configuration.setVfsImpl(this.vfs); } if (hasLength(this.typeAliasesPackage)) { String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeAliasPackageArray) { configuration.getTypeAliasRegistry().registerAliases(packageToScan, typeAliasesSuperType == null ? Object.class : typeAliasesSuperType); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases"); } } } if (!isEmpty(this.typeAliases)) { for (Class<?> typeAlias : this.typeAliases) { configuration.getTypeAliasRegistry().registerAlias(typeAlias); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type alias: '" + typeAlias + "'"); } } } if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } if (hasLength(this.typeHandlersPackage)) { String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeHandlersPackageArray) { configuration.getTypeHandlerRegistry().register(packageToScan); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers"); } } } if (!isEmpty(this.typeHandlers)) { for (TypeHandler<?> typeHandler : this.typeHandlers) { configuration.getTypeHandlerRegistry().register(typeHandler); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type handler: '" + typeHandler + "'"); } } } if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls try { configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); } catch (SQLException e) { throw new NestedIOException("Failed getting a databaseId", e); } } if (this.cache != null) { configuration.addCache(this.cache); } if (xmlConfigBuilder != null) { try { xmlConfigBuilder.parse(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'"); } } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); } finally { ErrorContext.instance().reset(); } } if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource)); if (!isEmpty(this.mapperLocations)) { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { 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(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found"); } } return this.sqlSessionFactoryBuilder.build(configuration); } /** * {@inheritDoc} */ public SqlSessionFactoryBean getObject() throws Exception { return this; } public SqlSessionFactory getSqlSessionFactory() throws Exception { afterPropertiesSet(); return this.sqlSessionFactory; } /** * {@inheritDoc} */ public Class<? extends SqlSessionFactoryBean> getObjectType() { return SqlSessionFactoryBean.class; } /** * {@inheritDoc} */ public boolean isSingleton() { return true; } /** * {@inheritDoc} */ public void onApplicationEvent(ApplicationEvent event) { if (failFast && event instanceof ContextRefreshedEvent) { // fail-fast -> check all statements are completed this.sqlSessionFactory.getConfiguration().getMappedStatementNames(); } } }
修改部分已標紅,主要就是將以前的
/** * {@inheritDoc} */ @Override public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { afterPropertiesSet(); } return this.sqlSessionFactory; }
修改為了:
/** * {@inheritDoc} */ public SqlSessionFactoryBean getObject() throws Exception { return this; } public SqlSessionFactory getSqlSessionFactory() throws Exception { afterPropertiesSet(); return this.sqlSessionFactory; }
getObject的修改很好理解,就是將以前返回SqlSessionFactory,現在要讓他返回SqlSessionFactoryBean。而新增的getSqlSessionFactory主要是因為SqlSessionFactoryBean中保存有SqlSessionFactory。
以前是通過getObject返回出去,現在getObject被占用了,那就必須給它留個后門對吧^_^。
測試一把,完美。
總結:實際解決這個問題,遠比上面寫的這么簡單,中間還有很多的曲折,但是每當遇到一個問題,解決這個問題的過程中我們就會獲得更多。人就是這樣慢慢成長的。
