在网关层中,统一异常响应处理是保障 API 风格一致

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

Spring Cloud Gateway 网关层中,统一异常响应处理是保障 API 风格一致、前端友好、便于监控的关键环节。

由于网关是所有请求的入口,当发生:

  • JWT 无效
  • 路由找不到服务
  • 请求超时
  • 限流被触发
  • 后端服务返回 500

……这些异常不应直接透传为原始错误页面或堆栈信息,而应返回结构化、标准化的 JSON 响应体,例如:

{
  "code": 401,
  "message": "认证失败:Token 已过期",
  "timestamp": "2025-04-05T10:30:00Z",
  "path": "/order/123"
}

✅ 推荐方案:使用 GlobalExceptionHandler + @ControllerAdvice 实现统一异常处理

⚠️ 注意:
Spring Cloud Gateway 是基于 WebFlux(响应式) 的非阻塞架构,不能使用传统的 @ControllerAdvice
我们必须使用 @ControllerAdvice 的 WebFlux 版本:@ControllerAdvice + @ResponseBody + ResponseEntityExceptionHandler 的替代品 —— ErrorWebExceptionHandler


📁 推荐目录结构(新增)

urbane-commerce-gateway/
├── src/
│   └── main/
│       └── java/
│           └── io/urbane/gateway/
│               ├── exception/
│               │   ├── GlobalExceptionHandler.java     # 👉 核心:统一异常处理器
│               │   ├── GatewayException.java           # 自定义网关异常类
│               │   └── ErrorResponse.java              # 统一响应结构体
│               └── ...

✅ 1. 统一响应结构体:ErrorResponse.java

package io.urbane.gateway.exception;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 统一的 API 响应体结构(用于所有异常场景)
 * 所有错误都返回此格式,前端可统一处理
 *
 * @author urbane-team
 */
@Data
public class ErrorResponse {

    private int code;           // HTTP 状态码,如 401、500
    private String message;     // 错误描述信息(用户可读)
    private String path;        // 请求路径,便于定位问题
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
    private LocalDateTime timestamp; // 时间戳(ISO 8601 格式)

    // 构造函数:根据状态码和消息构建
    public ErrorResponse(int code, String message, String path) {
        this.code = code;
        this.message = message;
        this.path = path;
        this.timestamp = LocalDateTime.now();
    }

    // 静态工厂方法,提高可读性
    public static ErrorResponse of(int code, String message, String path) {
        return new ErrorResponse(code, message, path);
    }
}

✅ 使用了 Lombok@Data 注解(需在 pom.xml 中引入 lombok 依赖)
✅ 时间格式使用 ISO 8601 标准,兼容前后端


✅ 2. 自定义网关异常类:GatewayException.java

package io.urbane.gateway.exception;

/**
 * 自定义网关异常基类
 * 所有网关内部异常都继承此类,便于在 GlobalExceptionHandler 中统一捕获
 *
 * @author urbane-team
 */
public class GatewayException extends RuntimeException {

    private final int statusCode;

    public GatewayException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }

    public int getStatusCode() {
        return statusCode;
    }
}

💡 使用示例(在过滤器中抛出):

throw new GatewayException(401, "认证失败:Token 无效");

✅ 3. 核心:全局异常处理器 —— GlobalExceptionHandler.java

✅ 这是最关键的部分 —— 使用 @Component + implements ErrorWebExceptionHandler

package io.urbane.gateway.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * 全局异常处理器(Spring Cloud Gateway 专用)
 * 功能:
 *   - 捕获所有未处理的异常(包括 404、500、JWT 失效、服务不可用等)
 *   - 返回统一格式的 JSON 错误响应
 *   - 避免向客户端暴露堆栈信息、敏感错误细节
 *
 * 注意:必须实现 ErrorWebExceptionHandler 接口,并标注 @Component
 *       且需要实现 Ordered 接口控制执行顺序(优先级越高越先执行)
 *
 * @author urbane-team
 */
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler, Ordered {

    private final ObjectMapper objectMapper; // 用于序列化 ErrorResponse

    /**
     * 构造函数注入 ObjectMapper(由 Spring 自动注入)
     */
    public GlobalExceptionHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 核心方法:处理异常并返回响应
     *
     * @param exchange 当前 HTTP 请求上下文
     * @param ex       发生的异常
     * @return Mono<Void> 异步响应流
     */
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 获取当前请求路径,用于响应中记录
        String path = exchange.getRequest().getURI().getPath();

        // 设置响应头:JSON 格式
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        // 构建统一错误响应对象
        ErrorResponse errorResponse;

        // ✅ 分类处理不同类型的异常
        if (ex instanceof GatewayException) {
            // 自定义网关异常(如 JWT 校验失败)
            GatewayException gatewayEx = (GatewayException) ex;
            errorResponse = ErrorResponse.of(gatewayEx.getStatusCode(), gatewayEx.getMessage(), path);

        } else if (ex instanceof NotFoundException) {
            // Spring Cloud Gateway 默认 404 异常(路由未匹配)
            errorResponse = ErrorResponse.of(404, "请求路径不存在,请检查接口地址", path);

        } else if (ex instanceof org.springframework.cloud.gateway.support.TimeoutException) {
            // 请求超时
            errorResponse = ErrorResponse.of(504, "服务请求超时,请稍后再试", path);

        } else if (ex instanceof org.springframework.web.server.ResponseStatusException) {
            // 由业务主动抛出的 HttpStatus 异常(如 @ResponseStatus)
            org.springframework.web.server.ResponseStatusException rse = (org.springframework.web.server.ResponseStatusException) ex;
            errorResponse = ErrorResponse.of(rse.getStatus().value(), rse.getMessage(), path);

        } else {
            // 未知异常(如后端服务崩溃、网络故障)
            // 生产环境建议记录日志,并返回通用错误
            errorResponse = ErrorResponse.of(500, "服务器内部错误,请联系管理员", path);
        }

        // 将 ErrorResponse 序列化为 JSON 字符串
        String jsonResponse;
        try {
            jsonResponse = objectMapper.writeValueAsString(errorResponse);
        } catch (Exception e) {
            // 序列化失败,返回最基础的错误
            jsonResponse = "{\"code\":500,\"message\":\"系统内部错误\",\"path\":\"" + path + "\",\"timestamp\":\"" + errorResponse.getTimestamp() + "\"}";
        }

        // 将 JSON 写入响应体
        byte[] bytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }

    /**
     * 设置执行优先级,数值越小,优先级越高
     * 保证此处理器在其他处理器之前执行(比如默认的 ErrorWebExceptionHandler)
     *
     * Spring Boot 默认的 ErrorWebExceptionHandler 优先级是 -1
     * 我们设为 -2,确保我们最先处理
     */
    @Override
    public int getOrder() {
        return -2; // 高于默认值 -1,确保优先捕获
    }
}

✅ 4. 在 application.yml 中开启调试(可选)

为了方便测试异常处理是否生效,你可以临时关闭日志过滤:

logging:
  level:
    io.urbane.gateway: DEBUG
    org.springframework.cloud.gateway: TRACE

✅ 5. 测试用例演示(模拟各种异常)

请求触发异常返回响应
GET /auth/login(无 Token)JwtAuthenticationFilter 抛出 GatewayException(401, "Token 无效"){ "code": 401, "message": "Token 无效", "path": "/auth/login", "timestamp": "..." }
GET /product/99999999(服务不存在)→ 路由找不到 → NotFoundException{ "code": 404, "message": "请求路径不存在...", "path": "/product/99999999" }
GET /order/123(后端服务宕机)→ 网关调用失败 → TimeoutException{ "code": 504, "message": "服务请求超时...", "path": "/order/123" }
GET /unknown-path(完全不存在的路径)→ 无匹配路由 → NotFoundException同上
GET /order/123(后端返回 500)→ 后端服务报错,网关收到 500{ "code": 500, "message": "服务器内部错误...", "path": "/order/123" }

✅ 所有情况均返回结构化 JSON,无 HTML、无堆栈、无敏感信息


✅ 6. 为什么这样设计是工业级标准?

特性说明
统一格式前端只需一个 axios.interceptors.response 统一处理,无需每个接口判断
安全合规不泄露 JVM 堆栈、数据库错误、文件路径等敏感信息
可扩展性强新增异常类型只需在 handle() 方法中加 else if 判断
高性能使用 DataBuffer 直接写入响应,避免中间转换
符合 RFC7807类似 Problem Details for HTTP APIs 标准(RFC 7807),专业规范
与微服务风格一致所有服务(包括网关)返回相同格式,降低集成成本

🚀 Bonus:生产环境增强建议

增强项实现方式
记录异常日志handle() 开头添加 log.error("网关异常", ex),便于 ELK/Sentry 监控
区分环境如果是 dev 环境,可返回更详细的错误;prod 环境只返回“服务器错误”
集成 Prometheus记录每种错误码的计数,用于告警
集成 Sentry将异常上报至 Sentry,实时通知开发人员

示例:带日志记录的增强版

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
    String path = exchange.getRequest().getURI().getPath();
    
    // ✅ 生产环境:记录异常日志(但不暴露给用户)
    if (ex instanceof Exception) {
        log.error("【网关异常】路径: {}, 异常类型: {}, 消息: {}", path, ex.getClass().getSimpleName(), ex.getMessage(), ex);
    }

    // ...后续逻辑不变...
}

✅ 总结:你现在的网关异常处理体系

组件作用
ErrorResponse.java定义统一响应结构
GatewayException.java自定义异常基类,便于分类
GlobalExceptionHandler.java核心:捕获所有异常,返回标准化 JSON
application.yml配置日志级别,辅助调试

最终效果
无论前端调用哪个接口,哪怕网关崩了,返回的永远是:

{
  "code": 401,
  "message": "认证失败:Token 已过期",
  "timestamp": "2025-04-05T10:30:00Z",
  "path": "/order/123"
}

前端开发者再也不用猜“这个错误是什么意思”了。


📦 下一步:完整项目模板

如果你希望我为你打包成一个 完整的 GitHub 可运行项目,包含:

  • application.yml
  • GlobalExceptionHandler.java
  • ErrorResponse.java
  • GatewayException.java
  • JwtAuthenticationFilter.java
  • pom.xml(含 Lombok、WebFlux 依赖)
  • Dockerfile
  • ✅ 测试用例(Postman Collection)

👉 请回复:

“请给我完整可运行项目模板!”

我会立刻发送你一个 ZIP 或 Git 仓库链接,一键导入 IDEA,启动即用,适合企业级开发 🚀

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值