简介:本资源是一份面向Java微服务开发者与Spring Cloud初学者的Spring Boot整合Gateway实战项目,聚焦API网关核心功能实现,解决微服务架构中统一入口、路由分发、权限校验与高可用治理等典型问题。压缩包共196个文件,含170个XML配置文件(用于依赖管理与Spring Bean定义)、6个Java源码(含启动类、控制器及测试类)、4个YML配置文件(定义路由规则、服务发现与全局参数)以及配套的Git忽略规则与说明文档,整体仅77KB,轻量易导入。已有6035人学习下载,资源结构清晰,包含完整客户端与服务端双应用模块(GatewayClientApplication/GatewayServiceApplication)、跨域配置类、单元测试用例及基础过滤器示例,覆盖动态路由、断言匹配、全局/局部过滤器、服务注册集成等关键实践点,可直接运行调试并快速理解网关在真实微服务链路中的定位与作用。
1. SpringBoot整合Gateway实现网关功能:为什么你写的路由5分钟就返回502,而生产环境能扛住万级并发?
你刚在本地跑通SpringBoot + Gateway,加了两条- id: user-service路由,前端一调就报502 Bad Gateway;换台机器又变成Connection refused;上线后某天凌晨三点告警:Upstream connect error or disconnect/reset before headers——这根本不是“网关没配好”,而是你漏掉了路由生命周期、负载均衡器初始化时机、响应体缓冲区大小这三个黑匣子。SpringBoot整合Gateway不是把starter加进pom就完事,它本质是用Reactor Netty构建的异步非阻塞反向代理,所有HTTP/1.1分块传输、超时重试、SSL透传、跨域预检都得在WebFlux语义下重写逻辑。本文面向已能写Controller但第一次搭网关的后端工程师:不讲Reactor原理,只告诉你哪三行配置决定服务是否存活、哪个参数改错会让Vue打包静态资源404、为什么SpringBoot 3.x必须用Gateway 4.x且不能降级。你会亲手从零搭出带熔断、限流、JWT透传的网关,并精准定位Bad Gateway Error EOF这类玄学错误的根因。
2. 用SpringBoot 3.2 + Gateway 4.1跑通最小可运行网关:从依赖到第一个路由
2.1 选型铁律:SpringBoot版本与Gateway版本必须严格对齐
SpringBoot 3.x(基于Java 17+)彻底移除了Servlet API,Gateway 4.x才完全适配WebFlux Reactive Stack。若强行用Gateway 3.x(如3.1.5),会出现NoClassDefFoundError: org/springframework/web/server/adapter/HttpWebHandlerAdapter——这不是jar包冲突,而是Spring Framework 6的WebHandler接口和旧版不兼容。当前最稳组合是:
| SpringBoot 版本 | Gateway 版本 | 关键变更 |
|---|---|---|
| 3.2.0+ | 4.1.0+ | 支持spring.cloud.gateway.httpclient.pool.max-idle-time=30000精细化连接池控制 |
| 3.1.x | 4.0.x | GlobalFilter中exchange.getResponse().setStatusCode()需配合Mono<Void>返回 |
提示:不要用
spring-cloud-starter-gateway的BOM管理版本!它会锁死旧版。直接在pom.xml中声明精确版本:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>4.1.3</version> <!-- 2024年Q2最新稳定版 --> </dependency>2.2 最小化application.yml:去掉所有注释,只留启动必需项
# application.yml spring: application: name: api-gateway cloud: gateway: routes: - id: user-service uri: http://localhost:8081 # 后端真实服务地址 predicates: - Path=/api/users/** # 路由匹配路径 filters: - StripPrefix=2 # 去掉/api前缀再转发 default-filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin httpclient: pool: max-idle-time: 30000 # 连接空闲30秒回收,防TIME_WAIT堆积 max-life-time: 60000 # 连接最大存活60秒 web: server: port: 8080 # 网关监听端口 # 必须显式关闭SpringMVC,否则WebMvcConfigurer干扰WebFlux server: servlet: context-path: "" # 网关不设context-path,避免路径二次拼接2.3 验证路由是否生效:curl命令比Postman更可靠
# 测试路由匹配(不走后端,看网关是否识别) curl -v http://localhost:8080/api/users/list # 正常应返回:HTTP/1.1 503 Service Unavailable(因后端8081未启动) # 若返回404,说明Path谓词未匹配——检查yml缩进是否为2空格(YAML对缩进敏感) # 启动一个mock后端验证全链路 python3 -m http.server 8081 --directory ./mock-data # 在./mock-data下放users.json curl http://localhost:8080/api/users/list # 应返回mock-data/users.json内容参数说明:
StripPrefix=2:/api/users/list→ 转发时变为/list,数字2表示跳过/api和/users两级路径max-idle-time:当后端服务重启时,网关保持的空闲连接若超过此时间会被强制关闭,避免Connection reset by peerDedupeResponseHeader:解决CORS预检请求中重复设置Access-Control-Allow-Origin导致浏览器拒绝响应
3. 实现JWT透传与动态路由:让网关成为认证中心而非摆设
3.1 JWT解析不走Filter链:用GlobalFilter在请求头注入用户ID
网关不该校验JWT(性能瓶颈),但必须解析并透传关键字段。以下代码在GlobalFilter中提取Authorization: Bearer xxx,解码后将userId注入请求头,下游服务直接读取:
@Component public class JwtAuthGlobalFilter implements GlobalFilter, Ordered { private static final String AUTH_HEADER = "Authorization"; private static final String USER_ID_HEADER = "X-User-Id"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String authHeader = exchange.getRequest().getHeaders().getFirst(AUTH_HEADER); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); try { // 使用jjwt-api 0.12.5(适配Java 17) Jws<Claims> claimsJws = Jwts.parser() .verifyWith(KeyGenerator.generateKey()) // 生产用RSA公钥 .build() .parseSignedClaims(token); String userId = claimsJws.getPayload().get("userId", String.class); // 注入新请求头,下游服务通过request.getHeader("X-User-Id")获取 ServerHttpRequest request = exchange.getRequest() .mutate() .header(USER_ID_HEADER, userId) .build(); return chain.filter(exchange.mutate().request(request).build()); } catch (Exception e) { // Token无效时,不中断流程,下游服务自行处理401 return chain.filter(exchange); } } return chain.filter(exchange); } @Override public int getOrder() { return -1; // 优先级最高,在其他Filter之前执行 } }关键点:
mutate().header()创建新ServerHttpRequest,原请求头不可变getOrder() = -1确保在NettyRoutingFilter之前执行,否则下游收不到头- 不抛异常!JWT校验失败时静默透传,由业务服务统一返回
401 Unauthorized
3.2 动态路由:从数据库加载路由配置,支持热更新
硬编码路由无法应对灰度发布。我们用RouteDefinitionLocator从MySQL加载路由,表结构如下:
| id | route_id | uri | predicates | filters | order_num | status |
|---|---|---|---|---|---|---|
| 1 | user-v1 | http://192.168.1.10:8081 | Path=/api/users/** | StripPrefix=2 | 100 | 1 |
@Component public class DatabaseRouteDefinitionLocator implements RouteDefinitionLocator { @Autowired private JdbcTemplate jdbcTemplate; @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable( jdbcTemplate.query("SELECT * FROM gateway_route WHERE status=1", (rs, rowNum) -> { RouteDefinition route = new RouteDefinition(); route.setId(rs.getString("route_id")); route.setUri(URI.create(rs.getString("uri"))); // 解析predicates字符串:["Path=/api/users/**"] List<PredicateDefinition> preds = parsePredicates(rs.getString("predicates")); route.setPredicates(preds); route.setOrder(rs.getInt("order_num")); return route; }) ); } private List<PredicateDefinition> parsePredicates(String json) { // 实际项目用Jackson反序列化,此处简化为字符串分割 return Arrays.stream(json.replaceAll("[\\[\\]\"]", "").split(",")) .map(s -> { PredicateDefinition p = new PredicateDefinition(); p.setName("Path"); p.setArgs(Collections.singletonMap("pattern", s.trim())); return p; }) .collect(Collectors.toList()); } }注意:
DatabaseRouteDefinitionLocator需配合@RefreshScope或定时任务刷新,否则修改数据库后需重启网关。生产推荐用Nacos配置中心替代数据库。
4. Gateway集群部署与高可用:避开502 Bad Gateway的三大生死线
4.1 Nginx作为Gateway前置:为什么不能直接暴露Gateway端口
Gateway本身是Reactor Netty,单实例可抗万级并发,但它不处理SSL卸载、DDoS防护、静态资源缓存。若直接将8080端口暴露公网,会出现:
- SSL握手耗尽CPU(Gateway每连接都要做TLS协商)
- 大文件下载触发
Bad Gateway Error EOF(Netty缓冲区溢出) - 恶意IP扫描导致连接数打满
正确架构:Internet → Nginx(SSL卸载+限流)→ Gateway集群(负载均衡)→ 微服务
Nginx配置关键段:
upstream gateway_cluster { server 192.168.1.101:8080 max_fails=3 fail_timeout=30s; server 192.168.1.102:8080 max_fails=3 fail_timeout=30s; keepalive 32; # 保持长连接,减少TCP握手 } server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 关键:增大缓冲区防EOF proxy_buffering on; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; location / { proxy_pass http://gateway_cluster; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }参数说明:
proxy_buffer_size 128k:单个响应头缓冲区,防止大Cookie导致502proxy_buffers 4 256k:4个256KB缓冲区,总容量1MB,应对大JSON响应keepalive 32:每个worker进程保持32个空闲连接到Gateway,降低连接建立开销
4.2 Gateway集群Session一致性:用Redis共享路由元数据
当路由配置存在数据库时,多实例Gateway需同步路由变更。若A实例更新路由,B实例仍用旧配置,会导致404。解决方案:用Redis Pub/Sub广播变更事件。
@Component public class RouteRefreshPublisher { @Autowired private RedisTemplate<String, Object> redisTemplate; public void publishRouteChange(String routeId) { redisTemplate.convertAndSend("route:change", routeId); // 发布事件 } } @Component public class RouteRefreshSubscriber implements ApplicationRunner { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public void run(ApplicationArguments args) { redisTemplate.listen(new MessageListener() { @Override public void onMessage(Message message, byte[] pattern) { String routeId = new String(message.getBody()); // 触发路由刷新(实际调用RouteDefinitionLocator重新加载) System.out.println("Route " + routeId + " changed, reloading..."); } }, "route:change"); } }注意:Redis监听需在
@PostConstruct之后执行,否则redisTemplate未初始化。此处用ApplicationRunner确保上下文就绪。
5. 排查502 Bad Gateway与EOF错误:生产环境血泪经验总结
5.1 现象:前端调用返回502,Gateway日志无ERROR,只有WARN
2024-06-15 10:23:41.221 WARN [api-gateway,,] 12345 --- [or-http-epoll-4] o.s.c.g.f.WeightCalculatorWebFilter : Weight was not resolved for: user-service原因:WeightCalculatorWebFilter警告表示路由权重未配置,但真正导致502的是下游服务未启动或网络不通。该WARN被误认为根因,实则掩盖了Connection refused。
解决:
- 先用
telnet 192.168.1.10 8081确认网络连通性 - 查看
/actuator/gateway/routes端点,确认路由状态为UP - 在Gateway中启用DEBUG日志:
logging.level.org.springframework.cloud.gateway=DEBUG,搜索NettyRoutingFilter日志
5.2 现象:大文件上传(>10MB)返回Bad Gateway Error EOF
原因:Netty默认maxInitialLineLength=4096,当后端返回超长响应头(如含大量Set-Cookie)时,Netty直接断开连接,Nginx收到不完整响应即报EOF。
解决:在application.yml中扩大Netty参数:
spring: cloud: gateway: httpclient: max-initial-line-length: 8192 # 默认4096,改为8192 max-header-size: 65536 # 默认8192,改为64KB max-chunk-size: 262144 # 默认256KB,大文件需增大5.3 现象:Vue打包静态资源访问404,但API路由正常
原因:Vue Router使用history模式,前端路由/user/profile需由网关转发到index.html,但StripPrefix配置错误导致路径丢失。
解决:添加专用静态资源路由,必须放在所有API路由之前(顺序决定匹配优先级):
spring: cloud: gateway: routes: # 1. 静态资源路由(最高优先级) - id: vue-static uri: file:./dist/ # Vue打包输出目录 predicates: - Path=/** filters: - SetStatus=200 - RedirectTo=302, /index.html # 所有未匹配路径重定向到index.html # 2. API路由(低优先级) - id: user-service uri: http://localhost:8081 predicates: - Path=/api/**注意:
file:协议需在Gateway启动时指定JVM参数-Dspring.cloud.gateway.httpclient.file.enabled=true,否则报Unsupported protocol: file
5.4 现象:SpringBoot 3.2.3 + Gateway 4.1.2启动报NoSuchMethodError: reactor.netty.http.client.HttpClient.followRedirect
原因:reactor-netty-http版本冲突。Gateway 4.1.2要求reactor-netty-http 1.2.10,但SpringBoot 3.2.3默认带1.2.8。
解决:强制升级Netty版本:
<properties> <reactor-netty-http.version>1.2.10</reactor-netty-http.version> </properties> <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty-http</artifactId> <version>${reactor-netty-http.version}</version> </dependency>6. 熔断与限流实战:用Resilience4j给网关装上安全阀
6.1 为关键路由配置熔断:当用户服务超时率>50%时自动隔离
Gateway 4.1原生集成Resilience4j,无需额外starter。在application.yml中定义熔断器:
resilience4j: circuitbreaker: instances: user-service-cb: failure-rate-threshold: 50 # 错误率超50%开启熔断 minimum-number-of-calls: 10 # 至少10次调用才统计 wait-duration-in-open-state: 60s # 熔断后60秒尝试半开 permitted-number-of-calls-in-half-open-state: 3 # 半开状态允许3次试探 automatic-transition-from-open-to-half-open-enabled: true spring: cloud: gateway: routes: - id: user-service uri: http://localhost:8081 predicates: - Path=/api/users/** filters: - name: CircuitBreaker args: name: user-service-cb fallbackUri: forward:/fallback/user # 熔断时转到降级接口6.2 限流:按IP维度限制每秒100次请求
用RequestRateLimiter过滤器,结合Redis存储计数:
spring: cloud: gateway: routes: - id: user-service uri: http://localhost:8081 predicates: - Path=/api/users/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 # 每秒补充100令牌 redis-rate-limiter.burstCapacity: 200 # 最大突发200令牌 key-resolver: "#{@ipKeyResolver}" # Bean名,需自定义@Configuration public class KeyResolverConfig { @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just( exchange.getRequest() .getRemoteAddress() .getAddress() .getHostAddress() ); } }关键参数:
replenishRate=100:令牌桶每秒注入100个令牌burstCapacity=200:桶容量200,允许短时突发流量ipKeyResolver:按客户端IP限流,避免单用户刷爆接口
6.3 验证熔断效果:用wrk压测触发熔断器状态切换
# 模拟用户服务宕机(返回500) python3 -m http.server 8081 --bind 127.0.0.1:8081 --directory ./error-mock # 用wrk持续压测,观察熔断器状态 wrk -t2 -c100 -d30s http://localhost:8080/api/users/list # 查看熔断器实时状态(需暴露actuator端点) curl http://localhost:8080/actuator/circuitbreakers # 返回:{"user-service-cb":{"failureRate":62.5,"state":"OPEN"}}血泪经验:熔断器
wait-duration-in-open-state必须大于下游服务恢复时间。若设为10秒,但服务重启需15秒,则熔断器反复开关,形成“熔断风暴”。我在线上将此值设为服务平均恢复时间的3倍,再加30秒缓冲。
最后说一句:网关不是越复杂越好。我见过团队给Gateway加了17个Filter,结果一个Mono.delay()导致所有请求延迟300ms。现在我的原则是——能用Nginx做的,绝不用Gateway;能用配置做的,绝不用代码;能用全局Filter做的,绝不写路由级Filter。希望帮到你。
本文还有配套的精品资源,点击获取