开博也快三年还没正经的写过一篇文章,总是随便的记些随笔。今天抽空就将最近解决的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被占用了,那就必须给它留个后门对吧^_^。
测试一把,完美。
总结:实际解决这个问题,远比上面写的这么简单,中间还有很多的曲折,但是每当遇到一个问题,解决这个问题的过程中我们就会获得更多。人就是这样慢慢成长的。