记录springboot多数据源的配置
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。前一个灵活性更高,具体看自己喜欢。