Dynamic switching of multiple data sources during Spring3 + Mybatis3 integration

  • 2020-07-21 08:19:53
  • OfStack

In my previous project experience, I mostly used the combination of spring + hibernate + Spring JDBC. As for MyBatis, this project has just been tried out. Without further ado, let's get to the point.

In the previous framework combination, dynamic data source switching has been very mature, and there are a lot of blogs on the Internet, which inherit AbstractRoutingDataSource and rewrite the determineCurrentLookupKey () method. The specific approach is not this nonsense.

So when I encountered this problem in the project, I almost didn't think about it, so I adopted this method, but I didn't get any response for 1 test. It was impossible, so the breakpoint, log debugging, found that determineCurrentLookupKey () was not called at all.

Why column? That's impossible. Calm down and think carefully, and then come to a key question: Mybatis integrated Spring, not Spring integrated Mybatis! My gut tells me that's the problem.

So I took the time to study the 1 mybatis-ES23en. jar package, found that there is SqlSession this thing, it is very instinct to pay attention to this 1 piece, and then roughly 1 of his 1 some implementation classes. He found out that his implementation class had an inner class, SqlSessionInterceptor (I won't go into the research, it was a painful process)

Well, the purpose of this class column is to generate sessionProxy. The key code is as follows


final SqlSession sqlSession = getSqlSession( 
 SqlSessionTemplate.this.sqlSessionFactory, 
 SqlSessionTemplate.this.executorType, 
 SqlSessionTemplate.this.exceptionTranslator); 

This sqlSessionFactory is something that we're familiar with, that we created in our spring configuration file, right, so this thing is read directly from our configuration file, but this thing is associated with Datasource. So it occurred to me that if I could make this thing dynamic, then switching data sources would be dynamic.

So the first reaction is to write a class and define an Map to hold multiple SqlSessionFactory and use the Setter method for attribute injection.


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 

So the Spring configuration file looks like this:


<bean id="sqlSessionTemplate" class="com.ejushang.spider.datasource.EjsSqlSessionTemplate"> 
  <constructor-arg ref="sqlSessionFactory" /> 
  <property name="targetSqlSessionFactory"> 
   <map> 
    <entry value-ref="sqlSessionFactory" key="spider"/> 
    <entry value-ref="sqlSessionFactoryTb" key="sysinfo"/> 
   </map> 
  </property> 
 </bean> 
 <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 
  <property name="basePackage" value="com.foo.bar.**.mapper*" /> 
  <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"/> 
 </bean> 

So this idea is the column from where? Of course, it refers to the idea of dynamic data source of Spring. Compare the configuration of dynamic data source of Spring to see if it is similar.

Then I rewrote a key method:


/** 
  *  Rewrite the get SqlSessionFactory The method of  
  * @return 
  */ 
 @Override 
 public SqlSessionFactory getSqlSessionFactory() { 
 
  SqlSessionFactory targetSqlSessionFactory = this.targetSqlSessionFactory.get(SqlSessionContextHolder.getDataSourceKey()); 
  if (targetSqlSessionFactory != null) { 
   return targetSqlSessionFactory; 
  } else if ( this.getSqlSessionFactory() != null) { 
   return this.getSqlSessionFactory(); 
  } 
  throw new IllegalArgumentException("sqlSessionFactory or targetSqlSessionFactory must set one at least"); 
 } 

And SqlSessionContextHolder is very simple, it's an ThreadLocal idea


public class SqlSessionContextHolder { 
 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); 
 private static Logger logger = LoggerFactory.getLogger(SqlSessionContextHolder.class); 
 public static void setSessionFactoryKey(String dataSourceKey) { 
  contextHolder.set(dataSourceKey); 
 } 
 public static String getDataSourceKey() { 
  String key = contextHolder.get(); 
  logger.info(" The current thread Thread:"+Thread.currentThread().getName()+"  Current data source  key is "+ key); 
  return key; 
 } 
} 

The e blogger was confident enough to start the test. The results found no, switch over, is always bound to the constructor that is the default sqlSessionFactory, at that time because of looking at the source code for a day, the head is a little dizzy. Why is it actually a column?

Look at the code where we generated sessionProxy, whose sqlSessionFactory was taken directly from the constructor. sqlSessionFactory in the constructor is initialized as soon as the spring container is started, as evidenced by our Spring configuration file.

So this problem, how do you solve columns? So the blogger wants to rewrite sqlSessionInterceptor. Erase, the problem is, this class is private, there is no way to rewrite. The only difference is that Instead of reading sqlSessionFactory from the constructor, I call the getSqlSessionFactory() method every time. The code is as follows:


final SqlSession sqlSession = getSqlSession(    
EjsSqlSessionTemplate.this.getSqlSessionFactory(),    
EjsSqlSessionTemplate.this.getExecutorType(),     
EjsSqlSessionTemplate.this.getPersistenceExceptionTranslator()); 

Try again, find out it still doesn't work, find out the reason again, come back to the same problem. Because I did not override the constructor of SqlSessionTemplate, which is initialized in the constructor, the code is as follows:


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; 
 this.sqlSessionProxy = (SqlSession) newProxyInstance( 
  SqlSessionFactory.class.getClassLoader(), 
  new Class[] { SqlSession.class }, 
  new SqlSessionInterceptor()); 
} 

And SqlSessionInterceptor(), this is all private. So the parent class won't load the one I wrote, SqlSessionInterceptor(), at all. So that's the problem. Well, the blogger overrides the construct again


public EjsSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { 
  super(getSqlSessionFactory(), executorType, exceptionTranslator); 
 } 

Obviously this code won't compile. How can you call a class instance method column in a constructor? So what do I do? You have to bring in the superclass constructor copy, and then you have a problem, and you don't have these member attributes. Then we'll have to move them in too. Later, this dynamic data source function, finally completed.

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - line -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- The whole complete code is as follows:

1. Rewrite SqlSessionTemplate (the process of rewriting has been analyzed above)


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 private final SqlSessionFactory sqlSessionFactory; 
 private final ExecutorType executorType; 
 private final SqlSession sqlSessionProxy; 
 private final PersistenceExceptionTranslator exceptionTranslator; 
 private Map<Object, SqlSessionFactory> targetSqlSessionFactory; 
 public void setTargetSqlSessionFactory(Map<Object, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
 public EjsSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { 
  this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); 
 } 
 public EjsSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { 
  this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration() 
    .getEnvironment().getDataSource(), true)); 
 } 
 public EjsSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, 
         PersistenceExceptionTranslator exceptionTranslator) { 
  super(sqlSessionFactory, executorType, exceptionTranslator); 
  this.sqlSessionFactory = sqlSessionFactory; 
  this.executorType = executorType; 
  this.exceptionTranslator = exceptionTranslator; 
  this.sqlSessionProxy = (SqlSession) newProxyInstance( 
    SqlSessionFactory.class.getClassLoader(), 
    new Class[] { SqlSession.class }, 
    new SqlSessionInterceptor()); 
 } 
 @Override 
 public SqlSessionFactory getSqlSessionFactory() { 
  SqlSessionFactory targetSqlSessionFactory = this.targetSqlSessionFactory.get(SqlSessionContextHolder.getDataSourceKey()); 
  if (targetSqlSessionFactory != null) { 
   return targetSqlSessionFactory; 
  } else if ( this.sqlSessionFactory != null) { 
   return this.sqlSessionFactory; 
  } 
  throw new IllegalArgumentException("sqlSessionFactory or targetSqlSessionFactory must set one at least"); 
 } 
 @Override 
 public Configuration getConfiguration() { 
  return this.getSqlSessionFactory().getConfiguration(); 
 } 
 public ExecutorType getExecutorType() { 
  return this.executorType; 
 } 
 public PersistenceExceptionTranslator getPersistenceExceptionTranslator() { 
  return this.exceptionTranslator; 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <T> T selectOne(String statement) { 
  return this.sqlSessionProxy.<T> selectOne(statement); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <T> T selectOne(String statement, Object parameter) { 
  return this.sqlSessionProxy.<T> selectOne(statement, parameter); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <K, V> Map<K, V> selectMap(String statement, String mapKey) { 
  return this.sqlSessionProxy.<K, V> selectMap(statement, mapKey); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) { 
  return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 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} 
  */ 
 public <E> List<E> selectList(String statement) { 
  return this.sqlSessionProxy.<E> selectList(statement); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <E> List<E> selectList(String statement, Object parameter) { 
  return this.sqlSessionProxy.<E> selectList(statement, parameter); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 
  return this.sqlSessionProxy.<E> selectList(statement, parameter, rowBounds); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void select(String statement, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, handler); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void select(String statement, Object parameter, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, parameter, handler); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, parameter, rowBounds, handler); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int insert(String statement) { 
  return this.sqlSessionProxy.insert(statement); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int insert(String statement, Object parameter) { 
  return this.sqlSessionProxy.insert(statement, parameter); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int update(String statement) { 
  return this.sqlSessionProxy.update(statement); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int update(String statement, Object parameter) { 
  return this.sqlSessionProxy.update(statement, parameter); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int delete(String statement) { 
  return this.sqlSessionProxy.delete(statement); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public int delete(String statement, Object parameter) { 
  return this.sqlSessionProxy.delete(statement, parameter); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public <T> T getMapper(Class<T> type) { 
  return getConfiguration().getMapper(type, this); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void commit() { 
  throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void commit(boolean force) { 
  throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void rollback() { 
  throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void rollback(boolean force) { 
  throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void close() { 
  throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession"); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public void clearCache() { 
  this.sqlSessionProxy.clearCache(); 
 } 
 /** 
  * {@inheritDoc} 
  */ 
 public Connection getConnection() { 
  return this.sqlSessionProxy.getConnection(); 
 } 
 /** 
  * {@inheritDoc} 
  * @since 1.0.2 
  */ 
 public List<BatchResult> flushStatements() { 
  return this.sqlSessionProxy.flushStatements(); 
 } 
 /** 
  * 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 { 
   final SqlSession sqlSession = getSqlSession( 
     EjsSqlSessionTemplate.this.getSqlSessionFactory(), 
     EjsSqlSessionTemplate.this.executorType, 
     EjsSqlSessionTemplate.this.exceptionTranslator); 
   try { 
    Object result = method.invoke(sqlSession, args); 
    if (!isSqlSessionTransactional(sqlSession, EjsSqlSessionTemplate.this.getSqlSessionFactory())) { 
     // 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 (EjsSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { 
     Throwable translated = EjsSqlSessionTemplate.this.exceptionTranslator 
       .translateExceptionIfPossible((PersistenceException) unwrapped); 
     if (translated != null) { 
      unwrapped = translated; 
     } 
    } 
    throw unwrapped; 
   } finally { 
    closeSqlSession(sqlSession, EjsSqlSessionTemplate.this.getSqlSessionFactory()); 
   } 
  } 
 } 
} 

2. Custom 1 annotation


/** 
 *  Annotated data source for data source switching  
 * User:Amos.zhou 
 * Date: 14-2-27 
 * Time:  In the afternoon 2:34 
 */ 
@Target({ElementType.METHOD, ElementType.TYPE}) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
public @interface ChooseDataSource { 
 String value() default ""; 
} 

3. Define a section of AspectJ (I am used to using AspectJ because spring AOP does not support cflow() syntax), so when compiling and packaging, 1 must use the compiler of aspectJ, not the native JDK. Some of these methods are based on previous Hibernate,JDBC dynamic data sources.


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
0

These three classes are then packaged as a common component.

So how do you actually use columns in your project?

4. Define a specific AspectJ section in the project


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
1

If you rewrite the method as you want, I don't have to rewrite it, so the empty section is fine.

5. Configuration of spring, which has been posted in the above analysis, is basically 1 dataSource and 1 SqlSessionFactory per database. And then finally, SqlSessionTemplate, which we rewrote. Then there is MapperScan, which looks something like this (database connection information has been removed, package name is fabricated) :


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
2

6. Specific application


@ChooseDataSource("spider") 
public class ShopServiceTest extends ErpTest { 
 private static final Logger log = LoggerFactory.getLogger(ShopServiceTest.class); 
 @Autowired 
 private IShopService shopService; 
 @Autowired 
 private IJdpTbTradeService jdpTbTradeService; 
 @Test 
 @Rollback(false) 
 public void testFindAllShop(){ 
  List<Shop> shopList1 = shopService.findAllShop(); 
  for(Shop shop : shopList1){ 
   System.out.println(shop); 
  } 
  fromTestDB(); 
 } 
 @ChooseDataSource("sysinfo") 
 private void fromTestDB(){ 
  List<Shop> shopList = jdpTbTradeService.findAllShop(); 
  for(Shop shop : shopList){ 
   System.out.println(shop); 
  } 
 } 
} 

The tests found that shopList1 was retrieved from the spider library, while fromDB was retrieved from sysinfo. So we're done.
To do the above, Spring AOP cannot do it because it does not support Cflow(), which is why I use AspectJ.

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- line again -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ---------------

Ok, we have implemented the function, do you find it troublesome, this point is not Spring style, Spring extension everywhere is very convenient. So let's see, what changes in SqlSessionTemplate can we make to this function column easily? You can understand, then go back to 1 source.

In fact, as long as the source SqlSessionInterceptor this sentence:


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
4

To:


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
5

Ensure that every time when Session proxy is generated, the parameters passed in are obtained by calling getSqlSessionFactory (), then our custom SqlSessionTemplate, we only need to rewrite getSqlSessionFactory (), adding one more sentence as follows:


public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 
 private Map<String, SqlSessionFactory> targetSqlSessionFactory = new HashMap<String, SqlSessionFactory>(); 
 public void setTargetSqlSessionFactory(Map<String, SqlSessionFactory> targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 
6

Then dynamic data source switching can be implemented completely. Will the mybatis-spring project team maintain this way? I will communicate with them in the way of mail. Whether it can be improved, we don't know.

In fact, this leads to an idea about object-oriented design, which is also a question that has been debated endlessly for a long time:

In class methods, if you want to use attributes of a class, do you want to use this.filedName or getFiledName()?

In fact, In the past, I have preferred to operate directly with the ES185en. attribute, but after this experience, I think I will prefer the latter.


Related articles: