在 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,启动即用,适合企业级开发 🚀
689

被折叠的 条评论
为什么被折叠?



