使用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? 真实情况的都有可能用,不管当前用的是那个数据源,都可能需要换数据源。

文/中中 浏览次数:0次   2018-09-11 09:56:03

相关阅读

微信扫描-捐赠支持
加入QQ群-技术交流

评论: