SpringBoot+Mybatis+Druid動態多資料來源
背景
前兩天突然想起了,咕泡老師寫的原始碼中有關於多資料來源的實現。翻出來看了看,想移植到springboot裡面去,可是移動過去,不起作用,而後又百度了些大神做法,還是不起作用,故自己研究了一番,最終實現了mybatis的動態資料來源。水平有限,還請大佬輕噴,希望能和各位大佬多多交流。
配置多資料來源
application.yml配置:
# spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8 username: root password: root driver-class-name: com.mysql.jdbc.Driver initialSize: 1 minIdle: 1 maxActive: 200 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: false maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,log4j,wall connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 useGlobalDataSourceStat: true datasource2: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/db_test3?useUnicode=true&characterEncoding=utf8 username: root password: root driver-class-name: com.mysql.jdbc.Driver initialSize: 1 minIdle: 1 maxActive: 200 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: false maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,log4j,wall connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 useGlobalDataSourceStat: true mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.liulei.study.xmlbatisboot.domain default: dataSource: dataSource1
我這裡配置了兩個mysql資料來源。配置bean,dataSource1 如下:
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.sql.SQLException; //解決 spring.datasource.filters=stat,wall,log4j 無法正常註冊進去 @Configuration @ConfigurationProperties(prefix = "spring.datasource") public class IDataSource1 { private String url; private String username; private String password; private String driverClassName; private int initialSize; private int minIdle; private int maxActive; private int maxWait; private int timeBetweenEvictionRunsMillis; private int minEvictableIdleTimeMillis; private String validationQuery; private boolean testWhileIdle; private boolean testOnBorrow; private boolean testOnReturn; private boolean poolPreparedStatements; private int maxPoolPreparedStatementPerConnectionSize; private String filters; private String connectionProperties; private boolean useGlobalDataSourceStat; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getMinIdle() { return minIdle; } public void setMinIdle(int minIdle) { this.minIdle = minIdle; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMaxWait() { return maxWait; } public void setMaxWait(int maxWait) { this.maxWait = maxWait; } public int getTimeBetweenEvictionRunsMillis() { return timeBetweenEvictionRunsMillis; } public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) { this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; } public int getMinEvictableIdleTimeMillis() { return minEvictableIdleTimeMillis; } public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public boolean isTestWhileIdle() { return testWhileIdle; } public void setTestWhileIdle(boolean testWhileIdle) { this.testWhileIdle = testWhileIdle; } public boolean isTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } public boolean isTestOnReturn() { return testOnReturn; } public void setTestOnReturn(boolean testOnReturn) { this.testOnReturn = testOnReturn; } public boolean isPoolPreparedStatements() { return poolPreparedStatements; } public void setPoolPreparedStatements(boolean poolPreparedStatements) { this.poolPreparedStatements = poolPreparedStatements; } public int getMaxPoolPreparedStatementPerConnectionSize() { return maxPoolPreparedStatementPerConnectionSize; } public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) { this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize; } public String getFilters() { return filters; } public void setFilters(String filters) { this.filters = filters; } public String getConnectionProperties() { return connectionProperties; } public void setConnectionProperties(String connectionProperties) { this.connectionProperties = connectionProperties; } public boolean isUseGlobalDataSourceStat() { return useGlobalDataSourceStat; } public void setUseGlobalDataSourceStat(boolean useGlobalDataSourceStat) { this.useGlobalDataSourceStat = useGlobalDataSourceStat; } @Bean(name="dataSource1") //宣告其為Bean例項 public DataSource dataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(url); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); //configuration datasource.setInitialSize(initialSize); datasource.setMinIdle(minIdle); datasource.setMaxActive(maxActive); datasource.setMaxWait(maxWait); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setValidationQuery(validationQuery); datasource.setTestWhileIdle(testWhileIdle); datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnReturn(testOnReturn); datasource.setPoolPreparedStatements(poolPreparedStatements); datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); try { datasource.setFilters(filters); } catch (SQLException e) { System.err.println("druid configuration initialization filter: " + e); } datasource.setConnectionProperties(connectionProperties); return datasource; } }
dataSource2 如下:
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.sql.SQLException; //解決 spring.datasource.filters=stat,wall,log4j 無法正常註冊進去 @Configuration @ConfigurationProperties(prefix = "spring.datasource2") public class IDataSource2 { private String url; private String username; private String password; private String driverClassName; private int initialSize; private int minIdle; private int maxActive; private int maxWait; private int timeBetweenEvictionRunsMillis; private int minEvictableIdleTimeMillis; private String validationQuery; private boolean testWhileIdle; private boolean testOnBorrow; private boolean testOnReturn; private boolean poolPreparedStatements; private int maxPoolPreparedStatementPerConnectionSize; private String filters; private String connectionProperties; private boolean useGlobalDataSourceStat; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getMinIdle() { return minIdle; } public void setMinIdle(int minIdle) { this.minIdle = minIdle; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMaxWait() { return maxWait; } public void setMaxWait(int maxWait) { this.maxWait = maxWait; } public int getTimeBetweenEvictionRunsMillis() { return timeBetweenEvictionRunsMillis; } public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) { this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; } public int getMinEvictableIdleTimeMillis() { return minEvictableIdleTimeMillis; } public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public boolean isTestWhileIdle() { return testWhileIdle; } public void setTestWhileIdle(boolean testWhileIdle) { this.testWhileIdle = testWhileIdle; } public boolean isTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } public boolean isTestOnReturn() { return testOnReturn; } public void setTestOnReturn(boolean testOnReturn) { this.testOnReturn = testOnReturn; } public boolean isPoolPreparedStatements() { return poolPreparedStatements; } public void setPoolPreparedStatements(boolean poolPreparedStatements) { this.poolPreparedStatements = poolPreparedStatements; } public int getMaxPoolPreparedStatementPerConnectionSize() { return maxPoolPreparedStatementPerConnectionSize; } public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) { this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize; } public String getFilters() { return filters; } public void setFilters(String filters) { this.filters = filters; } public String getConnectionProperties() { return connectionProperties; } public void setConnectionProperties(String connectionProperties) { this.connectionProperties = connectionProperties; } public boolean isUseGlobalDataSourceStat() { return useGlobalDataSourceStat; } public void setUseGlobalDataSourceStat(boolean useGlobalDataSourceStat) { this.useGlobalDataSourceStat = useGlobalDataSourceStat; } @Bean(name="dataSource2") //宣告其為Bean例項 public DataSource dataSource2() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(url); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); //configuration datasource.setInitialSize(initialSize); datasource.setMinIdle(minIdle); datasource.setMaxActive(maxActive); datasource.setMaxWait(maxWait); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setValidationQuery(validationQuery); datasource.setTestWhileIdle(testWhileIdle); datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnReturn(testOnReturn); datasource.setPoolPreparedStatements(poolPreparedStatements); datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); try { datasource.setFilters(filters); } catch (SQLException e) { System.err.println("druid configuration initialization filter: " + e); } datasource.setConnectionProperties(connectionProperties); return datasource; } }
配置SqlSessionFactory
實際上對於Mybatis來說,可以省略這個配置,springboot會預設建立,但是這裡為了後面操作的方便性,自己配置一個SqlSessionFactory:
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import javax.sql.DataSource;
@Configuration
@MapperScan("com.liulei.study.xmlbatisboot.dao")
public class MybatisSqlSessionFactoryConfig {
@Autowired
@Qualifier("dataSource1")
private DataSource dataSource1;
@Value("${mybatis.mapper-locations}")
private Resource[] mapperLocations;
@Value("${mybatis.type-aliases-package}")
private String typeAliasesPackage;
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource1); //
factoryBean.setMapperLocations(mapperLocations);
factoryBean.setTypeAliasesPackage(typeAliasesPackage);
return factoryBean.getObject();
}
}
建立DataSourceHelper輔助類,動態切換資料來源
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
public class DataSourceHelper implements InitializingBean,ApplicationContextAware {
private ApplicationContext applicationContext;
private static Map<String,Environment> Environments;
private static Configuration configuration;
private SqlSessionFactory sqlSessionFactory;
@Override
public void afterPropertiesSet() throws Exception {
//獲取所有資料來源
Map<String, DataSource> dataSources = applicationContext.getBeansOfType(DataSource.class);
sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
//獲取sqlSessionFactory的configuration
configuration = sqlSessionFactory.getConfiguration();
Environment environment;
if(Environments==null){
Environments=new HashMap<String,Environment>(dataSources.size());
}
for (Map.Entry<String, DataSource> entry : dataSources.entrySet()) {
environment=new Environment(SqlSessionFactoryBean.class.getSimpleName(),
new SpringManagedTransactionFactory(),entry.getValue());
//初始化所有資料來源的environment,便於之後切換資料來源使用
Environments.put(entry.getKey(),environment);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
public static void setSqlSessionFactoryEnvironment(String dsName){
//切換Mybatis的Environment
configuration.setEnvironment(Environments.get(dsName));
}
}
有必要說一下,這裡實現了InitializingBean,和ApplicationContextAware介面,實現的目的不多說,不明白的朋友請自行學習spring。並且這裡@Component註解不可少,這和單純使用spring有區別。 另外afterPropertiesSet方法裡,最終的目的是初始化Environment 的Map集合,待之後使用。
使用DataSourceHelper,實現動態切換資料來源
@RestController
@RequestMapping("/test")
@EnableTransactionManagement
public class TestController {
@Autowired
private PersonService personService;
@RequestMapping("/getPersonById")
public <T> T getPersonById(@RequestParam String id){
return (T)personService.getPersonById(id);
}
@RequestMapping("/insert")
public int insertPerson(){
Person person;
for(int age=28;age<33;age++){
person = new Person();
person.setId("sdfdf");
person.setName("adsfdf");
person.setAddress("adsfdf");
person.setAge(age);
if(age<30)
//這裡做了切換資料來源
DataSourceHelper.setSqlSessionFactoryEnvironment("dataSource2");
else
//這裡做了切換資料來源
DataSourceHelper.setSqlSessionFactoryEnvironment("dataSource1");
personService.insertPerson(person);
}
return 0;
}
}
這裡只需要呼叫 DataSourceHelper.setSqlSessionFactoryEnvironment(“dataSource2”)方法即可實現切換資料來源,省略mapper.xml和PersonService的程式碼。但是這樣做不太方便。故有了下面的aop的方式。
Aop的方式動態切換資料來源
首先,定義註解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DS {
String value() default "dataSource1";
}
然後,定義Aop
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
@Aspect
@Component
public class DynamicDataSourceAspect {
@Value("${default.dataSource}")
private String DEFAULT_SOURCE;
@Before("@annotation(DS)")
public void beforeSwitchDS(JoinPoint point){
//獲得當前訪問的class
Class<?> className = point.getTarget().getClass();
//獲得訪問的方法名
String methodName = point.getSignature().getName();
//得到方法的引數的型別
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
String dataSource = DEFAULT_SOURCE;
try {
// 得到訪問的方法物件
Method method = className.getMethod(methodName, argClass);
// 判斷是否存在@DS註解
if (method.isAnnotationPresent(DS.class)) {
DS annotation = method.getAnnotation(DS.class);
// 取出註解中的資料來源名
dataSource = annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
// 切換資料來源
DataSourceHelper.setSqlSessionFactoryEnvironment(dataSource);
}
/*@After("@annotation(DS)")
public void afterSwitchDS(JoinPoint point) {
DataSourceHelper.setSqlSessionFactoryEnvironment(DEFAULT_SOURCE);
}*/
}
這裡註釋了@After("@annotation(DS)這一部分,如果有需要,可以開啟註釋,做一些其他的操作。 然後使用方法:
@Service
@Transactional
public class PersonService {
@Autowired
private PersonMapper personMapper;
public Person getPersonById(String id){
return personMapper.getPersonById(id);
}
@DS("dataSource2")
public int insertPerson(Person person){
return personMapper.insertPerson(person);
}
}
只需要加上註解就可以指定,該操作的資料來源了。
總結
本篇所實現的切換資料來源的方式,仍然侵入到了mybatis中去,並不是很好的做法,但是沒辦法繼承AbstractRoutingDataSource類的方法,不管用。所以我閱讀了一下mybatis的原始碼,找到了這種方法,如果哪位大佬有更好的方法,請不吝賜教,大家互相學習,共同進步。
專案git地址: https://github.com/Lewis-Liulei/springboot.git 裡面commo包下的類並沒有使用到,DynamicDataSource類和DynamicDataSourceEntry類可以刪掉。