SpringBoot 项目 pom.xml

-----------------------------------------------------------------------------------------------------------------

SpringBoot 项目 pom.xml properties 节点详解(按功能分类,新手易懂)

 
<properties> 是 Maven 项目的版本统一管理节点,核心作用是「将所有依赖的版本号集中定义,避免多处重复写版本、版本不一致导致的冲突」。下面按功能模块拆解每一个配置项的作用、使用场景,结合实际开发说明核心注意点:
 

一、基础环境配置(JDK / 编译 / 核心框架)

 
表格
 
配置项含义 & 作用关键说明
<java.version>1.8</java.version> 指定项目使用的 JDK 版本为 1.8(即 Java 8) SpringBoot 2.7.x 最低支持 JDK 8,最高兼容 JDK 11;该配置会同步影响 Maven 编译和 SpringBoot 运行环境
<maven.compiler.source>8</maven.compiler.source> 指定 Maven 编译源码的 JDK 版本为 8 java.version 保持一致,避免编译版本与运行版本不一致导致的 UnsupportedClassVersionError
<maven.compiler.target>8</maven.compiler.target> 指定编译后字节码的目标 JDK 版本为 8 同上,保证编译产物能在 JDK 8 环境运行
<org.springframework.boot.version>2.7.18</org.springframework.boot.version> SpringBoot 核心版本(2.7.18 是 2.7.x 最后一个稳定版) 2.7.x 是长期支持(LTS)版本,兼容 Spring Cloud 2021.x,适合生产环境;若升级需注意依赖兼容性
<tomcat.version>9.0.96</tomcat.version> 内置 Tomcat 容器版本 SpringBoot 2.7.x 默认集成 Tomcat 9,该配置覆盖默认版本,修复已知漏洞(如 9.0.96 修复了多个安全漏洞)
 

二、微服务 & 注册配置中心

 
表格
 
配置项含义 & 作用关键说明
<spring.cloud.version>3.1.6</spring.cloud.version> Spring Cloud 核心版本(2021.0.6 对应 3.1.x) 与 SpringBoot 2.7.x 配套(Spring Cloud 2021.x ↔ SpringBoot 2.6.x/2.7.x),避免版本不兼容
<spring.cloud.gateway.version>3.1.7</spring.cloud.gateway.version> Spring Cloud Gateway 网关版本 微服务入口,负责路由转发、限流、认证;3.1.7 修复了网关转发的多个 bug
<com.alibaba.cloud.nacos.version>2021.0.5.0</com.alibaba.cloud.nacos.version> Nacos 注册 / 配置中心版本 阿里微服务生态核心,2021.0.5.0 适配 Spring Cloud 2021.x,支持服务注册、配置动态刷新
<com.alibaba.cloud.nacos.client.version>2.3.1</com.alibaba.cloud.nacos.client.version> Nacos 客户端版本 与 Nacos 服务端通信的核心依赖,需和服务端版本兼容(2.3.1 兼容 Nacos 2.x 服务端)
<sentinel.version>1.8.6</sentinel.version> Sentinel 限流 / 熔断版本 阿里开源的微服务治理组件,用于接口限流、熔断降级,保护系统不被高并发压垮
<feign.core.version>13.5</feign.core.version> Feign 声明式 HTTP 客户端核心版本 微服务间调用的核心组件,简化 HTTP 请求代码(如 @FeignClient 注解)
<feign.httpclient.version>11.10</feign.httpclient.version> Feign 整合 HttpClient 版本 替换 Feign 默认的 URLConnection,提升 HTTP 调用性能和稳定性
<feign.okhttp.version>13.5</feign.okhttp.version> Feign 整合 OkHttp 版本 可选的 HTTP 客户端,性能优于 HttpClient,按需启用
<feign.form.version>3.8.0</feign.form.version> Feign 支持表单提交(form-data)版本 解决 Feign 调用时传递表单参数的问题(如文件上传、表单提交)
 

三、数据层(数据库 / ORM / 缓存 / 检索)

 
表格
 
配置项含义 & 作用关键说明
<mysql.version>8.0.29</mysql.version> MySQL 驱动版本 8.0.x 适配 MySQL 8.0 数据库,需注意驱动类从 com.mysql.jdbc.Driver 改为 com.mysql.cj.jdbc.Driver
<com.dameng.version>8.1.3.62</com.dameng.version> 达梦数据库驱动版本 国产数据库,适配达梦 8.x 版本,常用于政务 / 国企项目
<com.kingbase8.version>8.6.0</com.kingbase8.version> 人大金仓数据库驱动版本 另一款国产数据库,8.6.0 是稳定版
<kingbase.sqlserver.version>9.0.0</kingbase.sqlserver.version> 金仓兼容 SQLServer 驱动版本 适配金仓数据库模拟 SQLServer 环境的场景
<kesDialect.hibernate4.version>2.0.1</kesDialect.hibernate4.version> 金仓 Hibernate 方言版本 解决 Hibernate 操作金仓数据库的语法兼容问题
<mybaits-plus.version>3.5.3</mybaits-plus.version> MyBatis-Plus 版本(注意拼写错误:mybaits → mybatis) MyBatis 增强工具,简化 CRUD 操作(如 BaseMapper、分页插件);3.5.3 是稳定版,兼容 SpringBoot 2.7.x
<redisson.starter.version>3.17.6</redisson.starter.version> Redisson(Redis 客户端)SpringBoot 启动器版本 比 Jedis 更强大的 Redis 客户端,支持分布式锁、集合操作,3.17.6 兼容 Redis 6.x/7.x
<elasticsearch.version>8.9.0</elasticsearch.version> Elasticsearch 客户端版本 全文检索引擎,8.9.0 是高版本,需注意客户端与服务端版本一致(避免通信协议不兼容)
 

四、开发效率 & 工具类

 
表格
 
配置项含义 & 作用关键说明
<org.projectlombok.version>1.18.30</org.projectlombok.version> Lombok 版本 简化实体类代码(如 @Data@Slf4j),1.18.30 修复了与 JDK 8/11 的兼容问题
<cn.hutool.vsersion>5.8.37</cn.hutool.vsersion> Hutool 工具类版本(注意拼写错误:vsersion → version) 国产全能工具类库,覆盖字符串、日期、加密、IO 等场景,5.8.37 是稳定版
<com.google.guava.version>30.1.1-jre</com.google.guava.version> Google Guava 工具类版本 谷歌开源的工具库,补充 Java 集合、缓存、并发等能力
<uuid-creator.version>5.3.3</uuid-creator.version> UUID 生成工具版本 比 JDK 自带的 UUID 更灵活,支持自定义规则生成唯一 ID
<mapstruct.version>1.5.2.Final</mapstruct.version> MapStruct 版本 实体类映射工具(如 DO → DTO),编译期生成映射代码,性能优于反射(如 BeanUtils)
<com.github.dozermapper.version>6.4.1</com.github.dozermapper.version> Dozer 映射工具版本 老牌实体映射工具,基于反射,适合简单场景(MapStruct 更推荐)
<commons.io.version>2.7</commons.io.version> Apache Commons IO 版本(注意重复定义:2.5/2.7) IO 工具类(文件读写、流操作),建议统一用 2.7 版本
<commons-io.version>2.5</commons-io.version> 重复定义,需删除其中一个 -
<org.apache.commons.version>1.9.4</org.apache.commons.version> Apache Commons 核心工具版本 通用工具类(如字符串、集合、数学计算)
<commons-pool2.version>2.11.1</commons-pool2.version> 连接池工具版本 为 Redis、数据库等提供连接池实现,SpringBoot 底层依赖
<jasypt.springboot.version>3.0.5</jasypt.springboot.version> Jasypt 加密启动器版本 加密配置文件中的敏感信息(如数据库密码),3.0.5 适配 SpringBoot 2.7.x
 

五、Web & 安全

 
表格
 
配置项含义 & 作用关键说明
<org.springframework.security.version>5.7.12</org.springframework.security.version> Spring Security 版本 安全框架,负责认证、授权、防跨站攻击,5.7.12 是 LTS 版本,适配 SpringBoot 2.7.x
<io.jsonwebtoken.version>0.9.1</io.jsonwebtoken.version> JWT 令牌生成工具版本 用于生成 / 解析 JWT 令牌(前后端分离认证),0.9.1 是经典版本(注意:高版本已升级为 jjwt-api)
<snakeyaml.version>2.0</snakeyaml.version> YAML 解析工具版本 SpringBoot 解析 application.yml 配置文件的核心依赖,2.0 修复了安全漏洞
<org.springdoc.version>1.6.6</org.springdoc.version> SpringDoc 版本 生成 OpenAPI 3.0 接口文档(替代 Swagger2),适配 SpringBoot 2.7.x
<com.github.xiaoymin.version>4.4.0</com.github.xiaoymin.version> Knife4j 版本 基于 Swagger/OpenAPI 的接口文档增强工具,美化界面、支持导出
 

六、消息 & 任务

 
表格
 
配置项含义 & 作用关键说明
<rocketmq.starter.version>2.2.3</rocketmq.starter.version> RocketMQ SpringBoot 启动器版本 集成 RocketMQ 消息队列,2.2.3 是稳定版,适配 SpringBoot 2.7.x
<xxl-job.version>2.4.0</xxl-job.version> XXL-Job 分布式任务调度版本 替代 Quartz 的分布式定时任务框架,2.4.0 支持 SpringBoot 2.7.x
 

七、文件 & 报表

 
表格
 
配置项含义 & 作用关键说明
<org.apache.poi.version>4.1.2</org.apache.poi.version> POI 版本 操作 Excel/WPS 文档的核心依赖,4.1.2 支持 Excel 2007+ 格式
<easyexcel.verion>4.0.3</easyexcel.verion> EasyExcel 版本(注意拼写错误:verion → version) 阿里开源的 Excel 操作工具,解决 POI 内存溢出问题,4.0.3 适配 SpringBoot 2.7.x
<freemarker.version>2.3.31</freemarker.version> FreeMarker 模板引擎版本 生成动态页面、导出 Word/PDF 模板,2.3.31 是稳定版
<e-iceblue.spire.doc.version>13.11.2</e-iceblue.spire.doc.version> Spire.Doc 版本 操作 Word 文档的商业工具(免费版有页数限制),支持复杂文档编辑
<e-iceblue.spire.pdf.version>10.8.1</e-iceblue.spire.pdf.version> Spire.PDF 版本 操作 PDF 文档的工具,支持生成、编辑、转换 PDF
<net.lingala.zip4j.version>2.9.0</net.lingala.zip4j.version> Zip4j 版本 压缩 / 解压 ZIP 文件,支持密码保护、分卷压缩
<commons.fileupload.version>1.5</commons.fileupload.version> 文件上传组件版本 处理 HTTP 文件上传(如表单上传文件),1.5 修复了安全漏洞
<commons.compress.version>1.21</commons.compress.version> 压缩工具版本 支持更多压缩格式(如 tar、gz)
 

八、网络 & 通信

 
表格
 
配置项含义 & 作用关键说明
<io.netty.version>4.1.94.Final</io.netty.version> Netty 网络框架版本 高性能 NIO 框架,RocketMQ、Elasticsearch 等底层依赖,4.1.94 是稳定版
<org.apache.httpcomponents.version>4.5.14</org.apache.httpcomponents.version> HttpClient 版本 HTTP 客户端,用于调用外部接口,4.5.14 修复了多个安全漏洞
<axis.version>1.4</axis.version> Axis 版本 老旧的 WebService 框架,适配遗留系统(新系统建议用 Spring Cloud OpenFeign)
<cxf.version>3.4.4</cxf.version> CXF 版本 新一代 WebService 框架,支持 REST/SOAP 协议
<commonsNet.version>3.8.0</commonsNet.version> Apache Commons Net 版本 网络工具类(如 FTP、SMTP、Telnet 客户端)
 

九、日志 & 监控

 
表格
 
配置项含义 & 作用关键说明
<logback.version>1.2.13</logback.version> Logback 日志框架版本(注意重复定义:1.2.9/1.2.13) SpringBoot 默认日志框架,1.2.13 修复了安全漏洞,建议统一用该版本
<logback.version>1.2.9</logback.version> 重复定义,需删除 -
<skywalking.toolkit.version>9.6.0</skywalking.toolkit.version> SkyWalking 监控工具包版本 分布式链路追踪、性能监控,9.6.0 适配 SpringBoot 2.7.x
 

十、其他依赖(兼容 / 适配)

 
表格
 
配置项含义 & 作用关键说明
<maven.build.timestamp>yyyy-MM-dd HH:mm</maven.build.timestamp> 构建时间戳格式 可在项目中引用构建时间(如 ${maven.build.timestamp}),用于版本号、文档生成
<com.fasterxml.jackson.datatype.version>2.13.2</com.fasterxml.jackson.datatype.version> Jackson 数据类型扩展版本 JSON 序列化 / 反序列化工具,适配 Java 8 时间类型(LocalDateTime)
<org.dom4j.version>2.1.3</org.dom4j.version> Dom4j 版本 XML 解析工具,2.1.3 修复了安全漏洞
<jaxen.version>1.2.0</jaxen.version> XPath 解析工具版本 配合 Dom4j 解析 XML 中的 XPath 表达式
<pinyin4j.version>2.5.1</pinyin4j.version> 拼音转换工具版本 中文转拼音(如姓名、地址转拼音)
<jakarta-json.version>2.0.1</jakarta-json.version> Jakarta JSON 版本 适配 Jakarta EE 规范的 JSON 工具,替代老旧的 javax.json
<wsdl4j.version>1.6.3</wsdl4j.version> WSDL 解析工具版本 解析 WebService 的 WSDL 文档
<commons.discovery.version>0.2</commons.discovery.version> 服务发现工具版本 Axis/CXF 底层依赖,用于加载配置、类查找
<common.fileload.version>1.3.3</common.fileload.version> 自定义文件加载组件版本(非开源) 项目内部封装的文件加载工具,需确保依赖包存在
 

关键问题修正(避免踩坑)

 
  1. 拼写错误:
    • cn.hutool.vsersioncn.hutool.version
    • mybaits-plus.versionmybatis-plus.version
    • easyexcel.verioneasyexcel.version
     
  2. 版本重复定义:
    • commons.io.version:2.5 和 2.7 重复,建议保留 2.7(新版本);
    • logback.version:1.2.9 和 1.2.13 重复,保留 1.2.13;
     
  3. 版本兼容性:
    • SpringBoot 2.7.x 不兼容 Spring Cloud 2022.x,当前 spring.cloud.version=3.1.6(2021.0.6)是匹配的;
    • MyBatis-Plus 3.5.3 与 SpringBoot 2.7.x 完全兼容,无需升级;
    • RocketMQ Starter 2.2.3 适配 SpringBoot 2.7.x,若升级 SpringBoot 3.x 需换 2.3.x 版本。
     
 

总结

 
  1. <properties> 核心价值是版本统一管理,避免分散定义导致的版本冲突,降低维护成本;
  2. 该项目依赖覆盖「微服务、数据库、工具类、消息、安全、文件处理」等全场景,是典型的企业级 SpringBoot 项目配置;
  3. 需修正拼写错误、重复版本定义,保证版本与 SpringBoot 2.7.18 兼容,避免启动报错或运行时异常。

-----------------------------------------------------------------------------------------------------------------

dynamic-datasource-spring-boot-starter 的作用

一、dynamic-datasource-spring-boot-starter 核心作用

dynamic-datasource-spring-boot-starter 是一款基于 Spring Boot 的动态数据源切换组件,核心目的是帮你在 Spring Boot 项目中无侵入、低成本地实现多数据源切换(比如读写分离、分库分表、多租户按库隔离等场景),无需手动编写复杂的数据源切换逻辑。
简单说:它把「多数据源配置、动态切换、负载均衡」等能力封装成 Starter,你只需少量配置就能实现数据源的灵活切换,不用改核心业务代码。

二、核心功能(新手易懂版)

1. 多数据源配置

支持配置多个数据源(主库、从库、不同业务库),只需在 application.yml/application.properties 中声明即可,无需手动创建 DataSource 实例。示例配置:
yaml
 
 
spring:
  datasource:
    dynamic:
      # 主数据源(默认数据源)
      primary: master
      # 多数据源列表
      datasource:
        master: # 主库
          url: jdbc:mysql://localhost:3306/db_master
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave1: # 从库1
          url: jdbc:mysql://localhost:3306/db_slave1
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave2: # 从库2
          url: jdbc:mysql://localhost:3306/db_slave2
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
 

2. 动态切换数据源

通过注解 / API 两种方式切换数据源,对业务代码无侵入:
  • 注解方式(最常用):在 Service 方法上加 @DS 注解指定数据源
    java
     
    运行
     
     
     
     
    @Service
    public class UserService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        // 使用主库(默认)
        public void addUser(User user) {
            jdbcTemplate.update("INSERT INTO user(name) VALUES (?)", user.getName());
        }
    
        // 手动指定从库1
        @DS("slave1")
        public List<User> listUser() {
            return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));
        }
    
        // 从库负载均衡(组件自动轮询slave1/slave2)
        @DS("slave") // 需先配置slave为从库组
        public List<User> listUserBySlave() {
            return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));
        }
    }
    
     
     
  • API 方式:手动编码切换(适合复杂逻辑)
    java
     
    运行
     
     
     
     
    // 切换到slave2
    DynamicDataSourceContextHolder.push("slave2");
    // 执行业务逻辑
    listUser();
    // 移除当前数据源(避免污染)
    DynamicDataSourceContextHolder.poll();
    
     
     

3. 高级能力(适配信创 / 复杂场景)

  • 支持读写分离:主库写、从库读,组件可自动做负载均衡;
  • 支持分库分表:配合 ShardingSphere 实现更复杂的分库分表;
  • 支持国产数据库:适配达梦、人大金仓、高斯等信创数据库;
  • 支持动态新增 / 删除数据源:运行时可通过 API 新增数据源(适合多租户场景);
  • 事务支持:保证切换数据源后事务的正确性。

三、适用场景(信创 / 业务开发常用)

  1. 读写分离:主库(MySQL/Oracle)负责写操作,从库负责读操作,提升性能;
  2. 多业务库隔离:不同业务模块用不同数据库(比如用户库、订单库),通过注解切换;
  3. 多租户按库隔离:每个租户一个独立数据库,运行时根据租户 ID 切换数据源;
  4. 信创改造:部分业务迁到国产数据库(如达梦),部分保留原有库,通过动态数据源平滑过渡。

四、使用前置条件

  1. 项目基于 Spring Boot(2.x/3.x 均支持);
  2. 引入依赖(Maven 示例):
    xml
     
     
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.6.1</version> <!-- 推荐稳定版 -->
    </dependency>
    
     
     
  3. 配置数据源信息(如上 application.yml 示例)。

总结

  1. dynamic-datasource-spring-boot-starter 核心是简化多数据源切换,无需手动编写数据源切换逻辑,注解 / API 即可实现;
  2. 适配信创场景(国产数据库)、读写分离、多租户等主流业务需求,是 Spring Boot 项目多数据源的首选组件;
  3. 对业务代码无侵入,配置简单,学习成本低,适合快速落地多数据源架构。
seal springboot
你提到的 "seal springboot" 我理解为你想了解 如何在 Spring Boot 项目中实现接口防重复提交(Seal / 防重),这是企业级开发中非常常见的需求(比如防止用户重复点击下单、提交表单等)。
下面我会提供一个基于 Redis + 自定义注解 + AOP 的通用、可落地的 Spring Boot 防重复提交解决方案。

一、实现思路

  1. 自定义注解:标记需要防重的接口方法
  2. AOP 切面:拦截带注解的方法,统一处理防重逻辑
  3. Redis 存储:利用 Redis 的原子操作(SETNX)记录请求标识,设置过期时间
  4. 请求标识生成:基于用户 ID + 接口路径 + 请求参数(或业务唯一标识)生成唯一 KEY

二、完整代码实现

1. 第一步:引入依赖(pom.xml)

确保项目中引入 Spring Boot Web、Redis、AOP 相关依赖:
xml
 
 
<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Spring Boot AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- 可选:简化JSON处理 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.45</version>
</dependency>
 

2. 第二步:自定义防重注解(RepeatSubmit.java)

java
 
运行
 
 
 
 
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交注解
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface RepeatSubmit {
    /**
     * 防重过期时间(默认5秒)
     */
    long expireTime() default 5;

    /**
     * 时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 提示信息
     */
    String message() default "请勿重复提交,请稍后再试!";
}
 

3. 第三步:AOP 切面实现(RepeatSubmitAspect.java)

java
 
运行
 
 
 
 
import com.alibaba.fastjson2.JSON;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交切面
 */
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 切点:拦截所有带@RepeatSubmit注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")
    public void repeatSubmitPointcut() {}

    @Around("repeatSubmitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取当前请求和注解信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);

        // 2. 生成唯一请求标识(核心:确保同一用户+同一请求的唯一性)
        String userId = getCurrentUserId(request); // 替换为你的真实用户ID获取逻辑
        String requestURI = request.getRequestURI();
        String params = JSON.toJSONString(joinPoint.getArgs()); // 请求参数
        String key = "repeat_submit:" + md5(userId + ":" + requestURI + ":" + params);

        // 3. Redis原子操作:SETNX(不存在则设置,存在则返回false)
        boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", annotation.expireTime(), annotation.timeUnit());

        if (!success) {
            // 重复提交,抛出异常(可自定义业务异常)
            throw new RuntimeException(annotation.message());
        }

        // 4. 执行原方法
        try {
            return joinPoint.proceed();
        } finally {
            // 可选:如果是幂等接口,可在方法执行完成后主动删除KEY(避免过期时间内无法重复提交合法请求)
            // stringRedisTemplate.delete(key);
        }
    }

    /**
     * 获取当前登录用户ID(示例:实际项目中替换为你的鉴权逻辑,如从Token/Session中获取)
     */
    private String getCurrentUserId(HttpServletRequest request) {
        // 示例:从Header获取token解析用户ID,或从Session获取
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            return "anonymous"; // 未登录用户用匿名标识
        }
        // 此处替换为你的真实解析逻辑
        return "user123";
    }

    /**
     * MD5加密(生成短且唯一的KEY)
     */
    private String md5(String content) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("MD5加密失败", e);
        }
    }
}
 

4. 第四步:配置 Redis(application.yml)

yaml
 
 
spring:
  redis:
    host: localhost # 你的Redis地址
    port: 6379
    password: # 无密码则留空
    database: 0
 

5. 第五步:使用示例(Controller)

java
 
运行
 
 
 
 
import com.example.demo.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    /**
     * 下单接口:5秒内禁止重复提交
     */
    @PostMapping("/order/submit")
    @RepeatSubmit(expireTime = 5, message = "下单请求已提交,请勿重复操作!")
    public String submitOrder(@RequestBody OrderRequest request) {
        // 执行业务逻辑(创建订单)
        return "下单成功,订单号:" + System.currentTimeMillis();
    }

    // 测试用的请求实体
    public static class OrderRequest {
        private Long productId;
        private Integer count;

        // getter/setter 省略
    }
}
 

三、关键说明

  1. 用户 ID 获取:getCurrentUserId 方法需要替换为你项目的真实鉴权逻辑(如从 JWT Token、Session 中解析),否则匿名用户的防重会失效。
  2. 过期时间设置:根据业务场景调整(比如支付接口可设 30 秒,普通表单设 5 秒)。
  3. 幂等性考虑:如果接口是幂等的(重复执行不影响结果),可在方法执行完成后主动删除 Redis KEY,避免用户正常重试被拦截。
  4. 异常处理:建议自定义业务异常(如 RepeatSubmitException),并通过全局异常处理器返回友好提示。

四、测试验证

  1. 启动 Redis 和 Spring Boot 项目
  2. 快速重复调用 /order/submit 接口(比如用 PostMan 连续点击发送)
  3. 第一次调用成功,5 秒内的重复调用会返回:{"msg":"下单请求已提交,请勿重复操作!","code":500}

总结

  1. 核心原理:通过 Redis 的 SETNX 原子操作实现请求唯一性校验,结合 AOP 统一拦截,无侵入式实现防重。
  2. 关键要素:唯一 KEY 的生成(用户 ID + 接口路径 + 请求参数)是防重的核心,确保仅拦截同一用户的重复请求。
  3. 灵活扩展:可根据业务需求调整过期时间、KEY 生成规则(比如仅用业务唯一标识,如订单号),或扩展支持分布式场景(Redis 集群)。

 

dynamic-datasource-spring-boot-starter使用详解

dynamic-datasource-spring-boot-starter 完整使用详解

dynamic-datasource-spring-boot-starter(简称动态数据源组件)是由苞米豆(baomidou)开源的 Spring Boot 多数据源解决方案,能极低成本实现多数据源切换、读写分离、动态增删数据源等能力。以下是从基础配置到高级用法的全套实操指南,新手也能直接落地。

一、核心前置准备

1. 环境要求

  • Spring Boot:2.0.x ~ 3.x(3.x 需使用组件 3.6.0+ 版本)
  • JDK:8+(信创场景建议 JDK 8/11)
  • 依赖管理:Maven/Gradle

2. 引入依赖(Maven)

xml
 
 
<!-- 核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.6.1</version> <!-- 稳定版,适配Spring Boot 2/3 -->
</dependency>

<!-- 数据库驱动(根据实际数据库选择,信创库需对应驱动) -->
<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 达梦(信创) -->
<dependency>
    <groupId>com.dameng</groupId>
    <artifactId>DmJdbcDriver18</artifactId>
    <version>8.1.2.47</version>
</dependency>
<!-- MyBatis-Plus(可选,无也可使用) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
 

二、基础用法:多数据源配置与切换

1. 配置文件(application.yml)

核心是在 spring.datasource.dynamic 下配置主数据源和从数据源,支持多类型数据库混合配置(如 MySQL + 达梦)。
yaml
 
 
spring:
  datasource:
    dynamic:
      # 1. 核心配置
      primary: master # 默认数据源(未指定时使用)
      strict: false # 非严格模式:找不到指定数据源时用默认库;true则抛异常
      datasource:
        # 2. 主库(MySQL)
        master:
          url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          # 连接池配置(默认使用HikariCP,可自定义)
          hikari:
            maximum-pool-size: 20
            minimum-idle: 5
            connection-timeout: 30000
        # 3. 从库1(MySQL)
        slave1:
          url: jdbc:mysql://127.0.0.1:3306/db_slave1?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 4. 从库2(达梦,信创示例)
        slave2:
          url: jdbc:dm://192.168.1.100:5236/DB_SLAVE2?SYSDBA=1
          username: SYSDBA
          password: DAMENG123
          driver-class-name: dm.jdbc.driver.DmDriver
      # 5. 全局配置(可选,统一设置所有数据源的连接池)
      hikari:
        max-lifetime: 1800000
 

2. 注解切换数据源(@DS)

这是最常用的方式,通过 @DS 注解指定方法 / 类使用的数据源,优先级:方法注解 > 类注解 > 默认数据源。

(1)基础使用示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
// 类上注解:该类所有方法默认使用slave1(可省略,默认用master)
// @DS("slave1")
public class UserService {

    @Resource
    private UserMapper userMapper;

    // 1. 无注解:使用默认数据源(master),用于写操作
    public void addUser(User user) {
        userMapper.insert(user);
    }

    // 2. 方法注解:指定slave1,用于读操作
    @DS("slave1")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    // 3. 方法注解:指定slave2(达梦库)
    @DS("slave2")
    public List<User> listUserByDm() {
        return userMapper.selectList(null);
    }
}
 

(2)@DS 注解支持的取值

表格
 
取值类型示例说明
具体数据源名称 @DS("slave1") 直接指定配置文件中的数据源名称
分组名称 @DS("slave") 需先配置数据源分组,自动负载均衡(见下文)
default @DS("default") 使用默认数据源(等同于不写注解)

3. API 手动切换数据源

适合复杂业务逻辑(如根据参数动态切换),需手动管理数据源上下文,核心类:DynamicDataSourceContextHolder
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void handleOrder(Long tenantId) {
        try {
            // 1. 根据租户ID动态切换数据源(假设租户1用slave1,租户2用slave2)
            String dsName = tenantId == 1 ? "slave1" : "slave2";
            DynamicDataSourceContextHolder.push(dsName);
            
            // 2. 执行业务逻辑(此时使用指定的数据源)
            queryOrderData();
        } finally {
            // 3. 必须移除上下文,避免污染后续请求
            DynamicDataSourceContextHolder.poll();
        }
    }

    private void queryOrderData() {
        // 数据库操作逻辑
    }
}
 

三、高级用法

1. 读写分离(数据源分组 + 负载均衡)

组件支持将多个从库分组,自动实现轮询负载均衡,只需在配置文件中给数据源命名加前缀,注解指定分组名即可。

(1)配置分组(修改 application.yml)

yaml
 
 
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master: # 主库(写)
          url: jdbc:mysql://127.0.0.1:3306/db_master
          # 省略其他配置...
        slave_1: # 从库1(读),前缀slave_
          url: jdbc:mysql://127.0.0.1:3306/db_slave1
        slave_2: # 从库2(读),前缀slave_
          url: jdbc:mysql://127.0.0.1:3306/db_slave2
 

(2)使用分组名切换

java
 
运行
 
 
 
 
@Service
public class UserService {

    // @DS("slave") 匹配所有slave_前缀的数据源,组件自动轮询slave_1/slave_2
    @DS("slave")
    public List<User> listAllUser() {
        return userMapper.selectList(null);
    }
}
 

2. 动态增删数据源(运行时配置)

支持在项目启动后新增 / 删除数据源(适合多租户、动态扩容场景),核心类:DynamicRoutingDataSource

(1)动态新增数据源示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;

@Component
public class DynamicDsManager {

    @Resource
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    @Resource
    private DataSourceCreator dataSourceCreator; // 组件自带的数据源创建器

    /**
     * 新增数据源
     * @param dsName 数据源名称(唯一)
     * @param url 数据库连接地址
     * @param username 用户名
     * @param password 密码
     * @param driverClassName 驱动类名
     */
    public void addDataSource(String dsName, String url, String username, String password, String driverClassName) {
        // 1. 构建数据源配置
        DataSourceProperty property = new DataSourceProperty();
        property.setUrl(url);
        property.setUsername(username);
        property.setPassword(password);
        property.setDriverClassName(driverClassName);
        
        // 2. 创建数据源(自动适配连接池)
        DataSource dataSource = dataSourceCreator.createDataSource(property);
        
        // 3. 添加到动态数据源管理器
        dynamicRoutingDataSource.addDataSource(dsName, dataSource);
    }

    /**
     * 删除数据源
     * @param dsName 数据源名称
     */
    public void removeDataSource(String dsName) {
        dynamicRoutingDataSource.removeDataSource(dsName);
    }
}
 

(2)使用新增的数据源

java
 
运行
 
 
 
 
@Service
public class TenantService {

    @Resource
    private DynamicDsManager dynamicDsManager;

    public void initTenantDs(Long tenantId) {
        // 1. 新增租户专属数据源
        String dsName = "tenant_" + tenantId;
        dynamicDsManager.addDataSource(
            dsName,
            "jdbc:mysql://127.0.0.1:3306/tenant_" + tenantId,
            "root",
            "123456",
            "com.mysql.cj.jdbc.Driver"
        );
        
        // 2. 切换到该数据源并初始化数据
        DynamicDataSourceContextHolder.push(dsName);
        initTenantData();
        DynamicDataSourceContextHolder.poll();
    }
}
 

3. 事务支持

动态数据源组件兼容 Spring 事务,需注意:一个事务内只能使用一个数据源(事务中切换数据源无效)。

(1)正常事务示例(单数据源)

java
 
运行
 
 
 
 
@Service
public class UserService {

    // 事务内使用master数据源(默认)
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user) {
        userMapper.updateById(user);
        // 即使手动切换数据源,事务内也不会生效
        DynamicDataSourceContextHolder.push("slave1");
        userMapper.selectById(user.getId()); // 仍使用master
    }
}
 

(2)多数据源事务(分布式事务)

如果需要跨数据源事务,需整合 Seata 等分布式事务框架,核心步骤:
  1. 引入 Seata 依赖;
  2. 配置 Seata 事务组;
  3. 在方法上添加 @GlobalTransactional 注解。

四、常见问题与避坑指南

1. 数据源切换不生效?

  • 检查 @DS 注解是否加在 Service 方法 上(加在 Controller/Mapper 上无效,因为 AOP 切面是 Service 层);
  • 检查方法是否被同类内部调用(AOP 失效),需通过 Bean 调用;
  • 检查 strict: true 时,是否拼写错数据源名称(严格模式会抛异常)。

2. 连接池配置不生效?

  • 组件默认使用 HikariCP,如需切换成 Druid,需引入 Druid 依赖并配置 spring.datasource.dynamic.type=com.alibaba.druid.pool.DruidDataSource
  • 全局连接池配置需写在 spring.datasource.dynamic.hikari 下,单个数据源的连接池配置优先级更高。

3. 信创数据库适配?

  • 确保驱动类名正确(如达梦:dm.jdbc.driver.DmDriver,人大金仓:com.kingbase8.Driver);
  • 连接 URL 按国产数据库规范配置;
  • 建议使用组件 3.6.0+ 版本,对国产数据库兼容性更好。

五、核心配置项速查

表格
 
配置项默认值说明
spring.datasource.dynamic.primary master 默认数据源名称
spring.datasource.dynamic.strict false 严格模式:找不到数据源时是否抛异常
spring.datasource.dynamic.datasource - 多数据源配置列表
spring.datasource.dynamic.type HikariCP 全局数据源类型(Druid/ HikariCP)
spring.datasource.dynamic.hikari - 全局连接池配置(HikariCP)

总结

  1. dynamic-datasource-spring-boot-starter 核心是通过 @DS 注解 / API 实现多数据源切换,无侵入、配置简单;
  2. 基础场景(读写分离)只需配置多数据源 + @DS 注解,高级场景(动态增删数据源)可通过 DynamicRoutingDataSource 实现;
  3. 避坑关键:注解加在 Service 方法上、事务内不切换数据源、信创库需适配驱动和 URL。
DynamicDataSourceContextHolder peek push poll 等方法使用详解

DynamicDataSourceContextHolder 核心方法(peek/push/poll)使用详解

DynamicDataSourceContextHolder 是 dynamic-datasource-spring-boot-starter 中手动控制数据源上下文的核心工具类,底层基于 ThreadLocal<Deque<String>> 实现(线程内的栈结构),push/peek/poll 等方法对应栈的「入栈 / 查看栈顶 / 出栈」操作,专门解决手动动态切换数据源的场景(如多租户、复杂业务逻辑动态选库)。

一、核心设计原理

先理解底层结构:
  • 每个线程有独立的 ThreadLocal 存储区,避免多线程数据源上下文污染;
  • 存储结构是双端队列(Deque)(栈),支持「嵌套切换数据源」(如方法 A 切换到 slave1,方法 A 调用的方法 B 再切换到 slave2,执行完 B 后回到 slave1);
  • 核心方法对应栈的基础操作:
    表格
     
    方法名栈操作核心作用
    push 入栈 将指定数据源名称放入栈顶(切换数据源)
    peek 查看栈顶 获取当前生效的数据源名称(不修改栈)
    poll 出栈 移除栈顶的数据源名称(恢复上一层数据源)
    clear 清空栈 清空当前线程的所有数据源上下文
     

二、核心方法逐个拆解(附示例)

1. push (String dsName) - 切换 / 设置数据源

作用

将指定的数据源名称(如 "slave1"、"tenant_001")放入线程上下文的栈顶,后续数据库操作会使用该数据源。

用法要点

  • 必须传入配置文件中已定义的数据源名称(严格模式下传错会抛异常,非严格模式用默认库);
  • 支持嵌套调用(多次 push),栈会逐层存储数据源名称。

基础示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void handleOrder(Long tenantId) {
        // 1. 初始状态:栈为空,使用默认数据源(master)
        System.out.println("初始数据源:" + DynamicDataSourceContextHolder.peek()); // null → 用master
        
        // 2. push:切换到租户专属数据源(tenant_001)
        String tenantDsName = "tenant_" + tenantId;
        DynamicDataSourceContextHolder.push(tenantDsName);
        System.out.println("push后数据源:" + DynamicDataSourceContextHolder.peek()); // tenant_001
        
        // 3. 执行业务逻辑(此时使用tenant_001数据源)
        queryOrderData();
        
        // 4. 嵌套push:临时切换到slave1(用于查基础数据)
        DynamicDataSourceContextHolder.push("slave1");
        System.out.println("嵌套push后数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
        queryBaseData();
        
        // 5. 必须出栈(poll),恢复到上一层数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("嵌套poll后数据源:" + DynamicDataSourceContextHolder.peek()); // tenant_001
        
        // 6. 最终出栈,恢复默认数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("最终poll后数据源:" + DynamicDataSourceContextHolder.peek()); // null → 用master
    }

    private void queryOrderData() { /* 租户库操作 */ }
    private void queryBaseData() { /* 从库1操作 */ }
}
 

2. peek () - 查看当前生效的数据源

作用

获取当前线程上下文栈顶的数据源名称(仅查看,不修改栈结构),返回值:
  • null:栈为空,使用默认数据源(配置的 primary 库,如 master);
  • 非 null:返回栈顶的数据源名称(当前生效的数据源)。

典型场景

  • 日志打印 / 监控:记录当前使用的数据源;
  • 校验:判断是否已切换到目标数据源;
  • 调试:排查数据源切换是否生效。

示例

java
 
运行
 
 
 
 
// 校验是否切换到了slave1
if (!"slave1".equals(DynamicDataSourceContextHolder.peek())) {
    throw new RuntimeException("数据源切换失败,当前生效的是:" + DynamicDataSourceContextHolder.peek());
}
// 执行业务逻辑
listUser();
 

3. poll () - 恢复上一层数据源

作用

移除栈顶的数据源名称(出栈),返回被移除的数据源名称;若栈为空,返回 null

用法要点

  • 必须和 push 成对使用(放在 finally 块中),否则会导致后续请求复用错误的数据源上下文;
  • 嵌套 push 时,poll 会逐层恢复(先进后出)。

规范写法(try-finally 确保 poll 执行)

java
 
运行
 
 
 
 
public void processData(String dsName) {
    try {
        // 切换数据源
        DynamicDataSourceContextHolder.push(dsName);
        // 核心业务逻辑
        doBusiness();
    } finally {
        // 无论是否异常,都要出栈,避免上下文污染
        DynamicDataSourceContextHolder.poll();
    }
}
 

4. clear () - 清空所有数据源上下文

作用

清空当前线程的整个数据源栈(所有 push 的数据源名称都被移除),直接恢复到默认数据源。

适用场景

  • 批量操作结束后快速重置上下文;
  • 异常场景下强制清空(避免栈堆积)。

示例

java
 
运行
 
 
 
 
public void batchProcess(List<String> dsNames) {
    try {
        for (String dsName : dsNames) {
            DynamicDataSourceContextHolder.push(dsName);
            processSingleData(dsName);
            DynamicDataSourceContextHolder.poll();
        }
    } catch (Exception e) {
        // 异常时清空所有上下文,避免残留
        DynamicDataSourceContextHolder.clear();
        throw new RuntimeException("批量处理失败", e);
    }
}
 

5. 扩展方法:getDataSourceLookupKey ()

作用

等价于 peek(),是更语义化的别名,返回当前生效的数据源名称(推荐新代码使用)。

示例

java
 
运行
 
 
 
 
String currentDs = DynamicDataSourceContextHolder.getDataSourceLookupKey();
System.out.println("当前使用的数据源:" + (currentDs == null ? "默认库(master)" : currentDs));
 

三、常见使用场景与最佳实践

场景 1:多租户按库隔离(最常用)

java
 
运行
 
 
 
 
@Service
public class TenantService {

    @Resource
    private TenantDsConfigMapper dsConfigMapper;

    public void handleTenantRequest(Long tenantId) {
        // 1. 根据租户ID查询数据源配置
        String dsName = dsConfigMapper.getDsNameByTenantId(tenantId);
        if (dsName == null) {
            throw new RuntimeException("租户" + tenantId + "无专属数据源");
        }
        
        // 2. 安全切换数据源(try-finally 必加)
        try {
            DynamicDataSourceContextHolder.push(dsName);
            // 3. 执行租户专属操作(查询/修改租户数据)
            queryTenantData(tenantId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    private void queryTenantData(Long tenantId) { /* 租户库操作 */ }
}
 

场景 2:嵌套切换数据源(方法调用链)

java
 
运行
 
 
 
 
@Service
public class NestedDsService {

    // 外层方法:切换到slave1
    public void outerMethod() {
        try {
            DynamicDataSourceContextHolder.push("slave1");
            System.out.println("外层数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
            innerMethod(); // 调用内层方法
            System.out.println("内层执行后数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    // 内层方法:临时切换到slave2
    private void innerMethod() {
        try {
            DynamicDataSourceContextHolder.push("slave2");
            System.out.println("内层数据源:" + DynamicDataSourceContextHolder.peek()); // slave2
            // 内层业务逻辑
        } finally {
            DynamicDataSourceContextHolder.poll(); // 移除slave2,回到slave1
        }
    }
}
 

最佳实践总结

  1. 必须成对使用 push/poll:所有 push 操作都要放在 try 块,poll 放在 finally 块,避免异常导致上下文残留;
  2. 避免滥用 clear ():clear 会清空整个栈,嵌套场景下慎用(优先用 poll 逐层恢复);
  3. 日志打印 peek ():关键节点打印当前数据源名称,方便排查切换异常;
  4. 严格校验数据源名称:push 前校验数据源是否存在(可通过 DynamicRoutingDataSource 获取所有已配置数据源);
  5. 事务内不切换数据源:Spring 事务内切换数据源无效(事务绑定初始数据源),需在事务外切换。

四、常见问题与避坑

问题 1:数据源切换后不生效?

  • 原因:push 后未执行数据库操作,或被 Spring 事务覆盖(事务内切换无效);
  • 解决:在事务注解(@Transactional)外执行 push 操作。

问题 2:多线程场景下数据源串库?

  • 原因:ThreadLocal 是线程私有,若使用线程池,需确保任务执行完后 clear ();
  • 解决:线程池任务结束前调用 DynamicDataSourceContextHolder.clear()

问题 3:嵌套 push 后忘记对应 poll?

  • 现象:后续请求使用错误的数据源;
  • 解决:嵌套 push 几次,就必须 poll 几次,或最终用 clear () 兜底。

总结

  1. push 是「切换数据源」,poll 是「恢复数据源」,二者必须成对出现在 try-finally 中,避免上下文污染;
  2. peek 仅用于「查看当前数据源」,不修改栈结构,适合日志 / 校验场景;
  3. 嵌套切换数据源时,栈结构会自动维护层级,poll 会逐层恢复,无需手动记录层级。

 

-----------------------------------------------------------------------------------------------------------------

 

你提到的 "seal springboot" 我理解为你想了解 如何在 Spring Boot 项目中实现接口防重复提交(Seal / 防重),这是企业级开发中非常常见的需求(比如防止用户重复点击下单、提交表单等)。

 
下面我会提供一个基于 Redis + 自定义注解 + AOP 的通用、可落地的 Spring Boot 防重复提交解决方案。
 

 

一、实现思路

 
  1. 自定义注解:标记需要防重的接口方法
  2. AOP 切面:拦截带注解的方法,统一处理防重逻辑
  3. Redis 存储:利用 Redis 的原子操作(SETNX)记录请求标识,设置过期时间
  4. 请求标识生成:基于用户 ID + 接口路径 + 请求参数(或业务唯一标识)生成唯一 KEY
 

二、完整代码实现

 

1. 第一步:引入依赖(pom.xml)

 
确保项目中引入 Spring Boot Web、Redis、AOP 相关依赖:
 
xml
 
 
<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Spring Boot AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- 可选:简化JSON处理 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.45</version>
</dependency>
 
 

2. 第二步:自定义防重注解(RepeatSubmit.java)

 
java
 
运行
 
 
 
 
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交注解
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface RepeatSubmit {
    /**
     * 防重过期时间(默认5秒)
     */
    long expireTime() default 5;

    /**
     * 时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 提示信息
     */
    String message() default "请勿重复提交,请稍后再试!";
}
 
 

3. 第三步:AOP 切面实现(RepeatSubmitAspect.java)

 
java
 
运行
 
 
 
 
import com.alibaba.fastjson2.JSON;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交切面
 */
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 切点:拦截所有带@RepeatSubmit注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")
    public void repeatSubmitPointcut() {}

    @Around("repeatSubmitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取当前请求和注解信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);

        // 2. 生成唯一请求标识(核心:确保同一用户+同一请求的唯一性)
        String userId = getCurrentUserId(request); // 替换为你的真实用户ID获取逻辑
        String requestURI = request.getRequestURI();
        String params = JSON.toJSONString(joinPoint.getArgs()); // 请求参数
        String key = "repeat_submit:" + md5(userId + ":" + requestURI + ":" + params);

        // 3. Redis原子操作:SETNX(不存在则设置,存在则返回false)
        boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", annotation.expireTime(), annotation.timeUnit());

        if (!success) {
            // 重复提交,抛出异常(可自定义业务异常)
            throw new RuntimeException(annotation.message());
        }

        // 4. 执行原方法
        try {
            return joinPoint.proceed();
        } finally {
            // 可选:如果是幂等接口,可在方法执行完成后主动删除KEY(避免过期时间内无法重复提交合法请求)
            // stringRedisTemplate.delete(key);
        }
    }

    /**
     * 获取当前登录用户ID(示例:实际项目中替换为你的鉴权逻辑,如从Token/Session中获取)
     */
    private String getCurrentUserId(HttpServletRequest request) {
        // 示例:从Header获取token解析用户ID,或从Session获取
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            return "anonymous"; // 未登录用户用匿名标识
        }
        // 此处替换为你的真实解析逻辑
        return "user123";
    }

    /**
     * MD5加密(生成短且唯一的KEY)
     */
    private String md5(String content) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("MD5加密失败", e);
        }
    }
}
 
 

4. 第四步:配置 Redis(application.yml)

 
yaml
 
 
spring:
  redis:
    host: localhost # 你的Redis地址
    port: 6379
    password: # 无密码则留空
    database: 0
 
 

5. 第五步:使用示例(Controller)

 
java
 
运行
 
 
 
 
import com.example.demo.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    /**
     * 下单接口:5秒内禁止重复提交
     */
    @PostMapping("/order/submit")
    @RepeatSubmit(expireTime = 5, message = "下单请求已提交,请勿重复操作!")
    public String submitOrder(@RequestBody OrderRequest request) {
        // 执行业务逻辑(创建订单)
        return "下单成功,订单号:" + System.currentTimeMillis();
    }

    // 测试用的请求实体
    public static class OrderRequest {
        private Long productId;
        private Integer count;

        // getter/setter 省略
    }
}
 
 

三、关键说明

 
  1. 用户 ID 获取:getCurrentUserId 方法需要替换为你项目的真实鉴权逻辑(如从 JWT Token、Session 中解析),否则匿名用户的防重会失效。
  2. 过期时间设置:根据业务场景调整(比如支付接口可设 30 秒,普通表单设 5 秒)。
  3. 幂等性考虑:如果接口是幂等的(重复执行不影响结果),可在方法执行完成后主动删除 Redis KEY,避免用户正常重试被拦截。
  4. 异常处理:建议自定义业务异常(如 RepeatSubmitException),并通过全局异常处理器返回友好提示。
 

四、测试验证

 
  1. 启动 Redis 和 Spring Boot 项目
  2. 快速重复调用 /order/submit 接口(比如用 PostMan 连续点击发送)
  3. 第一次调用成功,5 秒内的重复调用会返回:{"msg":"下单请求已提交,请勿重复操作!","code":500}
 

 

总结

 
  1. 核心原理:通过 Redis 的 SETNX 原子操作实现请求唯一性校验,结合 AOP 统一拦截,无侵入式实现防重。
  2. 关键要素:唯一 KEY 的生成(用户 ID + 接口路径 + 请求参数)是防重的核心,确保仅拦截同一用户的重复请求。
  3. 灵活扩展:可根据业务需求调整过期时间、KEY 生成规则(比如仅用业务唯一标识,如订单号),或扩展支持分布式场景(Redis 集群)。

-----------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------

posted @ 2026-03-10 10:10  hanease  阅读(12)  评论(0)    收藏  举报