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
是實例化動態數據源類, 並且把默認的數據源和自定義數據源放在裏面
mybatisSqlSessionFactoryBean
和 transactionManager
的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();
}