使用Aop配置spring多数据库源
spring用xml配置多数据源很简单,就是配多个datasource,再配相应的sessionFactory,sqlSession,对应相应的Mapper文件夹与sql xml文件。springBoot第一次配,记录下:
1.配置Mapper接口,sql xml路径,数据库连接串
//配置扫描Dao文件包路径 @MapperScan(basePackages = {"com.xx.dao","com.xx.module.yuwen.dao"}) @Configuration public class DynamicDataSourceConfiguration { private static final String DB1_MASTER="db1"; private static final String DB1_SLAVE1="db1"; private static final String DB2_MASTER="db1"; private static final String DB2_SLAVE1="db2"; @Bean @ConfigurationProperties(prefix = "db1.datasource.master") //配置文件前缀 public DataSource dbMaster() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "db1.datasource.slave1") public DataSource dbSlave1() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "db2.datasource.master") public DataSource yuwenMaster() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "db2.datasource.slave1") public DataSource yuwenSlave1() { return DruidDataSourceBuilder.create().build(); } /** * 核心动态数据源 * * @return 数据源实例 */ @Bean public DataSource dynamicDataSource() { DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); dataSource.setDefaultTargetDataSource(dbMaster()); Map<Object, Object> dataSourceMap = new HashMap<>(4); dataSourceMap.put(DB1_MASTER, dbMaster()); dataSourceMap.put(DB1_SLAVE1, dbSlave1()); dataSourceMap.put(DB2_MASTER, yuwenMaster()); dataSourceMap.put(DB2_SLAVE1, yuwenSlave1()); dataSource.setTargetDataSources(dataSourceMap); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); //mapper文件夹下分多个数据源,比如,mapper/db1/a.xml,mapper/db2/b.xml。 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate() throws Exception { return new SqlSessionTemplate(sqlSessionFactory()); } /** * 事务管理 * * @return 事务管理实例 */ @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } }
这里有一句(DynamicRoutingDataSource) dataSource.setDefaultTargetDataSource(dbMaster()),这个方法实际调用的是org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.setDefaultTargetDataSource ,可以看出来这是个spring的类,为什么一定要调用下这个?因为有这种情况,一个方法类需要使用多个数据源如:
1.使用数据源A
2.使用数据源B BService.b();
3.使用数据源A
假设这种情况调用本方法设置了数据源A,到"2"时又由aop设置了数据B。到"3"就没有aop设置数据源了,而它要使用数据源A,怎么做呢?先清除数据源,再使用默认的数据源,而默认的数据源就是A. 源码略
里面用的DynamicRoutingDataSource:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { //private static final Logger LOG = LoggerFactory.getLogger(DynamicRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { //LOG.info("当前数据源:" + DynamicDataSourceContextHolder.get()); return DynamicDataSourceContextHolder.get(); } }
2.拦截器,设置数据源
@Aspect @Order(-1) @Component public class DynamicDataSourceAspect { private static final Logger LOG = LoggerFactory.getLogger(DynamicDataSourceAspect.class); //对服务实现类进行切面,动态指定数据源 @Pointcut("(execution(* com.xx.service.api.impl..*.*(..))) || (execution(* com.xx.module.xxx.service.impl..*.*(..)))") public void pointCut() { } /** * 执行方法前更换数据源 * * @param joinPoint 切点 * @param targetDataSource 动态数据源 */ @Before("@annotation(targetDataSource)") public void doBefore(JoinPoint joinPoint, TargetDataSource targetDataSource) { DataSourceKey dataSourceKey = targetDataSource.dataSourceKey(); if (dataSourceKey == DynamicDataSourceConfiguration.DB_1SLAVE1) { LOG.debug(String.format("使用设定数据源 %s", DynamicDataSourceConfiguration.DB1_SLAVE1)); DynamicDataSourceContextHolder.set(DynamicDataSourceConfiguration.DB1_SLAVE1); }else { LOG.debug(String.format("使用默认数据源 %s", DynamicDataSourceConfiguration.DB_MASTER)); DynamicDataSourceContextHolder.set(DynamicDataSourceConfiguration.DB_MASTER); } } /** * 执行方法后清除数据源设置 * * @param joinPoint 切点 * @param targetDataSource 动态数据源 */ @After("@annotation(targetDataSource)") public void doAfter(JoinPoint joinPoint, TargetDataSource targetDataSource) { LOG.debug(String.format("当前数据源 %s 执行清理方法", targetDataSource.dataSourceKey())); DynamicDataSourceContextHolder.clear(); } @Before(value = "pointCut()") public void doBeforeWithDefault(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //获取当前切点方法对象 Method method = methodSignature.getMethod(); Class dclass=method.getDeclaringClass(); if (dclass.isInterface()) {//判断是否为借口方法 try { //获取实际类型的方法对象 method = joinPoint.getTarget().getClass() .getDeclaredMethod(joinPoint.getSignature().getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { LOG.error("方法不存在!", e); } } //common下的Service从db1库查数据 if(dclass.getTypeName().startsWith("com.xx.module.xxx")) { //如果方法上没有标注数据源注解,默认master分支 DynamicDataSourceContextHolder.set(DynamicDataSourceConfiguration.DB1_MASTER); }else { //如果方法上没有标注数据源注解,默认master分支 DynamicDataSourceContextHolder.set(DynamicDataSourceConfiguration.DB_MASTER); } } @After(value = "pointCut()") public void doAfterWithDefault(JoinPoint joinPoint) { DynamicDataSourceContextHolder.clear(); } } //上面的TargetDataSource是自定义的一个标注 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TargetDataSource { DataSourceKey dataSourceKey() default DataSourceKey.DB_MASTER; } //另外里面还用了一个类DynamicDataSourceContextHolder,把数据源设置进ThreadLocal public class DynamicDataSourceContextHolder { private static final Logger LOG = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); private static final ThreadLocal<datasourcekey> currentDatesource = new ThreadLocal<>(); /** * 清除当前数据源 */ public static void clear() { currentDatesource.remove(); } /** * 获取当前使用的数据源 * * @return 当前使用数据源的ID */ public static DataSourceKey get() { return currentDatesource.get(); } /** * 设置当前使用的数据源 * * @param value 需要设置的数据源ID */ public static void set(DataSourceKey value) { currentDatesource.set(value); } /** * 设置从从库读取数据 */ public static void setSlave() { if (RandomUtils.nextInt(0, 2) > 0) { DynamicDataSourceContextHolder.set(DataSourceKey.DB_MASTER); } else { DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE1); } } }
注意,上面的DynamicDataSourceAspect类中,定义了两类拦截器:
1.按照方法上面的标注类型拦截
@Before("@annotation(dataSource)")这样的写法,其中的dataSource表示下一行函数的参数名,而这个参数是一个自定义的注解类型。
2.按照包名拦截
@Pointcut("(execution(* com.xx.service.api.impl..*.*(..))) || (execution(* com.xx.module.xxx.service.impl..*.*(..)))") public void pointCut() { } @Before(value = "pointCut()") public void doBeforeWithDefault(JoinPoint joinPoint)
这种方式有个坑:
如果给Service的方法加个@Transaction ,里面数据源将会只使用同一个。如是里面的代码用了多个数据库,那么就会报错了。
在分析原因之前,先看一下从业务代码到拿到数据库的Connection的堆栈。
代码里用的连接池是druid,数据结构如下:
connection.conn.connection.myURL
connection:com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2fbbc6b1
conn:com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2fbbc6b1
connection:com.mysql.jdbc.JDBC4Connection@534585e9
myURL:数据库连接地址
为什么数据库会选择错误,因为数据库连接对象(sqlSession)就不是从我们的(DynamicRoutingDataSource)获得的,而是从TransactionSynchronizationManager的Holder获得的,如下图:
只要有@Transaction注解,就会把dataSource注入到当前线程:
上面可以看到有@Tranaction的方法是从TransactionSynchronizationManager获取sqlSession了,而没有的情况呢?从那获得sqlSession?
看下SqlSessionTemplate$SqlSessionInterceptor.invoke(Object, Method, Object[]) line: 434 (上面图里也有这个) ,代码:
SqlSession sqlSession = getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { Object result = method.invoke(sqlSession, args); if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {再进入getSqlSession方法,会发现它就是上图的org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionFactory, ExecutorType, PersistenceExceptionTranslator)方法。
所以带@Transaction的方法会多一个保存dataSource的方法,里面用的是同一个sqlSession,会引起错误.
如果没有,每次都是sessionFactory.openSession,这个sqlSession对应的dataSource就是执行时在DynamicRoutingDataSource(AbstractRoutingDataSource)动态获取的了。
还需要注意当前业务类使用的数据源是什么:
因为每个Service方法前都使用了aop设置了数据源,那么有这种情况 :
serviceA方法前设置了 datasourceA数据源
serviceA方法里面调用了另一个类的serviceB方法,它上面又设置了datasourceB数据源。
serviceA方法剩下的代码是用datasourceA还是datasourceB? 真实情况的都有可能用,不管当前用的是那个数据源,都可能需要换数据源。