SpringBoot配置數據庫密碼加密的實現

你在使用 MyBatis 的過程中,是否有想過多個數據源應該如何配置,如何去實現?出於這個好奇心,我在 Druid Wiki 的數據庫多數據源中知曉 Spring 提供瞭對多數據源的支持,基於 Spring 提供的 AbstractRoutingDataSource,可以自己實現數據源的切換。

一、配置動態數據源

下面就如何配置動態數據源提供一個簡單的實現:

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,代碼如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

 @Nullable
 private Object defaultTargetDataSource;

 @Nullable
 private Map<Object, DataSource> resolvedDataSources;

 @Nullable
 private DataSource resolvedDefaultDataSource;

 @Override
 public Connection getConnection() throws SQLException {
 return determineTargetDataSource().getConnection();
 }

 @Override
 public Connection getConnection(String username, String password) throws SQLException {
 return determineTargetDataSource().getConnection(username, password);
 }

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

 /**
 * Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
 * <p>
 * Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the
 * {@link #resolveSpecifiedLookupKey} method.
 */
 @Nullable
 protected abstract Object determineCurrentLookupKey();
 
 // 省略相關代碼...
}

 重寫 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,可以實現對多數據源的支持

思路:

  • 重寫其 determineCurrentLookupKey() 方法,支持選擇不同的數據源
  • 初始化多個 DataSource 數據源到 AbstractRoutingDataSource 的 resolvedDataSources 屬性中
  • 然後通過 Spring AOP, 以自定義註解作為切點,根據不同的數據源的 Key 值,設置當前線程使用的數據源

接下來的實現方式是 Spring Boot 結合 Druid 配置動態數據源

(一)引入依賴

基於 3.繼承SpringBoot 中已添加的依賴再添加對AOP支持的依賴,如下:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(二)開始實現

1. DataSourceContextHolder

DataSourceContextHolder 使用 ThreadLocal 存儲當前線程指定的數據源的 Key 值,代碼如下:

package cn.tzh.mybatis.config;

import lombok.extern.slf4j.Slf4j;

import java.util.HashSet;
import java.util.Set;

/**
 * @author tzh
 * @date 2021/1/4 11:42
 */
@Slf4j
public class DataSourceContextHolder {

  /**
   * 線程本地變量
   */
  private static final ThreadLocal<String> DATASOURCE_KEY = new ThreadLocal<>();

  /**
   * 配置的所有數據源的 Key 值
   */
  public static Set<Object> ALL_DATASOURCE_KEY = new HashSet<>();

  /**
   * 設置當前線程的數據源的 Key
   *
   * @param dataSourceKey 數據源的 Key 值
   */
  public static void setDataSourceKey(String dataSourceKey) {
    if (ALL_DATASOURCE_KEY.contains(dataSourceKey)) {
      DATASOURCE_KEY.set(dataSourceKey);
    } else {
      log.warn("the datasource [{}] does not exist", dataSourceKey);
    }
  }

  /**
   * 獲取當前線程的數據源的 Key 值
   *
   * @return 數據源的 Key 值
   */
  public static String getDataSourceKey() {
    return DATASOURCE_KEY.get();
  }

  /**
   * 移除當前線程持有的數據源的 Key 值
   */
  public static void clear() {
    DATASOURCE_KEY.remove();
  }
}

2. MultipleDataSource

重寫其 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,代碼如下:

package cn.tzh.mybatis.config;

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

/**
 * @author tzh
 * @date 2021/1/4 11:44
 */
public class MultipleDataSource extends AbstractRoutingDataSource {

  /**
   * 返回當前線程是有的數據源的 Key
   *
   * @return dataSourceKey
   */
  @Override
  protected Object determineCurrentLookupKey() {
    return DataSourceContextHolder.getDataSourceKey();
  }
}

3. DataSourceAspect切面

使用 Spring AOP 功能,定義一個切面,用於設置當前需要使用的數據源,代碼如下:

package cn.tzh.mybatis.config;

import lombok.extern.log4j.Log4j2;
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;

/**
 * @author tzh
 * @date 2021/1/4 11:46
 */
@Aspect
@Component
@Log4j2
public class DataSourceAspect {

  @Before("@annotation(cn.tzh.mybatis.config.TargetDataSource)")
  public void before(JoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    if (method.isAnnotationPresent(TargetDataSource.class)) {
      TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
      DataSourceContextHolder.setDataSourceKey(targetDataSource.value());
      log.info("set the datasource of the current thread to [{}]", targetDataSource.value());
    } else if (joinPoint.getTarget().getClass().isAnnotationPresent(TargetDataSource.class)) {
      TargetDataSource targetDataSource = joinPoint.getTarget().getClass().getAnnotation(TargetDataSource.class);
      DataSourceContextHolder.setDataSourceKey(targetDataSource.value());
      log.info("set the datasource of the current thread to [{}]", targetDataSource.value());
    }
  }

  @After("@annotation(cn.tzh.mybatis.config.TargetDataSource)")
  public void after() {
    DataSourceContextHolder.clear();
    log.info("clear the datasource of the current thread");
  }
}

4. DruidConfig

Druid 配置類,代碼如下:

package cn.tzh.mybatis.config;

import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;
import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author tzh
 * @date 2021/1/4 11:49
 */
@Configuration
public class DruidConfig {
  

  @Bean(value = "druid-stat-interceptor")
  public DruidStatInterceptor druidStatInterceptor() {
    return new DruidStatInterceptor();
  }

  @Bean
  public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
    BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
    beanNameAutoProxyCreator.setProxyTargetClass(true);
    // 設置要監控的bean的id
    beanNameAutoProxyCreator.setInterceptorNames("druid-stat-interceptor");
    return beanNameAutoProxyCreator;
  }
}

5. MultipleDataSourceConfig

MyBatis 的配置類,配置瞭 2 個數據源,代碼如下:

package cn.tzh.mybatis.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.beans.FeatureDescriptor;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author tzh
 * @projectName code-demo
 * @title MultipleDataSourceConfig
 * @description
 * @date 2021/1/4 13:43
 */
@Configuration
@EnableConfigurationProperties({MybatisProperties.class})
public class MultipleDataSourceConfig {


  private final MybatisProperties properties;
  private final Interceptor[] interceptors;
  private final TypeHandler[] typeHandlers;
  private final LanguageDriver[] languageDrivers;
  private final ResourceLoader resourceLoader;
  private final DatabaseIdProvider databaseIdProvider;
  private final List<ConfigurationCustomizer> configurationCustomizers;

  public MultipleDataSourceConfig(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = (Interceptor[]) interceptorsProvider.getIfAvailable();
    this.typeHandlers = (TypeHandler[]) typeHandlersProvider.getIfAvailable();
    this.languageDrivers = (LanguageDriver[]) languageDriversProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = (DatabaseIdProvider) databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = (List) configurationCustomizersProvider.getIfAvailable();
  }


  @Bean(name = "master", initMethod = "init", destroyMethod = "close")
  @ConfigurationProperties(prefix = "spring.datasource.druid.master")
  public DruidDataSource master() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean(name = "slave", initMethod = "init", destroyMethod = "close")
  @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
  public DruidDataSource slave() {
    return DruidDataSourceBuilder.create().build();
  }

  @Bean(name = "dynamicDataSource")
  public DataSource dynamicDataSource() {
    MultipleDataSource dynamicRoutingDataSource = new MultipleDataSource();

    Map<Object, Object> dataSources = new HashMap<>();
    dataSources.put("master", master());
    dataSources.put("slave", slave());

    dynamicRoutingDataSource.setDefaultTargetDataSource(master());
    dynamicRoutingDataSource.setTargetDataSources(dataSources);

    DataSourceContextHolder.ALL_DATASOURCE_KEY.addAll(dataSources.keySet());

    return dynamicRoutingDataSource;
  }

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dynamicDataSource());
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }

    this.applyConfiguration(factory);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }

    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }

    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }

    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }

    if (this.properties.getTypeAliasesSuperType() != null) {
      factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
    }

    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }

    if (!ObjectUtils.isEmpty(this.typeHandlers)) {
      factory.setTypeHandlers(this.typeHandlers);
    }

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

    Set<String> factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
      factory.setScriptingLanguageDrivers(this.languageDrivers);
      if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
        defaultLanguageDriver = this.languageDrivers[0].getClass();
      }
    }

    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }

    return factory.getObject();
  }

  private void applyConfiguration(SqlSessionFactoryBean factory) {
    org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
    if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
      configuration = new org.apache.ibatis.session.Configuration();
    }

    if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
      Iterator var3 = this.configurationCustomizers.iterator();

      while (var3.hasNext()) {
        ConfigurationCustomizer customizer = (ConfigurationCustomizer) var3.next();
        customizer.customize(configuration);
      }
    }

    factory.setConfiguration(configuration);
  }

  @Bean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
  }

  @Bean
  public PlatformTransactionManager masterTransactionManager() {
    // 配置事務管理器
    return new DataSourceTransactionManager(dynamicDataSource());
  }
}

6. 添加配置

server:
 port: 9092
 servlet:
  context-path: /mybatis-springboot-demo
 tomcat:
  accept-count: 200
  min-spare-threads: 200
spring:
 application:
  name: mybatis-springboot-demo
 profiles:
  active: test
 servlet:
  multipart:
   max-file-size: 100MB
   max-request-size: 100MB
 datasource:
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
   master:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mybatis-demo
    username: root
    password: root
    initial-size: 5 # 初始化時建立物理連接的個數
    min-idle: 20 # 最小連接池數量
    max-active: 20 # 最大連接池數量
   slave:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mybatis-demo1
    username: root
    password: root
    initial-size: 5 # 初始化時建立物理連接的個數
    min-idle: 20 # 最小連接池數量
    max-active: 20 # 最大連接池數量
mybatis:
 type-aliases-package: cn.tzh.mybatis.entity
 mapper-locations: classpath:cn/tzh/mybatis/mapper/*.xml
 config-location: classpath:mybatis-config.xml
pagehelper:
 helper-dialect: mysql
 reasonable: true # 分頁合理化參數
 offset-as-page-num: true # 將 RowBounds 中的 offset 參數當成 pageNum 使用
 supportMethodsArguments: true # 支持通過 Mapper 接口參數來傳遞分頁參數

其中分別定義瞭 master 和 slave 數據源的相關配置

這樣一來,在 DataSourceAspect 切面中根據自定義註解,設置 DataSourceContextHolder 當前線程所使用的數據源的 Key 值,MultipleDataSource 動態數據源則會根據該值設置需要使用的數據源,完成瞭動態數據源的切換

7. 使用示例

在 Mapper 接口上面添加自定義註解 @TargetDataSource,如下:

package cn.tzh.mybatis.mapper;

import cn.tzh.mybatis.config.TargetDataSource;
import cn.tzh.mybatis.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * @author tzh
 * @date 2020/12/28 14:29
 */
@Mapper
public interface UserMapper {

  User selectUserOne(@Param("id") Long id);

  @TargetDataSource("slave")
  User selectUserTwo(@Param("id") Long id);
}

 總結

上面就如何配置動態數據源的實現方式僅提供一種思路,其中關於多事務方面並沒有實現,采用 Spring 提供的事務管理器,如果同一個方法中使用瞭多個數據源,並不支持多事務的,需要自己去實現(筆者能力有限),可以整合JAT組件,參考:SpringBoot2 整合JTA組件多數據源事務管理

分佈式事務解決方案推薦使用 Seata 分佈式服務框架

到此這篇關於SpringBoot配置數據庫密碼加密的實現的文章就介紹到這瞭,更多相關SpringBoot 數據庫密碼加密內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: