1. 程式人生 > >Spring Boot + Mybatis + Druid 動態切換多資料來源

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&
&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
jdbc.properties

二、配置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

執行結果如下:

 按照以上步驟,就可以完成動態切換資料來源了,下面附上 完整程式碼連線