Spring Boot + Mybatis + Druid 動態切換多資料來源
在大型應用程式中,配置主從資料庫並使用讀寫分離是常見的設計模式。
在Spring應用程式中,要實現讀寫分離,最好不要對現有程式碼進行改動,而是在底層透明地支援。
這樣,就需要我們再一個專案中,配置兩個,乃至多個數據源。
今天,小編先來介紹一下自己配置動態多資料來源的步驟
專案簡介:
編譯器:IDEA
JDK:1.8
框架:Spring Boot 2.1.0.RELEASES + Mybatis + Druid
一、配置資料庫連線資料
因為專案使用的是Spring Boot 框架,該框架會自動配置資料來源,自動從application.properties中讀取資料來源資訊,如果沒有配置,啟動時會報錯,因此我們再配置自定義的資料來源的時候,需要禁掉資料來源的自動配置。
但是小編在啟動專案的時候,還是報錯了,可是由於jdbcTemplate重複了,框架自動幫我們定義了一個jdbcTemplate,而小編自己又自定義了一個,因此,也要將這個自動配置禁止掉
啟動類方法如下:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class}) @MapperScan(sqlSessionTemplateRef = "jdbcTemplate") public class DynamicDatasourseApplication {public static void main(String[] args) { SpringApplication.run(DynamicDatasourseApplication.class, args); } }
下面開始配置自定義的資料來源。
新建jdbc.properties檔案,配置資料庫的連線,資料來源1為寫庫,資料來源2為讀庫
jdbc.driverClassName.db=com.mysql.jdbc.Driver #寫資料來源 jdbc.w.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&jdbc.properties&serverTimezone=UTC jdbc.w.user=root jdbc.w.password=123456 #讀資料來源 jdbc.r.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC jdbc.r.user=root jdbc.r.password=123456 #連線池屬性 druid.initialSize=2 druid.minIdle=30 druid.maxActive=80 druid.maxWait=60000 druid.timeBetweenEvictionRunsMillis=60000 druid.minEvictableIdleTimeMillis=300000 druid.validationQuery=SELECT 'x' druid.testWhileIdle=true druid.testOnBorrow=false druid.testOnReturn=false druid.poolPreparedStatements=true druid.maxPoolPreparedStatementPerConnectionSize=20 druid.filters=wall,stat
二、配置mybatis的屬性
在application.properties中配置mybatis的屬性
mybatis.type-aliases-package:實體類的位置,如果將實體類放到Application.java檔案的同級包或者下級包時,這個屬性可以不配置
mybatis.mapper-locations:mapper.xml的位置
mybatis.config-location:mybatis配置檔案的位置,無則不填
mybatis.type-aliases-package=cn.com.exercise.dynamicDatasourse.module.condition
mybatis.mapper-locations=/mappers/**.xml
mybatis.config-location=/config/sqlmap-config.xml
三、使用Java檔案讀取資源資料
1)配置主資料來源(寫庫)
@Bean(name = '寫庫名字') @Primary public DataSource master(){ DruidDataSource source = new DruidDataSource(); //使用source.setXxx(Yyy);進行配置 //資料庫基本屬性driverClassName url、user、password配置 //連線池基本屬性配置 return source; }
@Primary表示優先為注入的Bean,此處用來標識住資料來源
2)配置從資料來源(讀庫),配置內容和主資料來源相同
@Bean(name = '讀庫名字') public DataSource master(){ DruidDataSource source = new DruidDataSource(); //使用source.setXxx(Yyy);進行配置 //資料庫基本屬性driverClassName url、user、password配置 //連線池基本屬性配置 return source; }
3)資料來源支援,配置預設資料來源
@Bean(name = "dynamicDataSource") public DataSource dynamicDataSource(){ DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource(); //配置多資料來源 Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put("寫庫名字", master()); dataSourceMap.put("讀庫名字", slave()); // 將 master 資料來源作為預設指定的資料來源 dynamicRoutingDataSource.setDefaultTargetDataSource(master()); // 將 master 和 slave 資料來源作為指定的資料來源 dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); return dynamicRoutingDataSource; } @Bean(name="dataSourceProxy") public TransactionAwareDataSourceProxy dataSourceProxy(){ TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(); proxy.setTargetDataSource(dynamicDataSource()); return proxy; }
4)配置sqlSessionFactory和jdbcTemplate
在sqlSessionFactory中,配置mybatis相關的三個內容:typeAliasesPackage,configLocation和mapperLocation,分別對應了application.properties中的三個內容,有則配置,無則省略。
@Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setVfs(SpringBootVFS.class); sqlSessionFactoryBean.setTypeAliasesPackage(typeAlias); sqlSessionFactoryBean.setConfigLocation( new ClassPathResource(sqlmapConfigPath)); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+mapperLocation; sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath)); sqlSessionFactoryBean.setDataSource(dataSourceProxy()); return sqlSessionFactoryBean; } @Bean(name = "jdbcTemplate") public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){ return new SqlSessionTemplate(sqlSessionFactory); }
5)配置事務傳播相關內容
@Bean(name = "transactionManager") public PlatformTransactionManager transactionManager() { DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSourceProxy()); return manager; } /** * 配置事務的傳播特性 */ @Bean(name = "txAdvice") public TransactionInterceptor txAdvice(){ TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionManager(transactionManager()); Properties transactionAttributes = new Properties(); //使用transactionAttributes.setProperty()配置傳播特性 interceptor.setTransactionAttributes(transactionAttributes); return interceptor; } @Bean(name = "txAdviceAdvisor") public Advisor txAdviceAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); String transactionExecution = "execution(* cn.com.hiveview.springboot.demoapi..service.*.*(..))"; pointcut.setExpression(transactionExecution); return new DefaultPointcutAdvisor(pointcut, txAdvice()); }
四、動態資料來源支援
在上面配置動態資料來源支援的時候,我們使用了一個類“DynamicDataSource.java”。
這個類是自定義的類,繼承了抽象類AbstractRoutingDataSource,正是通過這個抽象類來實現動態資料來源的選擇的。
來看下這個抽象類的成員變數:
private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource;private Map<Object, DataSource> resolvedDataSources;
以下介紹可以參考上一節(3)的內容。
【1】targetDataSources:儲存了key和資料庫連線的對映關係
【2】defaultTargetDataSource:表示預設的資料庫連線
【3】resolvedDataSources:是通過targetDataSources構建而來,儲存的結構也是資料庫標識和資料來源的對映關係
接下來就是根據這個類,實現我們自己的類DynamicDataSource.java
public class DynamicDataSource extends AbstractRoutingDataSource { @Autowired private DBHelper helper; @Override protected Object determineCurrentLookupKey() { return helper.getDBType(); } }
determineCurrentLookUpKey():決定需要使用哪個資料庫,這個方法需要我們自己實現
先看一下在抽象類中,是如何使用這個方法的
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource 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 + "]"); } else { return dataSource; } }
因此我們只需在determineCurrentLookUpKey方法中,返回資料庫的標誌即可。
DBHelper類也是自定義的類,資料來源持有類,存放了讀、寫庫名字,以及設定資料來源型別、獲取資料來源型別、清除資料來源型別的方法
@Component public class DBHelper { /** * 執行緒獨立 */ private ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static final String DB_TYPE_RW = "dataSource_db01"; public static final String DB_TYPE_R = "dataSource_db02"; public String getDBType() { String db = contextHolder.get(); if (db == null) { db = DB_TYPE_RW; // 預設是讀寫庫 } return db; } public void setDBType(String str) { contextHolder.set(str); } public void clearDBType() { contextHolder.remove(); } }
五、動態切換
1)配置註解 DS.java
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface DS { String value(); }
2)使用AOP切換
@Aspect @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class.getName()); @Around("execution(* cn.com.exercise.dynamicDatasourse.module..service.*.*(..))") public Object switchDS(ProceedingJoinPoint point) throws Throwable { Class<?> className = point.getTarget().getClass(); String dataSource = DBHelper.DB_TYPE_RW; if (className.isAnnotationPresent(DS.class)) { DS ds = className.getAnnotation(DS.class); dataSource = ds.value(); }else{ // 得到訪問的方法物件 String methodName = point.getSignature().getName(); Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes(); Method method = className.getMethod(methodName, argClass); // 判斷是否存在@DS註解 if (method.isAnnotationPresent(DS.class)) { DS annotation = method.getAnnotation(DS.class); // 取出註解中的資料來源名 dataSource = annotation.value(); } } // 切換資料來源 DBHelper dbHelper = new DBHelper(); dbHelper.setDBType(dataSource); logger.info("當前資料來源"+dataSource); try { return point.proceed(); } finally { dbHelper.clearDBType(); } } }
完成以上內容後,就可以在service層的方法上,新增@DS註解,來實現資料來源的切換了。
六、使用
service層程式碼
@Service public class DynamicService { @Autowired DynamicDao dynamicDao; public List<DynamicCondition> getListFromSource1(){ return dynamicDao.getListFromSource1(); } @DS('讀庫名字') public List<DynamicCondition> getListFromSource2(){ return dynamicDao.getListFromSource2(); } }
由於寫庫是預設資料來源,因此當不使用@DS配置資料來源,以及使用@DS(“寫庫名字”)時,使用的都是寫庫。
依次訪問地址:
http://localhost:8082/dynamic/source1,
http://localhost:8082/dynamic/source2
執行結果如下:
按照以上步驟,就可以完成動態切換資料來源了,下面附上 完整程式碼連線