一、实现目标:
1) 可以灵活的在项目中添加新的数据源
2) 可以通过注解的方式动态切换数据源
3) 集成HttpClient实现api的远程调用
二、实现思路:
单数据源的时候,只要把初始化的数据源注入到对应的事务管理器TransactionManager和SqlSessionFactoryBean
中去即可;
多数剧源的时候,只需要定义一个自定义的数据源类,把其代替单数据源注入对应的事务管理器TransactionManager
和SqlSessionFactoryBean中去即可;
自定义的数据源,需要包含项目所有的的数据源信息,而且最好设置一个默认的数据源
三、实现方法:
根据实现思路,根据现有框架所支持的,自定义的数据源类只需继承AbstractRoutingDataSource类,重写
determineCurrentLookupKey方法即可
四、需要解决的问题:
1) 如何在项目启动的时候根据配置文件的信息,初始化所有的数据源,并把所有的数据源注入自定义的数据源类
2) 如何在执行业务方法的时候,自动切换数据源
问题1的解决方案: 利用下面注解实现根据属性文件初始化数据源信息
@EnableConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "master.datasource.druid")
@PropertySource(value = "classpath:masterdb.properties")
利用下面的注解,把数据源信息注入到自定义的数据源中去
@Autowired
1.1) 定义一个基类
package com.roger.core.config;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import javax.sql.DataSource;
import java.sql.SQLException;
@Data
public class DBConfig {
protected String driverClassName;
protected String url;
protected String userName;
protected String password;
protected int initialSize;
protected int minIdle;
protected int maxActive;
protected long maxWait;
protected boolean poolPreparedStatements;
protected String validationQuery;
protected boolean testOnBorrow;
protected boolean testOnReturn;
protected boolean testWhileIdle;
protected long timeBetweenEvictionRunsMillis;
protected long minEvictableIdleTimeMillis;
protected String filters;
public DataSource initDruidDataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(userName);
druidDataSource.setPassword(password);
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMaxWait(maxWait);
druidDataSource.setPoolPreparedStatements(poolPreparedStatements);
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.setTestOnBorrow(testOnBorrow);
druidDataSource.setTestOnReturn(testOnReturn);
druidDataSource.setTestWhileIdle(testWhileIdle);
druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
druidDataSource.setFilters(filters);
return druidDataSource;
}
}
1.2) 定义主数据源信息类
package com.roger.core.config.datasource;
import com.roger.core.config.DBConfig;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Data
@Configuration
@ConfigurationProperties(prefix = "master.datasource.druid")
@PropertySource(value = "classpath:masterdb.properties")
public class MasterDBConfig extends DBConfig {
}
1.3) 定义从数据源信息类
package com.roger.core.config.datasource;
import com.roger.core.config.DBConfig;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Data
@Configuration
@ConfigurationProperties(prefix = "slave.datasource.druid")
@PropertySource(value = "classpath:slavedb.properties")
public class SlaveDBConfig extends DBConfig {
}
1.4) 自定义需要注入事务管理器TransactionManager和SqlSessionFactoryBean中的数据源类
package com.roger.core.config;
import com.roger.core.config.datasource.MasterDBConfig;
import com.roger.core.config.datasource.SlaveDBConfig;
import com.roger.core.enumeration.DataSourceType;
import com.roger.core.multiple.DataSourceContextHolder;
import com.roger.core.multiple.MultipleDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@Configuration
@Import({MasterDBConfig.class, SlaveDBConfig.class})
@Slf4j
public class DynamicDataSource {
@Autowired
private MasterDBConfig masterDBConfig;
@Autowired
private SlaveDBConfig slaveDBConfig;
@Bean(name = "multipleDataSource")
@Primary
public DataSource multipleDataSource() throws SQLException {
MultipleDataSource multipleDataSource = new MultipleDataSource();
DataSource masterDataSource = masterDBConfig.initDruidDataSource();
log.info("设置默认数据源{}", masterDataSource);
multipleDataSource.setDefaultTargetDataSource(masterDataSource);
log.info("开始配置多数据源");
Map<Object, Object> targetDataSources = new HashMap<>();
addTargetDataSources(targetDataSources, DataSourceType.MASTER, masterDataSource);
log.info("主数据源添加成功...");
addTargetDataSources(targetDataSources, DataSourceType.SLAVE, slaveDBConfig.initDruidDataSource());
log.info("从数据源添加成功...");
multipleDataSource.setTargetDataSources(targetDataSources);
log.info("配置多数据源完成");
return multipleDataSource;
}
private void addTargetDataSources(Map<Object, Object> targetDataSources, DataSourceType dataSourceType, DataSource dataSource) {
targetDataSources.put(dataSourceType.name(), dataSource);
DataSourceContextHolder.dataSourceIds.add(dataSourceType.name());
}
}
package com.roger.core.multiple;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
@Slf4j
public class MultipleDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String currentDataSource = DataSourceContextHolder.getDataSource();
if(currentDataSource == null){
log.info("未设置数据源,使用默认的数据源");
}else{
log.info("当前使用的数据源为{}",currentDataSource);
}
return currentDataSource;
}
}
package com.roger.core.multiple;
import java.util.ArrayList;
import java.util.List;
public class DataSourceContextHolder {
/**
* 当使用TreadLocal维护变量时,ThreadLocal为每个使用这个变量的线程
* 维护一个变量副本
* 因此每一个线程都能独立的改变自己的副本,而不会影响其他线程的所对应的副本
*/
private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();
/**
* 管理所有的数据源ID,主要了判断数据源是否存在
*/
public static List<String> dataSourceIds = new ArrayList<String>();
/**
* 设置数据源
* @param db
*/
public static void setDataSource(String db){
contextHolder.set(db);
}
/**
* 取得当前数据源
* @return
*/
public static String getDataSource(){
return contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clear(){
contextHolder.remove();
}
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
1.5) 一个总的Spring容器类
package com.roger.core.config;
import com.roger.core.config.datasource.MasterDBConfig;
import com.roger.core.config.datasource.SlaveDBConfig;
import com.roger.core.constant.SystemConstant;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* Spring管理容器
*/
@Configuration
@EnableConfigurationProperties({MasterDBConfig.class,SlaveDBConfig.class})
@ComponentScan(value = SystemConstant.COMPONENT_SCAN_PACKAGE)
@EnableAspectJAutoProxy//启动aop切面功能
public class SpringConfig {
}
问题2的解决方案: 利用aop切面功能和注解实现数据源的动态切换功能
2.1) 定义一个数据源切换时需要的注解类
package com.roger.core.annotation;
import com.roger.core.enumeration.DataSourceType;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
DataSourceType value() default DataSourceType.MASTER;
}
2.2) 定义一个数据源对应的枚举类
package com.roger.core.enumeration;
public enum DataSourceType {
MASTER,SLAVE;
}
2.3) 定义aop切面类
package com.roger.core.aspect;
import com.roger.core.annotation.TargetDataSource;
import com.roger.core.multiple.DataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@Component
@Aspect
@Order(-10)//保证切面的执行时间在@Transactional之前
public class DynamicDataSourceAspect {
@Pointcut("execution(* com.roger.biz.service..*.*(..)) || execution(* com.roger.core.service..*.*(..))")
public void selectDataSource(){
}
@After("selectDataSource()")
public void after(){
DataSourceContextHolder.clear();
}
@Before("selectDataSource()")
public void before(JoinPoint joinPoint){
Class<?> targetCls = joinPoint.getTarget().getClass();
TargetDataSource targetDataSourceAnno = targetCls.getAnnotation(TargetDataSource.class);
if(targetDataSourceAnno != null){
DataSourceContextHolder.setDataSource(targetDataSourceAnno.value().name());
return;
}
//获取即将要执行方法
Method method = getInvokedMethod(targetCls, joinPoint);
if (method == null) {
return;
}
targetDataSourceAnno = method.getAnnotation(TargetDataSource.class);
if(targetDataSourceAnno != null){
DataSourceContextHolder.setDataSource(targetDataSourceAnno.value().name());
}
}
private Method getInvokedMethod(Class targetCls, JoinPoint pJoinPoint) {
List<Class<? extends Object>> clazzList = new ArrayList<>();
Object[] args = pJoinPoint.getArgs();
for (Object arg : args) {
clazzList.add(arg.getClass());
}
Class[] argsCls = (Class[]) clazzList.toArray(new Class[0]);
String methodName = pJoinPoint.getSignature().getName();
Method method = null;
try {
method = targetCls.getMethod(methodName, argsCls);
} catch (NoSuchMethodException e) {
//不做任何处理,这个切面只处理事务相关逻辑
//其他任何异常不在这个切面的考虑范围
}
return method;
}
}