Spring Boot 實現Mybatis多數據源

Spring Boot 多數據源

1. 背景

項目日產生的數據量比較大,所以規定每天歸15天之前的數據到 Hive 庫, 當查詢的時候,15天之內的需要查詢應用所在的 ORACLE 庫,15 天之前的則通過 Presto 查詢 Hive 庫。
所以需要應用添加 Presto 的數據源, 初步想法構造一個多數據源的連接池, 使用 AOP 來實現連接池數據源的切換。

2. 定義數據源

2.1 定義動態數據源

定義數據源常量類

package com.hand.hcf.app.haep.datasource.constant;

/**
 * 數據源常量類
 *
 * @author [email protected]
 * @date 2019/10/21
 */
public class DataSourceConstants {
    /**
     * 默認數據源
     */
    public static final String DEFAULT_DATA_SOURCE = "defaultDataSource";

    /**
     * presto 數據源
     */
    public static final String PRESTO_DATA_SOURCE = "prestoDataSource";
}

定義一個數據源上下文處理器, 用於處理當前線程使用哪一個數據源

/**
 * 數據源上下文處理工具
 *
 * @author yanqiang.jiang
 * @date 2019/10/21
 */
public class DataSourceContextHolder {
    /**
     * 默認數據源
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 設置數據源名
     *
     * @param dbType 數據類型
     */
    public static void setDataSource(String dbType) {
        contextHolder.set(dbType);
    }

    /**
     * 獲取數據源名
     *
     * @return 返回數據源
     */
    public static String getDataSource() {
        return (contextHolder.get());
    }

    /**
     * 清除數據源名
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }


}

動態數據源類,用於動態獲取數據源

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

/**
 * 動態數據源類
 *
 * @author yanqiang.jiang
 * @date 2019/10/21
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

2.3 定義自定義數據源

添加配置

presto.spring.datasource.url=jdbc:presto://localhost:8080/catalog?user=root
presto.spring.datasource.driver-class-name=com.facebook.presto.jdbc.PrestoDriver
presto.spring.datasource.enabled=false

定義一個自定義數據源構造接口, 後續有自定義的數據源實現這個接口構造數據源即可, 返回的是數據源 BeanName和數據源Bean的 Map

import java.util.Map;
import org.springframework.context.ApplicationContextAware;

/**
 * 自定義數據源實現接口
 *
 * @author yanqiang.jiang
 * @version 1.0
 * @date 2019/10/23
 */
public interface CustomDataSourceBuilder extends ApplicationContextAware {

    /**
     * 構造數據源
     *
     * @return 構造結果,
     */
    Map<Object, Object> builder();
}

添加配置屬性類,讀取Presto 的配置

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Presto 數據源屬性信息
 *
 * @author yanqiang.jiang
 * @date 2019/10/23
 */
@Data
@Component
@Accessors(chain = true)
@ConfigurationProperties(prefix = "presto.spring.datasource")
public class PrestoDataSourceProperties {
    /**
     * 是否啓用
     */
    private Boolean enabled;
    /**
     * 驅動類
     */
    private String driverClassName;
    /**
     * jdbcUrl
     */
    private String url;

    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
}

添加Presto 數據源的實現類

import com.hand.hcf.app.haep.datasource.constant.DataSourceConstants;
import com.hand.hcf.app.haep.datasource.properties.PrestoDataSourceProperties;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

/**
 * Presto 數據源處理類
 *
 * @author yanqiang
 * @date 2019/10/23
 */
@Slf4j
@Component
@EnableConfigurationProperties(PrestoDataSourceProperties.class)
public class PresotDataSourceBuilder implements CustomDataSourceBuilder {

    /**
     * 應用上下文
     */
    private ConfigurableApplicationContext applicationContext;

    @Autowired
    private PrestoDataSourceProperties prestoDataSourceProperties;

    @Override
    public Map<Object, Object> builder() {
        Map<Object, Object> dsp = new HashMap<>();
        if (prestoDataSourceProperties.getEnabled()) {
            // 啓用 presto 配置
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setJdbcUrl(prestoDataSourceProperties.getUrl());
            hikariConfig.setDriverClassName(prestoDataSourceProperties.getDriverClassName());
            // 構建DaSource
            BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.rootBeanDefinition(HikariDataSource.class);
            beanBuilder.addConstructorArgValue(hikariConfig);
            DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
            // 構造數據源的bean
            beanFactory.registerBeanDefinition(DataSourceConstants.PRESTO_DATA_SOURCE,
                    beanBuilder.getBeanDefinition());
            // 添加在Map
            dsp.put(DataSourceConstants.PRESTO_DATA_SOURCE,
                    beanFactory.getBean(DataSourceConstants.PRESTO_DATA_SOURCE, HikariDataSource.class));
            log.info("===============數據源{}已經初始化===========", DataSourceConstants.PRESTO_DATA_SOURCE);
        }
        return dsp;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = (ConfigurableApplicationContext) applicationContext;
    }
}

3. 配置數據庫鏈接

編寫數據源處理類,builder 方法返回所有自定義的數據源組裝成 Map

import com.hand.hcf.app.haep.datasource.builder.CustomDataSourceBuilder;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 構建數據源Bean處理類
 *
 * @author yanqiang.jiang
 * @date 2019/10/23
 */
@Slf4j
@Component
public class CustomDataSourceHandler implements ApplicationContextAware {

    /**
     * 應用上下文
     */
    private ApplicationContext applicationContext;


    /**
     * 構造數據源處理類
     *
     * @return 構造結果
     */
    public Map<Object, Object> builder() {
        Map<Object, Object> dsp = new HashMap<>();
        // 遍歷該接口的所有實現類
        applicationContext.getBeansOfType(CustomDataSourceBuilder.class).forEach((k, v) -> {
            Map<Object, Object> dataMap = v.builder();
            if (dataMap.isEmpty()) {
                log.info("==============={}類對應的數據源無需初始化===============", k);
            } else {
                dsp.putAll(dataMap);
                log.info("==============={}類對應的數據源已經初始化===============", k);
            }
        });
        return dsp;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

數據源配置類, dynamicDataSource 是實例化動態數據源類, 並且把默認的數據源和自定義數據源放在裏面

mybatisSqlSessionFactoryBeantransactionManager 的dataSouce也換成動態數據源

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.hand.hcf.app.haep.datasource.CustomDataSourceHandler;
import com.hand.hcf.app.haep.datasource.DynamicDataSource;
import com.hand.hcf.app.haep.datasource.constant.DataSourceConstants;
import com.hand.hcf.core.annotation.EnableBaseI18nService;
import com.hand.hcf.core.persistence.DomainObjectMetaObjectHandler;
import com.hand.hcf.core.plugin.DataAuthProcessInterceptor;
import com.hand.hcf.core.plugin.DomainMetaInterceptor;
import com.hand.hcf.core.plugin.HcfOptimisticLockerInterceptor;
import com.hand.hcf.core.plugin.HcfPaginationInterceptor;
import com.hand.hcf.core.plugin.I18nSqlProcessInterceptor;
import com.hand.hcf.core.plugin.LoginInfoInterceptor;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * 數據源配置類
 *
 * @author [email protected]
 * @date 2019/10/23
 */
@Configuration
@EnableBaseI18nService
@EnableConfigurationProperties({MybatisPlusProperties.class,
        DataSourceProperties.class})
public class DatabaseConfiguration {
    private static final Map<DbType, String> DB_QUOTE = new HashMap(16);
    private final MybatisPlusProperties properties;
    private final ResourceLoader resourceLoader;

    public DatabaseConfiguration(MybatisPlusProperties properties, ResourceLoader resourceLoader,
                                 DataSourceProperties dataSourceProperties,
                                 CustomDataSourceHandler customDataSourceHandler) {
        this.customDataSourceHandler = customDataSourceHandler;
        System.getProperties().setProperty("oracle.jdbc.J2EE13Compliant", "true");
        this.properties = properties;
        this.resourceLoader = resourceLoader;
        this.dataSourceProperties = dataSourceProperties;
    }

    private final DataSourceProperties dataSourceProperties;


    private final CustomDataSourceHandler customDataSourceHandler;


    /**
     * 默認數據源
     *
     * @return 默認數據源
     */
    @Bean(name = DataSourceConstants.DEFAULT_DATA_SOURCE)
    public DataSource defaultDataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
        hikariConfig.setDriverClassName(dataSourceProperties.getDriverClassName());
        hikariConfig.setUsername(dataSourceProperties.getUsername());
        hikariConfig.setPassword(dataSourceProperties.getPassword());
        return new HikariDataSource(hikariConfig);
    }

    /**
     * 動態數據源: 通過AOP在不同數據源之間動態切換
     *
     * @return 獲取數據源
     */
    @Primary
    @Bean(name = "dataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默認數據源
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource());

        // 添加其他數據源
        Map<Object, Object> dsMap = customDataSourceHandler.builder();
        // 默認數據源加進去
        dsMap.put(DataSourceConstants.DEFAULT_DATA_SOURCE, defaultDataSource());
        // 設置數多據源
        dynamicDataSource.setTargetDataSources(dsMap);
        return dynamicDataSource;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean() {
        DataSource dataSource = dynamicDataSource();
        MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        mybatisSqlSessionFactoryBean.setDataSource(dataSource);
        mybatisSqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            mybatisSqlSessionFactoryBean.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        mybatisSqlSessionFactoryBean.setPlugins(this.getInterceptors());
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        globalConfig.setBanner(false);
        globalConfig.getDbConfig().setLogicDeleteValue("1");
        globalConfig.getDbConfig().setLogicNotDeleteValue("0");
        DbType dbType = JdbcUtils.getDbType(this.dataSourceProperties.getUrl());
        globalConfig.getDbConfig().setColumnFormat(DB_QUOTE.get(dbType));
        globalConfig.setMetaObjectHandler(new DomainObjectMetaObjectHandler());
        globalConfig.setEnableSqlRunner(true);
        mybatisSqlSessionFactoryBean.setGlobalConfig(globalConfig);
        MybatisConfiguration configuration = this.properties.getConfiguration();
        if (configuration == null) {
            configuration = new MybatisConfiguration();
        }

        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setJdbcTypeForNull(JdbcType.VARCHAR);
        mybatisSqlSessionFactoryBean.setConfiguration(configuration);
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            mybatisSqlSessionFactoryBean.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            mybatisSqlSessionFactoryBean.setMapperLocations(this.properties.resolveMapperLocations());
        }

        if (!StringUtils.isEmpty(this.properties.getTypeHandlersPackage())) {
            mybatisSqlSessionFactoryBean.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        mybatisSqlSessionFactoryBean.setDatabaseIdProvider(this.getDatabaseIdProvider());
        if (!StringUtils.isEmpty(this.properties.getTypeEnumsPackage())) {
            mybatisSqlSessionFactoryBean.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
        }

        return mybatisSqlSessionFactoryBean;
    }


    /**
     * 配置@Transactional註解事物
     *
     * @return 註解
     */
    @Bean(name = "dataSourceTransactionManager")
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

    private Interceptor[] getInterceptors() {
        List<Interceptor> interceptors = new ArrayList();
        interceptors.add(new LoginInfoInterceptor());
        DomainMetaInterceptor domainMetaInterceptor = new DomainMetaInterceptor();
        interceptors.add(domainMetaInterceptor);
        HcfPaginationInterceptor pagination = new HcfPaginationInterceptor();
        interceptors.add(pagination);
        interceptors.add(new HcfOptimisticLockerInterceptor());
        I18nSqlProcessInterceptor i18nSqlProcessInterceptor = new I18nSqlProcessInterceptor();
        interceptors.add(i18nSqlProcessInterceptor);
        DataAuthProcessInterceptor dataAuthProcessInterceptor = new DataAuthProcessInterceptor();
        interceptors.add(dataAuthProcessInterceptor);
        return interceptors.toArray(new Interceptor[0]);
    }

    @Bean
    public DatabaseIdProvider getDatabaseIdProvider() {
        DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        Properties properties = new Properties();
        properties.setProperty("Oracle", "oracle");
        properties.setProperty("MySQL", "mysql");
        properties.setProperty("DB2", "db2");
        properties.setProperty("Derby", "derby");
        properties.setProperty("H2", "h2");
        properties.setProperty("HSQL", "hsql");
        properties.setProperty("Informix", "informix");
        properties.setProperty("MS-SQL", "ms-sql");
        properties.setProperty("PostgreSQL", "postgresql");
        properties.setProperty("Sybase", "sybase");
        properties.setProperty("Hana", "hana");
        databaseIdProvider.setProperties(properties);
        return databaseIdProvider;
    }

    static {
        DB_QUOTE.put(DbType.MYSQL, "`%s`");
        DB_QUOTE.put(DbType.MARIADB, "`%s`");
        DB_QUOTE.put(DbType.ORACLE, null);
        DB_QUOTE.put(DbType.DB2, null);
        DB_QUOTE.put(DbType.H2, null);
        DB_QUOTE.put(DbType.HSQL, null);
        DB_QUOTE.put(DbType.SQLITE, "`%s`");
        DB_QUOTE.put(DbType.POSTGRE_SQL, "\"%s\"");
        DB_QUOTE.put(DbType.SQL_SERVER2005, null);
        DB_QUOTE.put(DbType.SQL_SERVER, null);
        DB_QUOTE.put(DbType.OTHER, null);
    }
}

4. 編寫數據源切面

切面用的自定義註解

import com.hand.hcf.app.haep.datasource.constant.DataSourceConstants;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 數據源註解
 *
 * @author [email protected]
 * @date 2019/10/21
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {
    String value() default DataSourceConstants.DEFAULT_DATA_SOURCE;
}

編寫切面類, 在使用註解的方法選擇數據源之前調用之前定義的 DataSourceContextHolder 設置當前線程的數據源,結束後再設置成默認數據源。

import com.hand.hcf.app.haep.datasource.annotation.DataSource;
import com.hand.hcf.app.haep.datasource.constant.DataSourceConstants;
import com.hand.hcf.core.exception.BizException;
import java.lang.reflect.Method;
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;

/**
 * 數據眼切面類
 *
 * @author yanqiang
 * @date 2019/10/22
 */
@Aspect
@Component
public class DynamicDataSourceAspect {

    /**
     * 切換需要的數據源
     *
     * @param point 切點
     */
    @Before("@annotation(com.hand.hcf.app.haep.datasource.annotation.DataSource)")
    public void beforeSwitchDataSource(JoinPoint point) {
        //獲得當前訪問的class
        Class<?> className = point.getTarget().getClass();
        //獲得訪問的方法名
        String methodName = point.getSignature().getName();
        //得到方法的參數的類型
        Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
        String dataSource = DataSourceConstants.DEFAULT_DATA_SOURCE;
        try {
            // 得到訪問的方法對象
            Method method = className.getMethod(methodName, argClass);
            // 判斷是否存在@DS註解
            if (method.isAnnotationPresent(DataSource.class)) {
                DataSource annotation = method.getAnnotation(DataSource.class);
                // 取出註解中的數據源名
                dataSource = annotation.value();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new BizException("獲取數據源錯誤!");
        }

        // 切換數據源
        DataSourceContextHolder.setDataSource(dataSource);
    }

    /**
     * 還原數據源爲默認數據源
     *
     * @param point 切點
     */
    @After("@annotation(com.hand.hcf.app.haep.datasource.annotation.DataSource)")
    public void afterSwitchDataSource(JoinPoint point) {
        DataSourceContextHolder.clearDataSource();
    }
}

使用註解在該方法上, 就可以切換對應的數據源

/**
  * presto查詢
  *
  * @return 查詢結果
  */
 @DataSource(DataSourceConstants.PRESTO_DATA_SOURCE)
 public List<Map<String, String>> prestoTest(){
     return baseMapper.prestoTest();
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章