以前的项目经历中,基本上都是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动态数据源的配置,看看是不是差不多?
然后重写了个关键的方法:
/**
* 重写得到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");
}
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;
}
}
看看我们产生sessionProxy的地方代码,他的sqlSessionFactory是直接从构造函数来拿的。而构造函数中的sqlSessionFactory在spring容器启动时,就已经初始化好了,这点也可以从我们Spring配置文件中得到证实。
那这个问题,怎么解决列? 于是博主便想重写那个sqlSessionInterceptor。 擦,问题就来了,这个类是private的,没办法重写啊。于是博主又只能在自己的EjsSqlSessionTemplate类中,也定义了一个内部类,把源码中的代码都copy过来,唯一不同的就是我不是读取构造函数中的sqlSessionFactory.而是每次都去调用 getSqlSessionFactory()方法。代码如下:
final SqlSession sqlSession = getSqlSession(
EjsSqlSessionTemplate.this.getSqlSessionFactory(),
EjsSqlSessionTemplate.this.getExecutorType(),
EjsSqlSessionTemplate.this.getPersistenceExceptionTranslator());
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());
}
public EjsSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
super(getSqlSessionFactory(), executorType, exceptionTranslator);
}
--------------------------------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------整个完整的代码如下:
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
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 "";
}
/**
* 类描述:完成数据源的切换,抽类切面,具体项目继承一下,不需要重写即可使用
*
* @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);
}
}
}
要做到我以上功能,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.属性来进行操作的,但是经历过这次以后,我想我会偏向于后者。