1. 程式人生 > >SpringCloud之基於SpringCloud+MybatisPlus+Config實現多個數據源的動態切換

SpringCloud之基於SpringCloud+MybatisPlus+Config實現多個數據源的動態切換

一、首先,專案基於SpringCloud,配置檔案在Git上(包括資料來源的配置資訊)。

二、開始基於原有專案進行重構

1、寫一個動態資料來源上下文.程式碼如下:

/**
 * 動態資料來源上下文
 */
public class DbContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal<>();

    /*
     * 管理所有的資料來源id;
     * 主要是為了判斷資料來源是否存在;
     */
    public static List<String> dataSourceIds = new ArrayList<String>();

    /**
     * 設定資料來源
     * @param dbTypeEnum
     */
    public static void setDbType(String dbTypeEnum) {
        contextHolder.set(dbTypeEnum);
    }

    /**
     * 取得當前資料來源
     * @return
     */
    public static String getDbType() {
        return (String)contextHolder.get();
    }

    /**
     * 清除上下文資料
     */
    public static void clearDbType() {
        contextHolder.remove();
    }

    /**
     * 判斷指定DataSrouce當前是否存在
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }
}

2、再寫一個數據源路由類,程式碼如下:

/**
 * 資料來源路由類
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 取得當前使用哪個資料來源
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }
}

3、寫一個自定義註解,用來傳遞需要哪個資料來源:

/**
 * 在方法上使用,用於指定使用哪個資料來源
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value();
}

4、寫一個AOP,只要有註解的就根據引數切換資料來源:

@Component
@Order(-10)//保證該AOP在@Transactional之前執行
@Aspect
public class DBTypeAOP {

    @Pointcut("@annotation(TargetDataSource)")
    public void dbType() {
    }
    @Before("dbType()")
    public void before(JoinPoint joinPoint) throws Throwable {
        //獲取當前的指定的資料來源;
        System.out.println("---------------before---------------");
        TargetDataSource targetDataSource = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(TargetDataSource.class);
        String dsId = targetDataSource.value();
        //如果不在我們注入的所有的資料來源範圍之內,那麼輸出警告資訊,系統自動使用預設的資料來源。
        if (!DbContextHolder.containsDataSource(dsId)) {
            System.err.println("資料來源[{}]不存在,使用預設資料來源 > {}"+targetDataSource.value()+joinPoint.getSignature());
        } else {
            System.out.println("Use DataSource : {} > {}"+targetDataSource.value()+joinPoint.getSignature());
            //找到的話,那麼設定到動態資料來源上下文中。
            DbContextHolder.setDbType(targetDataSource.value());
        }
    }

    @After("dbType()")
    public void after(JoinPoint joinPoint) {
        System.out.println("--------------after----------------");
        TargetDataSource targetDataSource = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(TargetDataSource.class);
        System.out.println("Revert DataSource : {} > {}"+targetDataSource.value()+joinPoint.getSignature());
        //方法執行完畢之後,銷燬當前資料來源資訊,進行垃圾回收。
        DbContextHolder.clearDbType();
    }
}

5、使用ImportBeanDefinitionRegistrar進行註冊我們的多資料來源:

/**
 * 動態資料來源註冊;
 */
public class DynamicDataSourceRegister  implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    //如配置檔案中未指定資料來源型別,使用該預設值
    private static final Object DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";

    private ConversionService conversionService = new DefaultConversionService();

    private PropertyValues dataSourcePropertyValues;

    // 預設資料來源
    private DataSource defaultDataSource;

    private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();

    /**
     * 載入多資料來源配置
     */
    @Override
    public void setEnvironment(Environment environment) {
       System.out.println("DynamicDataSourceRegister.setEnvironment()");
       initDefaultDataSource(environment);
        initCustomDataSources(environment);
    }

    /**
     * 載入主資料來源配置.
     * @param env
     */
    private void initDefaultDataSource(Environment env){
       // 讀取主資料來源
       RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
       Map<String, Object> dsMap = new HashMap<String, Object>();
       dsMap.put("type", propertyResolver.getProperty("type"));
        dsMap.put("driverClassName", propertyResolver.getProperty("driverClassName"));
        dsMap.put("url", propertyResolver.getProperty("url"));
        dsMap.put("username", propertyResolver.getProperty("username"));
        dsMap.put("password", propertyResolver.getProperty("password"));
        //建立資料來源;
        defaultDataSource = buildDataSource(dsMap);
        dataBinder(defaultDataSource, env);
    }

    /**
     * 初始化更多資料來源
     */
    private void initCustomDataSources(Environment env) {
        // 讀取配置檔案獲取更多資料來源,也可以通過defaultDataSource讀取資料庫獲取更多資料來源
        RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "custom.datasource.");
        String dsPrefixs = propertyResolver.getProperty("names");
        for (String dsPrefix : dsPrefixs.split(",")) {// 多個數據源
            Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + ".");
            DataSource ds = buildDataSource(dsMap);
            customDataSources.put(dsPrefix, ds);
            dataBinder(ds, env);
        }
    }

    /**
     * 建立datasource.
     * @param dsMap
     * @return
     */
    @SuppressWarnings("unchecked")
    public DataSource buildDataSource(Map<String, Object> dsMap) {
        Object type = dsMap.get("type");
        if (type == null){
            type = DATASOURCE_TYPE_DEFAULT;// 預設DataSource
        }
        Class<? extends DataSource> dataSourceType;
        try {
            dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
            String driverClassName = dsMap.get("driverClassName").toString();
            String url = dsMap.get("url").toString();
            String username = dsMap.get("username").toString();
            String password = dsMap.get("password").toString();
            DataSourceBuilder factory =   DataSourceBuilder.create().driverClassName(driverClassName).url(url).username(username).password(password).type(dataSourceType);
            return factory.build();
        } catch (ClassNotFoundException e) {
           e.printStackTrace();
        }
        return null;
    }

    /**
     * 為DataSource繫結更多資料
     * @param dataSource
     * @param env
     */
    private void dataBinder(DataSource dataSource, Environment env){
       RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
       dataBinder.setConversionService(conversionService);
       dataBinder.setIgnoreNestedProperties(false);//false
        dataBinder.setIgnoreInvalidFields(false);//false
        dataBinder.setIgnoreUnknownFields(true);//true
        if(dataSourcePropertyValues == null){
            Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties(".");
            Map<String, Object> values = new HashMap<>(rpr);
            // 排除已經設定的屬性
            values.remove("type");
            values.remove("driverClassName");
            values.remove("url");
            values.remove("username");
            values.remove("password");
            dataSourcePropertyValues = new MutablePropertyValues(values);
        }
        dataBinder.bind(dataSourcePropertyValues);
    }
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
       System.out.println("DynamicDataSourceRegister.registerBeanDefinitions()");
       Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
       // 將主資料來源新增到更多資料來源中
        targetDataSources.put("dataSource", defaultDataSource);
        DbContextHolder.dataSourceIds.add("dataSource");
        // 新增更多資料來源
        targetDataSources.putAll(customDataSources);
        for (String key : customDataSources.keySet()) {
            DbContextHolder.dataSourceIds.add(key);
        }
        // 建立DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //新增屬性:AbstractRoutingDataSource.defaultTargetDataSource
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        registry.registerBeanDefinition("dataSource", beanDefinition);
    }
}

三、最後,我們在Application.java這個啟動類中,使用@import註冊DynamicDataSourceRegister:

@EnableDiscoveryClient
@SpringBootApplication
//註冊
@Import({DynamicDataSourceRegister.class})
public class CircleOfFriendsApplication {

	public static void main(String[] args) {
		SpringApplication.run(CircleOfFriendsApplication.class, args);
	}
}

四、執行:

執行的話,只需在你原有的方法上加上我們第3步寫的自定義註解@TargetDataSource("資料來源1")

例如:

五、效果:

---------------before---------------
Use DataSource : {} > {}ds1 com.luda.springcloud.client.controller.CircleOfFriendsController.queryCircleOfFriends(String,String,String)
2018-08-03 12:19:06.942  INFO [spring-cloud-circleOfFriends,e1c88ebca78edb0d,f27d8e29d1a698a3,false] 6708 --- [  XNIO-2 task-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} inited
--------------after----------------
 Time:5 ms - ID:com.luda.springcloud.client.mapper.CircleOfFriendsMapper.selectPage
 Execute SQL:SELECT id,`createUser`,content,imgUrl,createTime,ip FROM circle_of_friends ORDER BY createTime DESC LIMIT 0,10

Revert DataSource : {} > {}ds1  com.luda.springcloud.client.controller.CircleOfFriendsController.queryCircleOfFriends(String,String,String)

進入了AOP,並且獲取我需要的資料來源,在方法完畢後,進行清除操作。

給我點個贊吧!拜託了^-^