refactor: 使用Mybatis plus插件实现动态数据源

This commit is contained in:
valarchie 2022-12-24 16:39:01 +08:00
parent 5fbf145962
commit 364091fe9a
15 changed files with 89 additions and 409 deletions

View File

@ -12,6 +12,7 @@ import com.agileboot.domain.system.notice.query.NoticeQuery;
import com.agileboot.infrastructure.annotations.AccessLog;
import com.agileboot.infrastructure.annotations.Resubmit;
import com.agileboot.orm.common.enums.BusinessTypeEnum;
import com.baomidou.dynamic.datasource.annotation.DS;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@ -52,6 +53,18 @@ public class SysNoticeController extends BaseController {
return ResponseDTO.ok(pageDTO);
}
/**
* 获取通知公告列表
* 从从库获取数据 例子 仅供参考
*/
@DS("slave")
@PreAuthorize("@permission.has('system:notice:list')")
@GetMapping("/listFromSlave")
public ResponseDTO<PageDTO> listFromSlave(NoticeQuery query) {
PageDTO pageDTO = noticeApplicationService.getNoticeList(query);
return ResponseDTO.ok(pageDTO);
}
/**
* 根据通知公告编号获取详细信息
*/

View File

@ -194,6 +194,13 @@
</exclusions>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,28 +0,0 @@
package com.agileboot.infrastructure.annotations;
import com.agileboot.infrastructure.enums.DataSourceType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义多数据源切换注解
*
* 优先级先方法后类如果方法覆盖了类上的数据源类型以方法的为准否则以类上的为准
*
* @author ruoyi
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
/**
* 切换数据源名称
*/
DataSourceType value() default DataSourceType.MASTER;
}

View File

@ -9,8 +9,8 @@ import java.lang.annotation.Target;
/**
* 自定义注解防止表单重复提交
*
* @author ruoyi
* 仅生效于有RequestBody注解的参数 因为使用RequestBodyAdvice来实现
* @author valarchie
*/
@Inherited
@Target(ElementType.METHOD)

View File

@ -1,60 +0,0 @@
package com.agileboot.infrastructure.aspectj;
import com.agileboot.infrastructure.annotations.DataSource;
import com.agileboot.infrastructure.datasource.DynamicDataSourceContextHolder;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 多数据源处理
*
* @author ruoyi
*/
@Aspect
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
@Pointcut("@annotation(com.agileboot.infrastructure.annotations.DataSource)"
+ "|| @within(com.agileboot.infrastructure.annotations.DataSource)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (dataSource != null) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
/**
* 获取需要切换的数据源
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}

View File

@ -55,6 +55,9 @@ public class SecurityConfig {
@NonNull
private CorsFilter corsFilter;
/**
* @see com.agileboot.infrastructure.web.service.UserDetailsServiceImpl#loadUserByUsername
*/
@Bean
public AuthenticationManager authManager(HttpSecurity http, PasswordEncoder passwordEncoder,
UserDetailsService userDetailService)
@ -118,5 +121,4 @@ public class SecurityConfig {
}
}

View File

@ -1,124 +0,0 @@
package com.agileboot.infrastructure.config.druid;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.infrastructure.datasource.DynamicDataSource;
import com.agileboot.infrastructure.enums.DataSourceType;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* druid 配置多数据源
* 数据事务相关的bean 在spring启动时会报这个错
* is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
* 不用管因为dateSource相关的bean 本身就不需要被AOP 这个warning可以不用管
* 如果service之类的bean 报这种错的话 就需要排查
* @author ruoyi
*/
@Configuration
@Slf4j
public class DruidConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
@ConditionalOnExpression("'${agileboot.embedded.mysql}' != 'true'")
public DataSource masterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnExpression("'${agileboot.embedded.mysql}' != 'true'")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
// @Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(8);
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称
* @param beanName bean名称
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
try {
DataSource dataSource = SpringUtil.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
} catch (Exception e) {
log.error("设置数据库失败", e);
}
}
/**
* 去除监控页面底部的广告
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js";
// 创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
// 重置缓冲区响应头不会被重置
response.resetBuffer();
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

View File

@ -1,78 +0,0 @@
package com.agileboot.infrastructure.config.druid;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration;
/**
* druid 配置属性
*
* @author ruoyi
*/
@ConditionalOnExpression("'${agileboot.embedded.mysql}' != 'true'")
@Configuration
public class DruidProperties {
@Value("${spring.datasource.druid.initialSize}")
private int initialSize;
@Value("${spring.datasource.druid.minIdle}")
private int minIdle;
@Value("${spring.datasource.druid.maxActive}")
private int maxActive;
@Value("${spring.datasource.druid.maxWait}")
private int maxWait;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}")
private boolean testOnReturn;
public DruidDataSource dataSource(DruidDataSource datasource) {
// 配置初始化大小最小最大
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
// 配置获取连接等待超时的时间
datasource.setMaxWait(maxWait);
// 配置间隔多久才进行一次检测检测需要关闭的空闲连接单位是毫秒
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// 配置一个连接在池中最小最大生存的时间单位是毫秒
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
//用来检测连接是否有效的sql要求是一个查询语句常用select 'x'如果validationQuery为null
// testOnBorrowtestOnReturntestWhileIdle都不会起作用
datasource.setValidationQuery(validationQuery);
// 建议配置为true不影响性能并且保证安全性申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis
// 执行validationQuery检测连接是否有效
datasource.setTestWhileIdle(testWhileIdle);
// 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能
datasource.setTestOnBorrow(testOnBorrow);
// 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能
datasource.setTestOnReturn(testOnReturn);
return datasource;
}
}

View File

@ -1,24 +0,0 @@
package com.agileboot.infrastructure.datasource;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源
*
* @author ruoyi
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}

View File

@ -1,40 +0,0 @@
package com.agileboot.infrastructure.datasource;
import lombok.extern.slf4j.Slf4j;
/**
* 数据源切换处理
*
* @author ruoyi
*/
@Slf4j
public class DynamicDataSourceContextHolder {
/**
* 使用ThreadLocal维护变量ThreadLocal为每个使用该变量的线程提供独立的变量副本
* 所以每一个线程都可以独立地改变自己的副本而不会影响其它线程所对应的副本
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(String dsType) {
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}

View File

@ -45,4 +45,5 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
chain.doFilter(request, response);
}
}

View File

@ -26,4 +26,5 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, S
ResponseDTO<Object> responseDTO = ResponseDTO.fail(Client.COMMON_NO_AUTHORIZATION, request.getRequestURI());
ServletHolderUtil.renderString(response, JSONUtil.toJsonStr(responseDTO));
}
}

View File

@ -4,37 +4,6 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:33067/agileboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 12345
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
@ -55,6 +24,41 @@ spring:
wall:
config:
multi-statement-allow: true
dynamic:
primary: master
strict: false
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
datasource:
# 主库数据源
master:
url: jdbc:mysql://localhost:33067/agileboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 12345
# 从库数据源
# slave:
# url: jdbc:mysql://localhost:33067/agileboot2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# username: root
# password: 12345
# redis 配置
redis:
# 地址

View File

@ -1,21 +1,27 @@
# 数据源配置
spring:
datasource:
# 驱动
driver-class-name: org.h2.Driver
# h2 内存数据库 内存模式连接配置 库名: agileboot
url: jdbc:h2:mem:agileboot;DB_CLOSE_DELAY=-1
h2:
# 开启console 访问 默认false
console:
enabled: true
settings:
# 开启h2 console 跟踪 方便调试 默认 false
trace: true
# 允许console 远程访问 默认false
web-allow-others: true
# h2 访问路径上下文
path: /h2-console
dynamic:
primary: master
strict: false
datasource:
master:
# h2 内存数据库 内存模式连接配置 库名: agileboot
url: jdbc:h2:mem:agileboot;DB_CLOSE_DELAY=-1
h2:
# 开启console 访问 默认false
console:
enabled: true
settings:
# 开启h2 console 跟踪 方便调试 默认 false
trace: true
# 允许console 远程访问 默认false
web-allow-others: true
# h2 访问路径上下文
path: /h2-console
sql:
init:

16
pom.xml
View File

@ -217,14 +217,6 @@
<version>${hutool.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>junit</groupId>-->
<!-- <artifactId>junit</artifactId>-->
<!-- <version>${junit.version}</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -272,6 +264,14 @@
<version>${com.google.guava.version}</version>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>