1. 程式人生 > >Spring Boot配置多資料來源並實現Druid自動切換

Spring Boot配置多資料來源並實現Druid自動切換

SpringBoot多資料來源切換,先上配置檔案:

1.pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.river</groupId>
    <artifactId>goalintl</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
    </parent>

    <dependencies>
        <!--spring-boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--spring-boot-freemarker-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!---->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatisplus-spring-boot-starter</artifactId>
            <version>1.0.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>
</project>

2.application.yaml

spring:
  freemarker:
    cache: false
    charset: utf-8
    enabled: true
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    primary:
      driverClassName: com.mysql.jdbc.Driver
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/as
      type: com.alibaba.druid.pool.DruidDataSource
    local:
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/test
        type: com.alibaba.druid.pool.DruidDataSource
    prod:
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/guns
        type: com.alibaba.druid.pool.DruidDataSource

server:
  port: 8085
diy.user:
   id: 12
logging:
  file: /log.txt
  level: trace

mybatis-plus:
  mapper-locations: classpath:mapper/*Mapper.xml
  typeAliasesPackage:
  global-config:
    id-type: 3
    refresh-mapper: true
    capital-mode: true
    field-strategy: 2
    db-column-underline: false


3.configuration

package com.river.datasource;


import com.alibaba.druid.pool.DruidDataSource;
import com.river.common.ContextConst;
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 org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;


@Configuration
public class MutiplyDataSource {


    @Bean(name = "dataSourcePrimary")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource(){
        return new DruidDataSource();
    }

    @Bean(name = "dataSourceLocal")
    @ConfigurationProperties(prefix = "spring.datasource.local")
    public DataSource localDataSource(){
        return new DruidDataSource();
    }

    @Bean(name = "dataSourceProd")
    @ConfigurationProperties(prefix = "spring.datasource.prod")
    public DataSource prodDataSource(){
        return new DruidDataSource();
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //配置預設資料來源
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());

        //配置多資料來源
        HashMap<Object, Object> dataSourceMap = new HashMap();
        dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(),primaryDataSource());
        dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(),localDataSource());
        dataSourceMap.put(ContextConst.DataSourceType.PROD.name(),prodDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    /**
     * 配置@Transactional註解事務
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

4.資料來源持有類

package com.river.datasource;

import lombok.extern.log4j.Log4j;

@Log4j
public class DataSourceContextHolder {

    private static final String DEFAULT_DATASOURCE = "PRIMARY_DATASOURCE";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public static void setDataSource(String dbType){
        log.info("切換到["+dbType+"]資料來源");
        contextHolder.set(dbType);
    }

    public static String getDataSource(){
        return contextHolder.get();
    }

    public static void clearDataSource(){
        contextHolder.remove();
    }
}

5.定義切換資料來源的註解和切面

package com.river.annotation;

import com.river.common.ContextConst;

import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY;

}
package com.river.aspect;


import com.river.annotation.DataSource;
import com.river.common.ContextConst;
import com.river.datasource.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.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;


@Component
@Aspect
public class DynamicDataSourceAspect {
    
    @Before("execution(* com.river.service..*.*(..))")
    public void before(JoinPoint point){
        try {
            DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
            String methodName = point.getSignature().getName();
            Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
            Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
            DataSource methodAnnotation = method.getAnnotation(DataSource.class);
            methodAnnotation = methodAnnotation == null ? annotationOfClass:methodAnnotation;
            ContextConst.DataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() !=null ? methodAnnotation.value() :ContextConst.DataSourceType.PRIMARY ;
            DataSourceContextHolder.setDataSource(dataSourceType.name());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    @After("execution(* com.river.service..*.*(..))")
    public void after(JoinPoint point){
        DataSourceContextHolder.clearDataSource();
    }



}
package com.river.common;

public interface ContextConst {

    enum DataSourceType{
        PRIMARY,LOCAL,PROD,TEST
    }
}

6.資料來源路由實現類

package com.river.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{


    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

7.使用時,通過註解指定資料來源

package com.river.service.impl;

import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.river.annotation.DataSource;
import com.river.common.ContextConst;
import com.river.entity.User;
import com.river.mapper.PrimaryUserMapper;
import com.river.service.ParmaryUserService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ParmaryUserServiceImpl extends ServiceImpl<PrimaryUserMapper,User> implements ParmaryUserService{

    @Autowired
    private PrimaryUserMapper primaryUserMapper;


    @Override
    public List<User> sel(){
       return primaryUserMapper.selectList(null);
    }

    @DataSource(ContextConst.DataSourceType.PROD)
    @Override
    public List<User> sell() {
        return primaryUserMapper.selectList(null);
    }
    
    @DataSource(ContextConst.DataSourceType.LOCAL)
    @Override
    public List<User> selle() {
        return primaryUserMapper.selectList(null);
    }

}

7.另外,補充一下其他部分程式碼;

entity:

@Data
@TableName("ACT_USER")
public class User {

    @TableId
    @TableField
    private Integer id;
    @TableField("USERNAME")
    private String username;
    @TableField("PASSWORD")
    private String password;
    @TableField("ROLE_ID")
    private Integer roleId;

}

mapper:

package com.river.mapper;

import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.river.entity.User;

public interface PrimaryUserMapper extends BaseMapper<User>{


}

controller:

@RestController
public class DataController {

    @Autowired
    private ParmaryUserService parmaryUserService;

    @RequestMapping("login")
    public List<User> login(Integer type){
        switch (type){
            case 1:
                return parmaryUserService.sel();
            case 2:
                return parmaryUserService.sell();
            default:
                return parmaryUserService.selle();
        }
    }
}

入口類:

package com.river;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeansException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

@MapperScan("com.river.mapper")
//排除DataSource自動配置類,否則會預設自動配置,不會使用我們自定義的DataSource,並且啟動報錯
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 
public class GoalintlApplication implements CommandLineRunner,ApplicationContextAware{

    public static void main(String[] args) {
        SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(GoalintlApplication.class);
        springApplicationBuilder.profiles("dev").logStartupInfo(true).run(args);
    }

    @Override
    public void run(String... args) throws Exception {

    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    }
}

說明一下實現思路:

      springboot有提供AbstractRoutingDataSource#determineCurrentLookupKey抽象方法去指定資料來源,我們要做的就是實現切換資料來源的邏輯;通過AOP在呼叫資料庫之前切換資料來源;

     本來在切面內做了一個快取,記錄上一次使用的資料來源,如果這一次使用相同的就不用切換了,但是發現初始化資料連線才是消耗大的,後面切換資料來源其實就是去指定用哪個資料庫連線而已,不再消耗資源了;

      下面的程式碼顯示了切換資料來源時只是通過key去拿對應的dataSource,而相關的dataSource在第一次呼叫時就初始化一次就可以了;

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;
	}

    類似文章很多,這裡自己實現了一把;