SpringBoot配置数据库密码加密的实现

时间:2022-09-23 08:13:16

你在使用 MyBatis 的过程中,是否有想过多个数据源应该如何配置,如何去实现?出于这个好奇心,我在 Druid Wiki 的数据库多数据源中知晓 Spring 提供了对多数据源的支持,基于 Spring 提供的 AbstractRoutingDataSource,可以自己实现数据源的切换。

一、配置动态数据源

下面就如何配置动态数据源提供一个简单的实现:

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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支持的依赖,如下:

?
1
2
3
4
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(二)开始实现

1. DataSourceContextHolder

DataSourceContextHolder 使用 ThreadLocal 存储当前线程指定的数据源的 Key 值,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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() 方法,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 功能,定义一个切面,用于设置当前需要使用的数据源,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 配置类,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 个数据源,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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. 添加配置

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 数据库密码加密内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/Zack_tzh/article/details/112175270