主要的思路:配置多個(gè)數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象中,根據(jù)實(shí)際的情況動(dòng)態(tài)的切換到相應(yīng)的數(shù)據(jù)源东涡。
架構(gòu)流程圖:
執(zhí)行的步驟:建立數(shù)據(jù)源->數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象->動(dòng)態(tài)數(shù)據(jù)源的配置->動(dòng)態(tài)切換
1、建立數(shù)據(jù)源
這一步比較簡(jiǎn)單折柠,根據(jù)連接池(例如:HikariCP蚣抗、c3p0)建立相關(guān)的數(shù)據(jù)源道偷,本例中使用HikariCP玖雁。
@Configuration// 配置數(shù)據(jù)源
@RefreshScope
public class DataSourceConfigure implements Ordered, EnvironmentAware {
private final Logger logger = LoggerFactory.getLogger(DataSourceConfigure.class);
private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
private Environment environment;
private Class dataSourceClass;
public DataSourceConfigure() {
Class dataSourceClass = null;
try {
dataSourceClass = Class.forName(DATASOURCE_TYPE_DEFAULT);
} catch (ClassNotFoundException e) {
logger.error("不存在類:" + DATASOURCE_TYPE_DEFAULT);
e.printStackTrace();
}
this.dataSourceClass = dataSourceClass;
}
@Bean
@RefreshScope
@Primary
public DataSource xxx() {
logger.info("注冊(cè)數(shù)據(jù)源:xxx");
return (DataSource) Binder.get(environment).bind( "custom.datasource.xxx", this.dataSourceClass).orElse(null);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
如上面的代碼段所示更扁,根據(jù)自己的實(shí)際內(nèi)容建立相應(yīng)的數(shù)據(jù)源。
2赫冬、數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象
這一步把相關(guān)的數(shù)據(jù)源增加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象中浓镜,直接上代碼。
@Configuration
@EnableAutoConfiguration
public class DatabaseConfiguration {
@Autowired
private ApplicationContext appContext;
private static final String DATASOURCE_PREFIX = "custom.datasource";
@Bean
public AbstractRoutingDataSource routingDataSource() {
RoutingDataSource proxy = new RoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap();
//獲取到所有數(shù)據(jù)源的名稱.
Map<String, Map<String, Object>> map = Binder.get(appContext.getEnvironment()).bind(DATASOURCE_PREFIX, Map.class).orElse(null);
/**
* 獲取每個(gè)數(shù)據(jù)源的屬性
*/
map.forEach((key, value) -> {
DataSource dataSource = (DataSource) appContext.getBean(key);
targetDataSources.put(key, dataSource);
if (value.get("isPrimary") != null && value.get("isPrimary").equals(1)) {
proxy.setDefaultTargetDataSource(dataSource);
}
});
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
}
3劲厌、動(dòng)態(tài)數(shù)據(jù)源的配置(根據(jù)實(shí)際的使用膛薛,配置相關(guān)的配置,本例中使用的是Spring-data-jpa补鼻。)
下面的代碼只是簡(jiǎn)單的配置一下JPA配置類哄啄,使得支持jpa的使用,需要根據(jù)實(shí)際情況修改辽幌。
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactoryDynamic",
transactionManagerRef = "transactionManagerDynamic",
basePackages = {"com.xxx.gbimclientdynamic.repository"},//設(shè)置Repository所在位置
repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class
)
public class DynamicConfig {
@Autowired
@Qualifier("routingDataSource") //配置中定義的名字
private DataSource routingDataSource;
@Bean(name = "entityManagerFactoryDynamic")
@Primary
public EntityManagerFactory entityManagerFactoryDynamic() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.xxx.gbimclientdynamic.entity");
factory.setDataSource(routingDataSource);//數(shù)據(jù)源
factory.setPersistenceUnitName("dynamicPersistenceUnit");
Properties properties = new Properties();
properties.put("hibernate.show_sql", true);
properties.put("hibernate.dialect","org.hibernate.dialect.SQLServer2012Dialect");
factory.setJpaProperties(properties);
factory.afterPropertiesSet();//在完成了其它所有相關(guān)的配置加載以及屬性設(shè)置后,才初始化
return factory.getObject();
}
@Bean(name = "transactionManagerDynamic")
@Primary
PlatformTransactionManager transactionManagerDynamic() {
return new JpaTransactionManager(entityManagerFactoryDynamic());
}
}
4增淹、動(dòng)態(tài)切換
這一步的這個(gè)例子的重點(diǎn)椿访,這里會(huì)說得稍微詳細(xì)一些乌企。
首先要考慮的就是怎么可以實(shí)現(xiàn)切換呢?Spring提供了AbstractRoutingDataSource來實(shí)現(xiàn)這樣的功能成玫,AbstractRoutingDataSource的功能是在其中可以根據(jù)key值動(dòng)態(tài)切換到具體的數(shù)據(jù)源加酵。
AbstractRoutingDataSource的具體的實(shí)現(xiàn)拳喻,可以看一下具體的實(shí)現(xiàn)代碼。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
......
......
}
由代碼中看到AbstractRoutingDataSource 是繼承于 AbstractDataSource 猪腕,而AbstractDataSource是DataSource 的一個(gè)實(shí)現(xiàn)類冗澈,所以這里主要是要看一下獲得連接的方法,即如下:
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
在getConnection()
方法中的determineTargetDataSource()
明顯就是確定數(shù)據(jù)源的方法陋葡,所以我們繼續(xù)看一下determineTargetDataSource()
這個(gè)方法亚亲。
/**
* 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;
}
在determineTargetDataSource()
中,determineCurrentLookupKey();
是獲取數(shù)據(jù)源dataSource的key值的腐缤,在AbstractRoutingDataSource
類中捌归,determineCurrentLookupKey()
是一個(gè)抽象方法,就該子類就必須重新該方法岭粤。接著根據(jù)determineCurrentLookupKey()
獲得的key值惜索,在this.resolvedDataSources
中獲得dataSource
。如果不存在剃浇,就根據(jù)默認(rèn)設(shè)置默認(rèn)的數(shù)據(jù)源巾兆。
所以,根據(jù)源碼虎囚,繼承AbstractRoutingDataSource
類角塑,并重寫其中的determineCurrentLookupKey()
方法,就可以實(shí)現(xiàn)數(shù)據(jù)源的切換溜宽。
上述內(nèi)容基本說明了如何切的問題吉拳,接下來需要考慮在何時(shí)切,并且應(yīng)該如何實(shí)現(xiàn)的問題适揉。
其實(shí)這里對(duì)簡(jiǎn)單的就是利用AOP了留攒,AOP的具體內(nèi)容可以自己去網(wǎng)上找一下,這里就不作詳細(xì)的介紹了嫉嘀。就是利用AOP在調(diào)用需要調(diào)用數(shù)據(jù)庫的方法前就設(shè)定好需要的數(shù)據(jù)源(利用上述的determineCurrentLookupKey()
)炼邀。
所以,動(dòng)態(tài)切換的大體思路是這樣的:
利用AOP剪侮,調(diào)用需要調(diào)用數(shù)據(jù)庫的方法前利用determineCurrentLookupKey()
設(shè)定好需要的數(shù)據(jù)源拭宁。
完整代碼如下:
//動(dòng)態(tài)數(shù)據(jù)源存放對(duì)象,保存key值瓣俯,用于動(dòng)態(tài)切換使用
public class DbContextHolder {
//線程本地環(huán)境杰标,ThreadLocal 是用于線性安全的
private static final ThreadLocal<String> dataSources = new ThreadLocal();
//設(shè)置數(shù)據(jù)源
public static void setDataSource(String customerType) {
dataSources.set(customerType);
}
//獲取數(shù)據(jù)源
public static String getDataSource() {
return dataSources.get();
}
//清除數(shù)據(jù)源
public static void clearDataSource() {
dataSources.remove();
}
}
//用于動(dòng)態(tài)切換,就是上面說的源碼的這部分
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder .getDataSource();
}
}
//定義一個(gè)注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DbName {
String value() default "";
}
//切面
@Aspect
@Component
public class DataSourceAspect {
private Logger logger = LoggerFactory.getLogger(getClass().getName());
//作用于service層 ||@args(com.xxx.connection.aop.RoutingDbName)
//這個(gè)匹配能匹配到類彩匕,方法腔剂,無法匹配到參數(shù)
@Pointcut("execution(public * com.xxx..*.service.impl..*.*(..))&&(@target(com.xxx.connection.aop.DbName)||@annotation(com.xxx.connection.aop.DbName))||execution(public * com.xxx..*.service.impl..*.*(.., @DbName(*), ..))")
public void routingDsPointCut() {
}
//方法切面,不指定注解的值時(shí)驼仪,第一個(gè)參數(shù)作為dbName掸犬,指定注解的值袜漩,可指定數(shù)據(jù)源,優(yōu)先級(jí):類->方法->參數(shù)
@Around("routingDsPointCut()")
public Object proceeRouting(ProceedingJoinPoint joinpoint) throws Throwable {
logger.info("set connection on service");
Object result = null;
MethodSignature signature = (MethodSignature) joinpoint.getSignature();
Object[] args = joinpoint.getArgs();
DbName dbName = null;
String dbStr = "";
//獲取切面方法
Method method = signature.getMethod();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();
//獲取參數(shù)注解
for(int i =0;i<parameterAnnotations.length;i++ ){
Annotation[] annotations = parameterAnnotations[i];
Class parameterType = parameterTypes[i];
for(int j=0;j<annotations.length;j++){
Annotation annotation = annotations[j];
if(annotation instanceof DbName && "String".equals(parameterType.getSimpleName())){
dbName = (DbName) annotation;
dbStr = (String) args[j];
logger.info("取得參數(shù)的注解湾碎!");
}
}
}
//獲取方法注解
if(dbName==null){
dbName = method.getAnnotation(DbName.class);
if(dbName!=null){
logger.info("取得方法的注解宙攻!");
}
}
//獲取類注解
if(dbName==null){
String targetClass = joinpoint.getTarget().getClass().getName();
//獲取切面所在類
Class clazz = Class.forName(targetClass);
dbName =(DbName) clazz.getAnnotation(DbName.class);
if(dbName!=null){
logger.info("取得類的注解!");
}
}
//切換數(shù)據(jù)源
if(dbName!=null){
if(dbName.value() != null&&!"".equals(dbName.value())){ //指定數(shù)據(jù)庫時(shí)
DbContextHolder.setDataSource(dbName.value());
logger.info("使用注解的值介褥,當(dāng)前使用"+dbName.value()+"數(shù)據(jù)源座掘!");
}else if(!"".equals(dbStr)){
DbContextHolder.setDataSource(dbStr);
logger.info("使用參數(shù)的值,當(dāng)前使用"+dbStr+"數(shù)據(jù)源柔滔!");
}else if(args.length>0){
DbContextHolder.setDataSource((String)args[0]);
logger.info("使用第一個(gè)參數(shù)的值雹顺,當(dāng)前使用"+(String)args[0]+"數(shù)據(jù)源!");
}else { //沒有指定數(shù)據(jù)源
logger.info("注解的值不能為空廊遍!使用默認(rèn)數(shù)據(jù)源嬉愧!");
}
}else{
logger.info("service不切換數(shù)據(jù)源!");
}
result = joinpoint.proceed();//執(zhí)行前后
logger.info("數(shù)據(jù)源切換完畢喉前!");
return result;
}
}
5没酣、拓展
上述的四步已經(jīng)基本的說明了一個(gè)簡(jiǎn)單的動(dòng)態(tài)數(shù)據(jù)源切換了,但是上面提交的數(shù)據(jù)源是已經(jīng)確定了卵迂,如果數(shù)據(jù)源數(shù)據(jù)源還不確定的情況下裕便,能不能動(dòng)態(tài)的生成數(shù)據(jù)源動(dòng)態(tài)的切換呢?答案是肯定的见咒。這就是本部分要說的內(nèi)容偿衰。
其實(shí)實(shí)現(xiàn)的思路也很簡(jiǎn)單:在切換前看看有沒有需要切換的數(shù)據(jù)源,如果沒有改览,就根據(jù)相關(guān)的配置生成下翎。所以,內(nèi)容基本和上述的四步很類似宝当,唯一不一樣的就是繼承AbstractRoutingDataSource
類重新的determineCurrentLookupKey()
方法视事。直接上代碼:
public class RoutingDataSource extends AbstractRoutingDataSource {
/**
* 默認(rèn)值數(shù)據(jù)源類型,如需要?jiǎng)e的數(shù)據(jù)源需要修改
*/
private final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
/**
* 別名
*/
private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
private Logger logger = LoggerFactory.getLogger(getClass().getName());
@Override
protected Object determineCurrentLookupKey() {
//需要切換的數(shù)據(jù)庫
String dbName = DbContextHolder.getDataSource();
if (dbName == null) {
return null;
}
dbName=dbName.toLowerCase();
try {
/**
* 獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
*`
**/
Map<Object, Object> targetSourceMap = getTargetSource();
synchronized (this) {
//判斷targetDataSources中是否已經(jīng)存在要設(shè)置的數(shù)據(jù)源bean
// 存在的話,則直接返回beanName
// 不存在的話庆揩,則需要建立數(shù)據(jù)源
if (!targetSourceMap.keySet().contains(dbName)) {
logger.info("數(shù)據(jù)不存在俐东,建立數(shù)據(jù)源。");
//建立數(shù)據(jù)源
Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
logger.info("建立數(shù)據(jù)源成功订晌。");
/**
* 在創(chuàng)建后的bean,放入到targetDataSources Map中
* **/
targetSourceMap.put(dbName, dataSource);
logger.info("數(shù)據(jù)源放入到targetDataSources Map中虏辫。");
//通知spring有bean更新
super.afterPropertiesSet();
logger.info("通知spring有bean更新");
}else{
logger.info("數(shù)據(jù)已存在,切換到相應(yīng)的數(shù)據(jù)源");
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return DbContextHolder.getDataSource().toLowerCase();
}
//獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
@SuppressWarnings("unchecked")
public Map<Object, Object> getTargetSource() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
Field field = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
field.setAccessible(true);
return (Map<Object, Object>) field.get(this);
}
/**
* 根據(jù)數(shù)據(jù)源信息在spring中創(chuàng)建bean,并返回
* @param dbName 數(shù)據(jù)源信息
* @return 數(shù)據(jù)源
* @throws IllegalAccessException
*/
public Object createDataSource(String dbName,Environment environment) throws Exception {
Class clazz = Class.forName(DATASOURCE_TYPE_DEFAULT);
//獲取參數(shù)
Map config=getBeanDef(environment,dbName);
// 通過類型綁定參數(shù)并獲得實(shí)例對(duì)象锈拨,若數(shù)據(jù)源修改時(shí)砌庄,這個(gè)需要修改
DataSource consumerDatasource = bind(clazz, config);
return consumerDatasource;
}
//獲取參數(shù)
private Map<String, Object> getBeanDef(Environment environment,String dbName) {
Map<String, Object> dataSourceMap = (Map)Binder.get(environment).bind("custom.datasource", Map.class).orElse(null);
if(dataSourceMap != null && !dataSourceMap.isEmpty()) {
Iterator var4 = dataSourceMap.entrySet().iterator();
if(var4.hasNext()){
Map.Entry<String, Map<String, Object>> entry = (Map.Entry)var4.next();
String jdbcurl=null!=entry.getValue().get("jdbcurl")?entry.getValue().get("jdbcurl").toString():"";
jdbcurl=jdbcurl.toLowerCase().replace(entry.getKey().toLowerCase(),dbName);
entry.getValue().put("jdbcurl",jdbcurl);
return entry.getValue();
}
}
return dataSourceMap;
}
//綁定數(shù)據(jù)源
private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
// 通過類型綁定參數(shù)并獲得實(shí)例對(duì)象
return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
}
}
在這里determineCurrentLookupKey()
方法與第四步不相同的地方主要是這部分:
/**
* 獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
*`
**/
Map<Object, Object> targetSourceMap = getTargetSource();
synchronized (this) {
//判斷targetDataSources中是否已經(jīng)存在要設(shè)置的數(shù)據(jù)源bean
// 存在的話,則直接返回beanName
// 不存在的話,則需要建立數(shù)據(jù)源
if (!targetSourceMap.keySet().contains(dbName)) {
logger.info("數(shù)據(jù)不存在,建立數(shù)據(jù)源鹤耍。");
//建立數(shù)據(jù)源
Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
logger.info("建立數(shù)據(jù)源成功。");
/**
* 在創(chuàng)建后的bean,放入到targetDataSources Map中
* **/
targetSourceMap.put(dbName, dataSource);
logger.info("數(shù)據(jù)源放入到targetDataSources Map中验辞。");
//通知spring有bean更新
super.afterPropertiesSet();
logger.info("通知spring有bean更新");
}else{
logger.info("數(shù)據(jù)已存在稿黄,切換到相應(yīng)的數(shù)據(jù)源");
}
簡(jiǎn)單的看一下,這部分其實(shí)是和剛剛提到的思路完全一致的跌造。首先判斷一下有沒有需要的數(shù)據(jù)源(if (!targetSourceMap.keySet().contains(dbName))
)杆怕,如果沒有的情況建立數(shù)據(jù)源(createDataSource(dbName,EnvironmentUtil.getEnvironment())
),然后加到AbstractRoutingDataSource
的targetDataSources
屬性中(targetSourceMap.put(dbName, dataSource);
)壳贪,最后通知spring有bean更新(super.afterPropertiesSet();
)陵珍。