spring boot 全局异常处理无法直接捕获“重复消费”这一业务概念,因为重复消费通常发生在消息队列(如 kafka、rocketmq)的投递阶段或业务逻辑层面,而非由框架抛出的标准 java 异常。要解决此问题,需结合消息中间件的幂等性机制与 spring 的全局异常处理器 @restcontrolleradvice。具体做法是:在业务层通过唯一键(如消息 id 或订单号)检查是否已处理过,若发现重复则抛出自定义异常(如 duplicateconsumptionexception),并在 @restcontrolleradvice 中统一捕获该异常,返回特定的 http 状态码(如 200 ok 配合特定业务码,或 409 conflict)以告知调用方或 mq 消费者忽略重试。

如何定义并抛出重复消费异常
首先需要明确,重复消费不是系统底层自动抛出的异常,而是开发者在检测到重复后主动抛出的信号。在 Service 层,通过查询数据库或缓存确认当前请求是否为重复处理后,实例化一个继承自 RuntimeException 的自定义异常类 DuplicateConsumptionException 并抛出。这样做的目的是将业务逻辑中的“幂等性拦截”转化为可被全局处理器捕获的信号,从而避免向客户端返回堆栈跟踪信息,同时允许 MQ 消费者根据返回结果决定是丢弃消息还是重新入队。

package com.example.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateConsumptionException extends RuntimeException {
public DuplicateConsumptionException(String message) {
super(message);
}
}
上述代码定义了一个名为 DuplicateConsumptionException 的异常类,它继承了 RuntimeException 并使用了 @ResponseStatus(HttpStatus.CONFLICT) 注解。当在业务代码中抛出此异常时,Spring MVC 会自动将其映射为 HTTP 409 Conflict 状态码。在实际业务中,你可以在 Service 方法中加入如下逻辑:if (isDuplicate(orderId)) { throw new DuplicateConsumptionException("Order already processed"); }。这种设计确保了异常能够被后续的全局处理器识别,并且前端或 MQ 消费者可以通过 HTTP 状态码快速判断处理结果。
如何在 ControllerAdvice 中统一捕获并响应
定义好异常后,下一步是在全局异常处理器中捕获它。创建一个配置类,使用 @RestControllerAdvice 注解,并编写一个处理方法,参数类型为刚才定义的 DuplicateConsumptionException。在这个方法中,你可以选择返回标准的 JSON 响应体,或者根据 MQ 消费者的要求返回特定的结构。对于 Web 端,通常返回包含错误码和消息的 JSON;对于 MQ 场景,如果使用的是 HTTP 回调或 REST 接口触发消费,返回 200 OK 但业务码表示“已存在”可能更合适,以避免 MQ 无限重试。

package com.example.config;
import com.example.exception.DuplicateConsumptionException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateConsumptionException.class)
public ResponseEntity<Map<String, Object>> handleDuplicateConsumption(DuplicateConsumptionException ex) {
Map<String, Object> body = new HashMap<>();
body.put("code", 409);
body.put("message", "Duplicate consumption detected: " + ex.getMessage());
body.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(409).body(body);
}
}
这段代码展示了如何使用 @RestControllerAdvice 和 @ExceptionHandler 来捕获 DuplicateConsumptionException。当业务层抛出该异常时,Spring 会执行此方法,构建一个包含错误码 409、详细消息和时间戳的 Map 对象,并以 HTTP 409 状态码返回。这种方式不仅统一了异常响应格式,还明确了“重复消费”的业务含义。需要注意的是,如果这个接口是被 MQ 消费者调用的,确保 MQ 客户端能正确解析 409 状态码并将其视为成功(不重试)或失败(重试),这取决于具体的 MQ 集成方式。例如,在 Spring Cloud Stream 中,可能需要配置 error-handling 策略来处理非 2xx 响应。
基于 orderId 判重条件的具体实现验证
为了验证上述机制的有效性,我们假设输入是一个包含 orderId 的消息对象。在 Service 层,我们首先检查 Redis 或数据库中是否存在该 orderId 的处理记录。如果存在,说明发生了重复消费,此时抛出 DuplicateConsumptionException。如果不存在,则执行业务逻辑并记录该 orderId。通过单元测试模拟两次相同 orderId 的请求,第一次请求正常处理并返回 200,第二次请求因命中判重逻辑而抛出异常,最终由全局处理器返回 409 状态码及对应的 JSON 响应。这一过程证明了从业务判重到异常捕获再到统一响应的完整链路是畅通的,能够有效防止重复消费带来的数据不一致问题。


















