关于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动态数据源的配置,看看是不是差不多?

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

/**
     * 重写得到SqlSessionFactory的方法
     * @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");
    }


而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;
    }

    /**
     * {@inheritDoc}
     */
    public  T selectOne(String statement) {
        return this.sqlSessionProxy. selectOne(statement);
    }

    /**
     * {@inheritDoc}
     */
    public  T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy. selectOne(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public  Map selectMap(String statement, String mapKey) {
        return this.sqlSessionProxy. selectMap(statement, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public  Map selectMap(String statement, Object parameter, String mapKey) {
        return this.sqlSessionProxy. selectMap(statement, parameter, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public  Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
        return this.sqlSessionProxy. selectMap(statement, parameter, mapKey, rowBounds);
    }

    /**
     * {@inheritDoc}
     */
    public  List selectList(String statement) {
        return this.sqlSessionProxy. selectList(statement);
    }

    /**
     * {@inheritDoc}
     */
    public  List selectList(String statement, Object parameter) {
        return this.sqlSessionProxy. selectList(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
        return this.sqlSessionProxy. 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 getMapper(Class 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 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。自定义了一个注解

/**
 * 注解式数据源,用来进行数据源切换
 * User:Amos.zhou
 * Date: 14-2-27
 * Time: 下午2:34
 */
@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动态数据源的时候改动的。
/**
 * 
  • 类描述:完成数据源的切换,抽类切面,具体项目继承一下,不需要重写即可使用
  • * * @author: amos.zhou * 2013-8-1 上午11:51:40 * @since v1.0 */ @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() { } /** * 对所有注解有ChooseDataSource的类进行拦截 */ @Pointcut("cflow(allServiceMethod()) && allServiceMethod()") public void changeDatasourcePoint() { } /** * 根据@ChooseDataSource的属性值设置不同的dataSourceKey,以供DynamicDataSource */ @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); } /** *

    创建时间: 2013-8-20 上午9:48:44

    * 如果需要修改获取数据源的逻辑,请重写此方法 * * @param jp * @return */ @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; } /** * 方法执行完毕以后,数据源切换回之前的数据源。 * 比如foo()方法里面调用bar(),但是bar()另外一个数据源, * bar()执行时,切换到自己数据源,执行完以后,要切换到foo()所需要的数据源,以供 * foo()继续执行。 *

    创建时间: 2013-8-16 下午4:27:06

    */ @After("changeDatasourcePoint()") public void restoreDataSourceAfterMethodExecution() { SqlSessionContextHolder.setSessionFactoryKey(preDatasourceHolder.get()); preDatasourceHolder.remove(); } /** *
  • 创建时间: 2013-6-17 下午5:34:13
  • 创建人:amos.zhou
  • 方法描述 :
  • * * @param targetClass * @param methodName * @return */ @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; } /** *
  • 创建时间: 2013-6-17 下午5:06:02
  • *
  • 创建人:amos.zhou
  • *
  • 方法描述 : 确定 * 最终数据源,如果方法上设置有数据源,则以方法上的为准,如果方法上没有设置,则以类上的为准,如果类上没有设置,则使用默认数据源
  • * * @param classDS * @param methodDS * @return */ private String determinateDataSource(String classDS, String methodDS) { // if (null == classDS && null == methodDS) { // return null; // } // 两者必有一个不为null,如果两者都为Null,也会返回Null return methodDS == null ? classDS : methodDS; } /** *
  • 创建时间: 2013-6-17 下午4:33:03
  • 创建人:amos.zhou
  • 方法描述 : 类级别的 @ChooseDataSource * 的解析
  • * * @param targetClass * @return */ @SuppressWarnings({"unchecked", "rawtypes"}) private String resolveDataSourceFromClass(Class targetClass) { ChooseDataSource classAnnotation = (ChooseDataSource) targetClass .getAnnotation(ChooseDataSource.class); // 直接为整个类进行设置 return null != classAnnotation ? resolveDataSourcename(classAnnotation) : null; } /** *
  • 创建时间: 2013-6-17 下午4:31:42
  • 创建人:amos.zhou
  • 方法描述 : * 组装DataSource的名字
  • * * @param ds * @return */ 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了,大致如下(数据库连接信息已去除,包名为杜撰):

        
            
        
    
        
            
        
    
        
        
            
        
        
        
    
    
        
            
            
            
        
    
        
            
            
            com/foo/bar/**/configtb/*mapper.xml" />
        
    
        
            
            
                
                    
                    
                
            
        
    
        
            
            
        
    


    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.属性来进行操作的,但是经历过这次以后,我想我会偏向于后者。


    你可能感兴趣的:(SSH那些事)