由于项目里原来的数据分了几个库,有一部分数据来源不止一个库,需要配置多数据源
第一步:
在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分布式事务,采用三阶段提交协议。后续。。。。