1.要实现多数据源切换,肯定不能让springboot自动配置数据源,所以启动时,不设置自动配置数据,在启动类上使用一下代码
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
2.Spring中AbstractRoutingDataSource类的方法determineTargetDataSource() 是决定当前线程所用数据源的方法,如下源码
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;
}
resolvedDataSources是一共可以有多少个数据源,resolvedDefaultDataSource是默认的数据源,lenientFallback默认为true,determineCurrentLookupKey()是个抽象方法,确定当前查找键,用于检查线程绑定的事务上下文。
因此写一个类继承AbstractRoutingDataSource,实现determineCurrentLookupKey()是必须的
public class DynamicDataSource extends AbstractRoutingDataSource {
private static Map<String, String> dataBaseDataSourceMapping = new HashMap<>();
private static List<String> dataBases = new ArrayList<>();
/**
* 数据源路由,此方法用于产生要选取的数据源逻辑名称
*/
@Override
protected Object determineCurrentLookupKey() {
//从共享线程中获取数据源名称
return DataSourceContextHolder.getDataSource();
}
//从数据库获得<数据库:数据源>集合,然后根据数据库得到该使用的数据源key
public static String getDataSource(String dataBase) {
if (dataBaseDataSourceMapping.isEmpty()) {
DataBaseDataSourceConfigMapper mapper = SpringUtils.getBean(DataBaseDataSourceConfigMapper.class);
List<DataBaseDataSourceConfig> configs = mapper.findAllConfig();
configs.forEach(config ->
dataBaseDataSourceMapping.put(config.getDataBase(), config.getDataSource()));
}
return dataBaseDataSourceMapping.get(dataBase);
}
//所有的数据源key 的枚举
public enum DataSourceType {
DEFAULT("oghma"),
OGHMA("oghma"),
DW("dw"),
QUARTZ("quartz"),
CLS("cls"),
OLD("old");
private String name;
DataSourceType(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
DataSourceContextHolder存放本地线程共享对象,ThreadLocal作用不赘述
public class DataSourceContextHolder {
private final static ThreadLocal<String> local = new ThreadLocal<>();
public static void setDataSource(String name) {
local.set(name);
}
public static String getDataSource() {
return local.get();
}
public static void removeDataSource() {
local.remove();
}
}
3.配置所有数据源的信息,这里只给出一个模式,并指定事务所依赖的数据源
application.properties
#分库
spring.datasource.cls.url=XXXXX
spring.datasource.cls.username=XX
spring.datasource.cls.password=XX
spring.datasource.cls.driver-class-name=XXX
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.cls.url}")
private String clsUrl;
@Value("${spring.datasource.cls.username}")
private String clsUserName;
@Value("${spring.datasource.cls.password}")
private String clsPassword;
@Value("${spring.datasource.cls.driver-class-name}")
private String clsDriver;
@Bean(name = "clsDataSource")
public DataSource dataSourceCls() {
return DataSourceBuilder.create().type(HikariDataSource.class)
.driverClassName(clsDriver)
.url(clsUrl)
.username(clsUserName)
.password(clsPassword).build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DataSource dataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
DataSource oghma = dataSourceOghma();
DataSource dw = dataSourceDW();
DataSource quartz = dataSourceQuartz();
DataSource cls = dataSourceCls();
DataSource old = dataSourceOld();
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(old);
//配置多个数据源
Map<Object,Object> map = new HashMap<>();
map.put(DataSourceType.OGHMA.getName(), oghma);
map.put(DataSourceType.DW.getName(), dw);
map.put(DataSourceType.QUARTZ.getName(), quartz);
map.put(DataSourceType.CLS.getName(), cls);
map.put(DataSourceType.OLD.getName(), old);
dynamicDataSource.setTargetDataSources(map);
return dynamicDataSource;
}
//事务管理器
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
}
配置数据源有一种简单写法,使用@ConfigurationProperties
@Bean(name = "clsDataSource")// 读取application.properties中的配置参数映射成为一个对象,prefix表示参数的前缀
@ConfigurationProperties(prefix = "spring.datasource.cls")
public DataSource dataSourceCls() {
return DataSourceBuilder.create().build();
}
当然,可以使用@PropertySource(value= {"classpath:jdbc.properties"}),将配置信息从application.properties移至jdbc.properties文件。
4.写一个注解,一个切面(一个拦截指定方法,另一个拦截指定类的所有方法),切面拦截并设置当前方法改使用哪个数据源,没有注解,就使用默认数据源.为了防止事务注解和自定义注解同时使用出现错误(当切事务面先执行的时候,线程还没有确定数据源,会报错),这里使用@Order指定自定义切面先于事务切面执行。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface TargetDataSource {
/**
* 如果确定用哪个数据源,可手动指定数据源名称
*/
DataSourceType value() default DataSourceType.OGHMA;
/**
* 如无法确定数据源,则指定数据库,系统自动查找数据源
*/
String dataBase() default "";
}
@Aspect
@Component
@Order(1)
public class DataSourceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 切入点:service类的方法上(这个包的子包及所有包的里面的以Service结尾的类的任意方法名任意参数的方法)
//@Pointcut("execution(* com.topideal.supplychain..*Service..*(..))")
@Pointcut("@annotation(com.topideal.supplychain.oghma.annotation.TargetDataSource)")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
private void before(JoinPoint joinPoint) {
try {
Method m = getMethod(joinPoint);
// 如果 m 上存在切换数据源的注解,则根据注解内容进行数据源切换;如果不存在,则使用默认数据源
if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource data = m.getAnnotation(TargetDataSource.class);
if (!StringUtils.isEmpty(data.dataBase())) { // 配置了数据库,根据数据库得到数据源
String dataBase = (String) resolver(joinPoint, data.dataBase());
String dataSource = DynamicDataSource.getDataSource(dataBase);
if (!StringUtils.isEmpty(dataSource)) {
DataSourceContextHolder.setDataSource(dataSource);
} else {
throw new RuntimeException("dataBase : " + dataBase + " 没有数据源!");
}
} else { // 指定数据源
DataSourceContextHolder.setDataSource(data.value().getName());
}
//logger.info("》》》》》》》 current thread " + Thread.currentThread().getName() + " add 【 " + data.value().getName() + " 】 to ThreadLocal");
}
} catch (Exception e) {
DataSourceContextHolder.setDataSource(DataSourceType.DEFAULT.getName());
e.printStackTrace();
}
}
// 执行完切面后,将线程共享中的数据源名称清空
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint) {
DataSourceContextHolder.removeDataSource();
}
private Method getMethod(JoinPoint pjp) throws Exception {
Signature sig = pjp.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("该注解只能用于方法");
} else {
msig = (MethodSignature) sig;
Object target = pjp.getTarget();
Method currentMethod = target.getClass().getDeclaredMethod(msig.getName(), msig.getParameterTypes());
return currentMethod;
}
}
public Object resolver(JoinPoint joinPoint, String str) {
if (str == null) return null;
Object value = null;
if (str.matches("#\\{\\D*\\}")) {// 如果name匹配上了#{},则把内容当作变量
String newStr = str.replaceAll("#\\{", "").replaceAll("\\}", "");
if (newStr.contains(".")) { // 复杂类型
try {
value = complexResolver(joinPoint, newStr);
} catch (Exception e) {
e.printStackTrace();
}
} else {
value = simpleResolver(joinPoint, newStr);
}
} else { //非变量
value = str;
}
return value;
}
private Object complexResolver(JoinPoint joinPoint, String str) throws Exception {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] names = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
String[] strs = str.split("\\.");
for (int i = 0; i < names.length; i++) {
if (strs[0].equals(names[i])) {
Object obj = args[i];
Method dmethod = obj.getClass().getDeclaredMethod(getMethodName(strs[1]), null);
Object value = dmethod.invoke(args[i]);
return getValue(value, 1, strs);
}
}
return null;
}
private Object getValue(Object obj, int index, String[] strs) {
try {
if (obj != null && index < strs.length - 1) {
Method method = obj.getClass().getDeclaredMethod(getMethodName(strs[index + 1]), null);
obj = method.invoke(obj);
getValue(obj, index + 1, strs);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private String getMethodName(String name) {
return "get" + name.replaceFirst(name.substring(0, 1), name.substring(0, 1).toUpperCase());
}
private Object simpleResolver(JoinPoint joinPoint, String str) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] names = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < names.length; i++) {
if (str.equals(names[i])) {
return args[i];
}
}
return null;
}
}
@Aspect
@Component
@Order(6)
public class DataSourceAspect2 {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("@within(com.topideal.supplychain.oghma.annotation.TargetDataSource)")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
private void before(JoinPoint joinPoint) {
try {
Class<?> aClass = joinPoint.getTarget().getClass();
TargetDataSource data = aClass.getAnnotation(TargetDataSource.class);
if (data != null) {
if (StringUtils.isEmpty(data.dataBase())) {
DataSourceContextHolder.setDataSource(data.value().getName());
} else {
// 配置了数据库,根据数据库得到数据源
String dataSource = DynamicDataSource.getDataSource(data.dataBase());
if (!StringUtils.isEmpty(dataSource)) {
DataSourceContextHolder.setDataSource(dataSource);
} else {
throw new RuntimeException("dataBase : " + data.dataBase() + " 没有数据源!");
}
}
}
} catch (Exception e) {
DataSourceContextHolder.setDataSource(DataSourceType.DEFAULT.getName());
}
}
// 执行完切面后,将线程共享中的数据源名称清空
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint) {
DataSourceContextHolder.removeDataSource();
}
}
DataSourceAspect直接写方法上即可,DataSourceAspect2可注解在抽象类上,假设@TargetDataSource(DataSourceType.DW),继承该抽象类的类,都使用数据源DW。前一个灵活性更高,具体看自己喜欢。