由于项目里原来的数据分了几个库,有一部分数据来源不止一个库,需要配置多数据源

第一步:

在application-dev.properties中配置数据源信息

# 开发环境 #
#第一个数据源
spring.datasource.db_ku.driverClassName=com.mysql.jdbc.Driver
spring.datasource.db_ku.url=jdbc:mysql://ip:3306/ku?useUnicode=true&characterEncoding=utf-8&useSSL=false&tinyInt1isBit=false&allowMultiQueries=true
spring.datasource.db_ku.username=root
spring.datasource.db_ku.password=root

#第二个数据源
spring.datasource.db_ku1.driverClassName = com.mysql.jdbc.Driver
spring.datasource.db_ku1.url = jdbc:mysql://ip:3306/ku1?useUnicode=true&characterEncoding=utf-8&useSSL=false&tinyInt1isBit=false
spring.datasource.db_ku1.username = root
spring.datasource.db_ku1.password = root

第二步:

创建动态数据源上下文类DynamicDataSourceContextHolder

/**
 * 动态数据源切换上下文
 * Ami
 */
public class DynamicDataSourceContextHolder {
    // 默认的数据源名
    public static final String DEFAULT_DS = "ds_ku";
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    public static List<String> dataSourceIds = new ArrayList<String>();

    /**
     * 设置数据源名,绑定到线程
     *
     * @param dbName
     */
    public static void setDB(String dbName) {
        if (StringUtils.isBlank(dbName)) {
            dbName = DEFAULT_DS;
        }
        contextHolder.set(dbName);
    }

    /**
     * 获取数据源名
     *
     * @return
     */
    public static String getDB() {
        String dbName = contextHolder.get();
        if (StringUtils.isBlank(dbName)) {
            dbName = DEFAULT_DS;
        }
        return dbName;
    }

    /**
     * 清除数据源名
     */
    public static void clearDB() {
        contextHolder.remove();
    }

    /**
     * 判断数据源是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDB(String dataSourceId) {
        return dataSourceIds.contains(dataSourceId);
    }
}

此类的作用主要是配合下面的一个注解,在线程中绑定当前使用的数据源名称,在数据源路由中介AbstractRoutingDataSource中切换数据源来实现从不同的数据源中查询数据。

第三步:

自定义注解TargetDataSource

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
/**
 *@description 切换数据源注解,Aspect指定注解用在方法级别上
 *@author Ami
 *@date 2018/4/14  11:22
 */
public @interface TargetDataSource {
    String value();
}

这个注解配合第二步的上下文,在方法上使用,具体使用往下面看

第四步:

使用AOP来切换数据源和清除绑定在线程上的数据源信息

@Aspect
@Order(-1)
@Component
/**
 * 动态数据源切面类
 * Ami
 */
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    /**
     * 前置通知
     * @param point
     * @param ds
     */
    @Before("@annotation(ds)")
    public void before(JoinPoint point,TargetDataSource ds) {
        String dbName = ds.value();
        if (StringUtils.isNotBlank(dbName) && !DynamicDataSourceContextHolder.containsDB(dbName)) {
            logger.error("不存在的数据源:" + dbName);
            throw new NonExistDataSourceException("不存在的数据源:" + dbName); // 自定义异常
        }
        DynamicDataSourceContextHolder.setDB(dbName);
    }

    /**
     * 后置最终通知
     * @param point
     * @param ds
     */
    @After("@annotation(ds)")
    public void after(JoinPoint point,TargetDataSource ds) {
        DynamicDataSourceContextHolder.clearDB();
    }
}

AOP只会对加有TargetDataSource注解的方法进行拦截来切换数据源,其他的不加TargetDataSource注解的方法默认都是使用默认数据源。

第五步:

继承数据源路由中介AbstractRoutingDataSource,重写determineTargetDataSource方法

/**
 * 多数据源的路由中介
 * Ami
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("切换数据源为{},默认主数据源为{}", DynamicDataSourceContextHolder.getDB(), DynamicDataSourceContextHolder.DEFAULT_DS);
        return DynamicDataSourceContextHolder.getDB();
    }
}

在AbstractRoutingDataSource中源码:

/**
 * Retrieve the current target DataSource. Determines the
 * {@link #determineCurrentLookupKey() current lookup key}, performs
 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
 * falls back to the specified
 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
 * @see #determineCurrentLookupKey()
 */
protected DataSource determineTargetDataSource() {
   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   Object lookupKey = determineCurrentLookupKey(); // 注意这个方法就是下面那个,抽象的,需要我们自己实现
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
   }
   if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
   }
   return dataSource;
}

/**
 * Determine the current lookup key. This will typically be
 * implemented to check a thread-bound transaction context.
 * <p>Allows for arbitrary keys. The returned key needs
 * to match the stored lookup key type, as resolved by the
 * {@link #resolveSpecifiedLookupKey} method.
 */
protected abstract Object determineCurrentLookupKey(); // 自己实现

第六步:

配置数据源

/**
 * 多数据源的配置类
 * Ami
 */
@Configuration
public class DataSourceConfig {

    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;
    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;
    @Value("${mybatis.configuration.lazy-loading-enabled}")
    private Boolean lazyLoadingEnabled;
    @Value("${mybatis.configuration.aggressive-lazy-loading}")
    private Boolean aggressiveLazyLoading;
    @Value("${mybatis.page-helper.properties.reasonable}")
    private String reasonable;
    @Value("${mybatis.page-helper.properties.offsetAsPageNum}")
    private String offsetAsPageNum;
    @Value("${mybatis.page-helper.properties.rowBoundsWithCount}")
    private String rowBoundsWithCount;
    @Value("${mybatis.page-helper.properties.dialect}")
    private String dialect;

    /**
     * 主数据源
     * @return
     */
    @Bean(name = "ds_ku")
    @ConfigurationProperties(prefix = "spring.datasource.db_ku")
    public DataSource getDs_ku(){
        DynamicDataSourceContextHolder.dataSourceIds.add("ds_ku");
        return DataSourceBuilder.create().build();
    }

    /**
     * 配置第二个数据源
     * @return
     */
    @Bean(name = "ds_ku1")
    @ConfigurationProperties(prefix = "spring.datasource.db_ku1")
    public DataSource getDs_ku1(){
        DynamicDataSourceContextHolder.dataSourceIds.add("ds_ku1");
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "dynamicDataSource")
    public DataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源
        dynamicDataSource.setDefaultTargetDataSource(getDs_ku());
        // 配置多数据源
        Map<Object, Object> map = new HashMap(5);
        map.put("ds_ku", getDs_ku());
        map.put("ds_ku1", getDs_ku1());
        dynamicDataSource.setTargetDataSources(map);
        return dynamicDataSource;
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources(mapperLocations));
        bean.setTypeAliasesPackage(typeAliasesPackage);

        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setAggressiveLazyLoading(aggressiveLazyLoading);
        configuration.setLazyLoadingEnabled(lazyLoadingEnabled);
        bean.setConfiguration(configuration);
        // 配置分页插件
        PageHelper pageHelper = new PageHelper();
        Properties properties = new Properties();
        properties.setProperty("reasonable",reasonable);
        properties.setProperty("offsetAsPageNum", offsetAsPageNum);
        properties.setProperty("rowBoundsWithCount", rowBoundsWithCount);
        properties.setProperty("dialect", dialect);
        pageHelper.setProperties(properties);
        bean.setPlugins(new Interceptor[]{pageHelper});
        return bean.getObject();
    }

    // 事务管理
    @Bean(name = "transactionManager")
    public PlatformTransactionManager getDataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

这样在项目初始化时,将读取的数据源以及名称放入到路由中介中targetDataSources这个Map中了

上面的sqlSessionFactory配置的多了点,可以看个人情况删减,比如分页插件用不到就不要配置,mybatis的懒加载用不到也不需要配置,但是mapper的xml路径需要配置,因为我们重写了sqlSessionFactory,springboot就会使用我们自己的,这样AutoConfiguration就不起作用了,需要手动去配置

最后一步:

在项目入口类需要排除DataSourceAutoConfiguration类的自动配置

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

不然会报找到多个数据源的错误,排除掉后就会根据路由中介的key去配置数据源了

使用方式:

@TargetDataSource("ds_ku1")
public Employee getEmployeeById(Long empId) {
    Employee employee = employeeMapper.getEmployeeById(empId);
    return employee;
}

通过注解,在运行时就切换到ds_ku1数据源上了。

还有一种方式采用分包的方式,即在配置数据源时使用MapperScan扫描不同的mapper包,这样代理的时候就会采用不同的数据源,不过这样代码改动大,增加一种就要加个mapper包以及配置数据源,分的更细的话,service层也要分开,微服务的话肯定不行,因为这样的话就可以继续拆分项目了。

上面两种都是基于一个方法使用一个数据源,还有一种就是在同一个方法使用多个数据源,这样需要用到分布式数据源。有时间配置下springboot+Atomikos分布式事务,采用三阶段提交协议。后续。。。。