栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

关于Spring3 + Mybatis3整合时多数据源动态切换的问题

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

关于Spring3 + Mybatis3整合时多数据源动态切换的问题

以前的项目经历中,基本上都是spring + hibernate + Spring JDBC这种组合用的多。至于MyBatis,也就这个项目才开始试用,闲话不多说,进入正题。

以前的这种框架组合中,动态数据源切换可谓已经非常成熟了,网上也有非常多的博客介绍,都是继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法。具体做法就不在此废话了。

所以当项目中碰到这个问题,我几乎想都没有想,就采用了这种做法,但是一测试,一点反应都没有。当时觉得不可能,于是断点,加log调试,发现determineCurrentLookupKey()根本没有调用。 

为什么列? 这不可能啊。静下心来,仔细想想,才想到一个关键的问题: Mybatis整合Spring,而不是Spring整合的Mybatis! 直觉告诉我,问题就出在这里。

于是花时间去研究一下mybatis-spring.jar 这个包,发现有SqlSession这东西,很本能的就注意到了这一块,然后大致看一下他的一些实现类。于是就发现了他的实现类里面有一个内部类SqlSessionInterceptor(研究过程就不多说了,毕竟是个痛苦的过程)

好吧,这个类的作用列,就是产生sessionProxy。关键代码如下

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

这个sqlSessionFactory 我们就很眼熟啦,是我们在spring配置文件中配了的,是吧,也就是说这东西是直接从我们配置文件中读进来,但这东西,就关联了Datasource。所以就想到,如果能把这东西,做到动态,那么数据源切换,也就动态了。

于是第一反应就是写了一个类,然后在里面定义一个Map,用来存放多个SqlSessionFactory,并采用Setter方法进行属性注入。

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

所以Spring的配置文件就变成了这样:

 
   
   
    
     
     
    
   
  
  
   
   
  

那么这个思想是那里来的列? 当然就是借鉴了Spring的动态数据源的思想啦,对比一下Spring动态数据源的配置,看看是不是差不多?

然后重写了个关键的方法:

 
 @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"); 
 } 

而SqlSessionContextHolder就很简单,就是一个ThreadLocal的思想

public class SqlSessionContextHolder { 
 private static final ThreadLocal contextHolder = new ThreadLocal(); 
 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("当前线程Thread:"+Thread.currentThread().getName()+" 当前的数据源 key is "+ key); 
  return key; 
 } 
} 

博主信心满满就开始测试了。。结果发现不行,切换不过来,始终都是绑定的是构造函数中的那个默认的sqlSessionFactory,当时因为看了一天源码,头也有点晕。其实为什么列?

看看我们产生sessionProxy的地方代码,他的sqlSessionFactory是直接从构造函数来拿的。而构造函数中的sqlSessionFactory在spring容器启动时,就已经初始化好了,这点也可以从我们Spring配置文件中得到证实。

那这个问题,怎么解决列? 于是博主便想重写那个sqlSessionInterceptor。 擦,问题就来了,这个类是private的,没办法重写啊。于是博主又只能在自己的EjsSqlSessionTemplate类中,也定义了一个内部类,把源码中的代码都copy过来,唯一不同的就是我不是读取构造函数中的sqlSessionFactory.而是每次都去调用 getSqlSessionFactory()方法。代码如下:

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

再试,发现还是不行,再找原因,又回归到了刚才那个问题。因为我没有重写SqlSessionTemplate的构造函数,而sqlSessionProxy是在构函数中初始化的,代码如下:

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()); 
} 

而SqlSessionInterceptor()这东西都是private。 所以父类压根就不会加载我写的那个SqlSessionInterceptor()。所以问题就出在这,那好吧,博主又重写构函数

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

很明显这段代码是编译不通过的,构造函数中,怎么可能调用类实例方法列?  那怎么办列? 又只有把父类的构造函数copy过来,那问题又有了,这些成员属性又没有。那又只得把他们也搬过来。。  后来,这个动态数据数据源的功能,终于完成了。

--------------------------------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------整个完整的代码如下:

1、重写SqlSessionTemplate (重写的过程已经在上面分析过了)

public class EjsSqlSessionTemplate extends SqlSessionTemplate { 
 private final SqlSessionFactory sqlSessionFactory; 
 private final ExecutorType executorType; 
 private final SqlSession sqlSessionProxy; 
 private final PersistenceExceptionTranslator exceptionTranslator; 
 private Map targetSqlSessionFactory; 
 public void setTargetSqlSessionFactory(Map 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; 
 } 
  
 public  T selectOne(String statement) { 
  return this.sqlSessionProxy. selectOne(statement); 
 } 
  
 public  T selectOne(String statement, Object parameter) { 
  return this.sqlSessionProxy. selectOne(statement, parameter); 
 } 
  
 public  Map selectMap(String statement, String mapKey) { 
  return this.sqlSessionProxy. selectMap(statement, mapKey); 
 } 
  
 public  Map selectMap(String statement, Object parameter, String mapKey) { 
  return this.sqlSessionProxy. selectMap(statement, parameter, mapKey); 
 } 
  
 public  Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) { 
  return this.sqlSessionProxy. selectMap(statement, parameter, mapKey, rowBounds); 
 } 
  
 public  List selectList(String statement) { 
  return this.sqlSessionProxy. selectList(statement); 
 } 
  
 public  List selectList(String statement, Object parameter) { 
  return this.sqlSessionProxy. selectList(statement, parameter); 
 } 
  
 public  List selectList(String statement, Object parameter, RowBounds rowBounds) { 
  return this.sqlSessionProxy. selectList(statement, parameter, rowBounds); 
 } 
  
 public void select(String statement, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, handler); 
 } 
  
 public void select(String statement, Object parameter, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, parameter, handler); 
 } 
  
 public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { 
  this.sqlSessionProxy.select(statement, parameter, rowBounds, handler); 
 } 
  
 public int insert(String statement) { 
  return this.sqlSessionProxy.insert(statement); 
 } 
  
 public int insert(String statement, Object parameter) { 
  return this.sqlSessionProxy.insert(statement, parameter); 
 } 
  
 public int update(String statement) { 
  return this.sqlSessionProxy.update(statement); 
 } 
  
 public int update(String statement, Object parameter) { 
  return this.sqlSessionProxy.update(statement, parameter); 
 } 
  
 public int delete(String statement) { 
  return this.sqlSessionProxy.delete(statement); 
 } 
  
 public int delete(String statement, Object parameter) { 
  return this.sqlSessionProxy.delete(statement, parameter); 
 } 
  
 public  T getMapper(Class type) { 
  return getConfiguration().getMapper(type, this); 
 } 
  
 public void commit() { 
  throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); 
 } 
  
 public void commit(boolean force) { 
  throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession"); 
 } 
  
 public void rollback() { 
  throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); 
 } 
  
 public void rollback(boolean force) { 
  throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession"); 
 } 
  
 public void close() { 
  throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession"); 
 } 
  
 public void clearCache() { 
  this.sqlSessionProxy.clearCache(); 
 } 
  
 public Connection getConnection() { 
  return this.sqlSessionProxy.getConnection(); 
 } 
  
 public List flushStatements() { 
  return this.sqlSessionProxy.flushStatements(); 
 } 
  
 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。自定义了一个注解

 
@Target({ElementType.METHOD, ElementType.TYPE}) 
@Retention(RetentionPolicy.RUNTIME) 
@documented 
public @interface ChooseDataSource { 
 String value() default ""; 
} 

3.定义一个AspectJ的切面(我习惯用AspectJ,因为spring AOP不支持cflow()这些语法),所以在编译,打包的时候一定要用aspectJ的编译器,不能直接用原生的JDK。有些方法就是我基于以前Hibernate,JDBC动态数据源的时候改动的。

 
@Aspect 
public abstract class ChooseDataSourceAspect { 
 protected static final ThreadLocal preDatasourceHolder = new ThreadLocal(); 
 @Pointcut("execution(public * *.*(..))") 
 public void allMethodPoint() { 
 } 
 @Pointcut("@within(com.ejushang.spider.annotation.ChooseDataSource) && allMethodPoint()") 
 public void allServiceMethod() { 
 } 
  
 @Pointcut("cflow(allServiceMethod()) && allServiceMethod()") 
 public void changeDatasourcePoint() { 
 } 
  
 @Before("changeDatasourcePoint()") 
 public void changeDataSourceBeforeMethodExecution(JoinPoint jp) { 
  //拿到anotation中配置的数据源 
  String resultDS = determineDatasource(jp); 
  //没有配置实用默认数据源 
  if (resultDS == null) { 
   SqlSessionContextHolder.setSessionFactoryKey(null); 
   return; 
  } 
  preDatasourceHolder.set(SqlSessionContextHolder.getDataSourceKey()); 
  //将数据源设置到数据源持有者 
  SqlSessionContextHolder.setSessionFactoryKey(resultDS); 
 } 
  
 @SuppressWarnings("rawtypes") 
 protected String determineDatasource(JoinPoint jp) { 
  String methodName = jp.getSignature().getName(); 
  Class targetClass = jp.getSignature().getDeclaringType(); 
  String dataSourceForTargetClass = resolveDataSourceFromClass(targetClass); 
  String dataSourceForTargetMethod = resolveDataSourceFromMethod( 
    targetClass, methodName); 
  String resultDS = determinateDataSource(dataSourceForTargetClass, 
    dataSourceForTargetMethod); 
  return resultDS; 
 } 
  
 @After("changeDatasourcePoint()") 
 public void restoreDataSourceAfterMethodExecution() { 
  SqlSessionContextHolder.setSessionFactoryKey(preDatasourceHolder.get()); 
  preDatasourceHolder.remove(); 
 } 
  
 @SuppressWarnings("rawtypes") 
 private String resolveDataSourceFromMethod(Class targetClass, 
     String methodName) { 
  Method m = ReflectUtil.findUniqueMethod(targetClass, methodName); 
  if (m != null) { 
   ChooseDataSource choDs = m.getAnnotation(ChooseDataSource.class); 
   return resolveDataSourcename(choDs); 
  } 
  return null; 
 } 
  
 private String determinateDataSource(String classDS, String methodDS) { 
//  if (null == classDS && null == methodDS) { 
//   return null; 
//  } 
  // 两者必有一个不为null,如果两者都为Null,也会返回Null 
  return methodDS == null ? classDS : methodDS; 
 } 
  
 @SuppressWarnings({"unchecked", "rawtypes"}) 
 private String resolveDataSourceFromClass(Class targetClass) { 
  ChooseDataSource classAnnotation = (ChooseDataSource) targetClass 
    .getAnnotation(ChooseDataSource.class); 
  // 直接为整个类进行设置 
  return null != classAnnotation ? resolveDataSourcename(classAnnotation) 
    : null; 
 } 
  
 private String resolveDataSourcename(ChooseDataSource ds) { 
  return ds == null ? null : ds.value(); 
 } 
} 

那么以上3个类,就可以作为一个公共的组件打个包了。

那么项目中具体 怎么用列?

4.  在项目中定义一个具体的AspectJ切面

@Aspect 
public class OrderFetchAspect extends ChooseDataSourceAspect { 
}

如果你的根据你的需要重写方法,我这边是不需要重写的,所以空切面就行了。

5.配置spring,在上面的分析过程中已经贴出了,基本上就是每个数据库,一个dataSource,每个DataSource一个SqlSessionFactory。最后配一个SqlSessionTemplate,也就是我们自己重写的。再就是MapperScan了,大致如下(数据库连接信息已去除,包名为杜撰):

 
 
 
 
 
 
  
 
 
 
 
  
  
  
 
 
  
  
   
    
    
   
  
 
 
  
  
 

6.具体应用

@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 shopList1 = shopService.findAllShop(); 
  for(Shop shop : shopList1){ 
   System.out.println(shop); 
  } 
  fromTestDB(); 
 } 
 @ChooseDataSource("sysinfo") 
 private void fromTestDB(){ 
  List shopList = jdpTbTradeService.findAllShop(); 
  for(Shop shop : shopList){ 
   System.out.println(shop); 
  } 
 } 
} 

测试发现 shopList1是从spider库查出来的数据,而fromDB则是从sysinfo中查出来的数据。 那么我们就大功告成。
要做到我以上功能,Spring AOP是做不到的,因为他不支持Cflow(),这也就是我为什么要用AspectJ的原因。

-----------------------------------------------------------------------------------------------再次分割线-------------------------------------------------------------------------------------------------------------------

好了,功能我们已经实现了,你有没有觉得很麻烦,这一点也不Spring的风格,Spring的各个地方扩展都是很方便的。那么我们看看,在SqlSessionTemplate中的什么地方改动一下,我们就可以很轻松的实现这个功能列?大家可以理解了,再回去看一下源码。

其实,只要将源码中的那个SqlSessionInterceptor的这句话:

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

改为:

final SqlSession sqlSession = getSqlSession( 
     EjsSqlSessionTemplate.this.getSqlSessionFactory(), 
     EjsSqlSessionTemplate.this.executorType,     
		 EjsSqlSessionTemplate.this.exceptionTranslator); 

保证 每次在产生Session代理的时候,传进去的参数都是调用getSqlSessionFactory()获取,那么我们自定义的SqlSessionTemplate,只要重写getSqlSessionFactory(),加多一个以下2句话:

private Map targetSqlSessionFactory; 
 public void setTargetSqlSessionFactory(Map targetSqlSessionFactory) { 
  this.targetSqlSessionFactory = targetSqlSessionFactory; 
 } 

那么就完全可以实现动态数据源切换。  那么mybatis-spring的项目团队会这样维护么? 我会以mail的方式与他们沟通。至于能否改进,我们不得而知了。

其实这也就引发一个关于面向对象设计时的思想,也是一直争论得喋喋不休的一个问题:

    在类的方法中,如果要用到类的属性时,是直接用this.filedName  来操作,还是用  getFiledName() 来进行操作?

其实以前我也是偏向于直接用this.属性来进行操作的,但是经历过这次以后,我想我会偏向于后者。

以上所述是小编给大家介绍的关于Spring3 + Mybatis3整合时多数据源动态切换的问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对考高分网网站的支持!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/146590.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号