Spring Cloud
Spring Cloud 是 Java 生态的微服务治理全家桶:Spring Boot 解决「单进程内的装配」,Spring Cloud 解决「多进程间的治理」——注册发现、声明式调用、网关路由、配置中心、链路追踪。本文用一个四模块工程(OpenJDK 17.0.20 / Spring Boot 3.3.5 / Spring Cloud 2023.0.3)实测主干链路:Eureka 注册中心(8761)→ user-service 双实例(8101/8102)→ order-service 经 OpenFeign + LoadBalancer 消费(8201)→ Spring Cloud Gateway 统一入口(8080)。代码与配置均可直接复现。
一、Spring Cloud 是什么:治理能力矩阵
| 维度 | Spring Boot | Spring Cloud |
|---|---|---|
| 解决对象 | 进程内:Bean、MVC、数据访问、Security | 进程间:服务发现、调用、路由、配置 |
| 心智模型 | 一个可独立运行的应用 | 一群协作的服务 + 基础设施 |
| 部署单位 | 单体 / 单模块 | 每个服务各自打包、各自伸缩 |
| 无微服务时 | 完全可以不用 | 拆成多服务后才需要 |
Spring Cloud 各组件对应微服务实践中的具体环节(完整模式对照见微服务):
| 治理环节 | Spring Cloud 组件 | 本文 |
|---|---|---|
| 服务注册发现 | Eureka Server/Client、Consul、Nacos | ✅ Eureka |
| 声明式服务调用 | OpenFeign(HTTP 客户端由接口生成) | ✅ |
| 客户端负载均衡 | Spring Cloud LoadBalancer(替代已停更的 Ribbon) | ✅ |
| API 网关 | Spring Cloud Gateway | ✅ |
| 配置中心 | Spring Cloud Config / Nacos / Apollo | 原理说明 |
| 熔断降级 | spring-cloud-circuitbreaker(Resilience4j) | 原理说明 |
| 链路追踪 | Micrometer Tracing(替代 Sleuth) | 原理说明 |
| 消息驱动 | Spring Cloud Stream | 原理说明 |
版本:Release Train 用 BOM 对齐
Spring Cloud 用「伦敦地铁站名」做整体版本号(Release Train),与 Spring Boot 小版本严格对应,直接依赖版本号经常配错。2023.0 代号 Leyton,支持 Boot 3.2/3.3:
<!-- 父工程只需用 spring-boot-starter-parent;Spring Cloud 以 BOM 形式 import -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>版本错配是最常见的坑:Cloud 2023.0.x 只能配 Boot 3.2/3.3;拿 2021.0.x(Boot 2.x 系)配 Boot 3.3 会因
javax/jakarta命名空间冲突启动即炸。选版本先查 Spring Cloud 官方版本说明 的「Release Train / Boot 兼容表」。
二、服务注册与发现:Eureka
搭建:注册中心不需要业务代码
@EnableEurekaServer 一个注解即可把应用变成注册中心,自身不注册、不拉取:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false # 注册中心自身不注册
fetch-registry: false # 自身不拉取注册表
server:
enable-self-preservation: false # 自我保护:演示故障剔除才关闭(生产保持默认 true)
eviction-interval-timer-in-ms: 2000 # 淘汰扫描周期服务端以 注册表(registry) 形式暴露实例信息:GET /eureka/apps 返回 XML,生产控制台是 http://localhost:8761/。
生产者:只需注册,调用方按名找
user-service 就是一个普通 Web 服务 + eureka-client,唯一增量是注册配置:
server:
port: ${PORT:8101} # 支持环境变量覆盖,便于一台机器起多实例
spring:
application:
name: user-service # 服务名:注册与调用的唯一逻辑身份
eureka:
instance:
lease-renewal-interval-in-seconds: 2 # 心跳间隔(生产默认 30s,演示调快)
lease-expiration-duration-in-seconds: 6 # 超过 N 秒未心跳则被剔除(生产默认 90s)
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/实测:注册表里的元数据长什么样
本文以 PORT=8101 与 PORT=8102 各启动一个 user-service 实例后,/eureka/apps/user-service 的实例节点:
<application>
<name>USER-SERVICE</name>
<instance>
<instanceId>192.168.5.52:user-service:8102</instanceId> <!-- 实例唯一身份 -->
<hostName>192.168.5.52</hostName>
<app>USER-SERVICE</app>
<status>UP</status> <!-- 注册中心视角的健康 -->
<port enabled="true">8102</port>
<leaseInfo>
<renewalIntervalInSecs>2</renewalIntervalInSecs> <!-- 与 yml 心跳配置一致 -->
<durationInSecs>6</durationInSecs> <!-- 失联多久算死 -->
<lastRenewalTimestamp>1788495214896</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1788495027705</serviceUpTimestamp>
</leaseInfo>
<homePageUrl>http://192.168.5.52:8102/</homePageUrl>
<vipAddress>user-service</vipAddress>
<actionType>ADDED</actionType>
</instance>
<!-- ... 8101 实例结构相同,靠 instanceId 区分 ... -->
</application>Eureka 是 AP 型注册中心(客户端发现模式),它保证「可用性优先,网络分区时各读各的缓存」——这正是它在网络抖动时会启动自我保护(self-preservation)的原因:宁可保留可能已死的实例,也不清空注册表造成雪崩。判活靠心跳租约(lease),不是探活:客户端每 N 秒续约一次,服务端到点没等到就剔除。
实测:注册中心晚于服务启动时,客户端会自动重试
把 Eureka 与三个服务同时启动(Eureka 约 8s 才就绪),user-service 日志先打失败后自动成功:
2026-09-04T12:04:29.375 INFO DiscoveryClient_USER-SERVICE/192.168.5.52:user-service:8101
- was unable to refresh its cache! This periodic background refresh will be retried in 30 seconds.
status = Cannot execute request on any known server
2026-09-04T12:04:29.393 INFO DiscoveryClient_USER-SERVICE/...: registering service...
2026-09-04T12:04:29.467 WARN DiscoveryClient_USER-SERVICE/... - registration failed
Cannot execute request on any known server
(Eureka 就绪后)→ 注册成功,注册表出现 8101/8102启动顺序反过来(先注册中心后服务)是常规做法;但不小心先起服务也不必慌:eureka-client 后台线程会按周期持续重试注册与刷新,注册中心稍后就绪即可自动补上。
三、声明式调用:OpenFeign + LoadBalancer
配置:客户端三件套
order-service 的 pom 增加 eureka-client、openfeign、loadbalancer;启动类开 @EnableFeignClients。
@SpringBootApplication
@EnableFeignClients // 扫描 @FeignClient 接口并生成 HTTP 客户端实现
public class OrderServiceApplication { ... }代码:调用方只写接口,不写 HTTP
@FeignClient(name = "user-service") 表示「按服务名找实例」——地址解析交给 LoadBalancer + Eureka,不写 IP:
@FeignClient(name = "user-service") // name = 注册中心里的服务名
public interface UserClient {
@GetMapping("/api/users/{id}") // 路径与目标 Controller 一致
UserDto getUser(@PathVariable("id") Long id);
}
// user-service 侧(生产者,双实例各自返回本实例端口,便于观察负载均衡)
@RestController
public class UserController {
@Value("${server.port}") private String port;
@GetMapping("/api/users/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
return Map.of("id", id, "name", "alice-" + id, "instance", "user-service:" + port);
}
}
// order-service 侧(消费者):同步聚合 user 数据
@RestController
public class OrderController {
private final UserClient userClient; // 接口注入,运行时是 Feign 代理
public OrderController(UserClient userClient) { this.userClient = userClient; }
@GetMapping("/api/orders/{id}")
public Map<String, Object> getOrder(@PathVariable Long id) {
UserDto user = userClient.getUser(id); // 网络调用发生在这一行
return Map.of("orderId", id, "product", "book-" + id, "user", user);
}
}实测:双实例轮询(round-robin)
连续调用 order-service 6 次,每次由 LoadBalancer 从注册表挑一个 user-service 实例:
$ for i in 1 2 3 4 5 6; do curl -s http://127.0.0.1:8201/api/orders/1; echo; done
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8101"}}
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8102"}}
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8101"}}
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8102"}}
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8101"}}
{"orderId":1,"product":"book-1","user":{"id":1,"name":"alice-1","instance":"user-service:8102"}}6 次请求在 8101 ↔ 8102 严格交替,默认负载均衡策略即轮询;要换权重/一致性哈希等只需配置 LoadBalancer 策略(spring.cloud.loadbalancer.xxx),换算法不换 Feign 代码。
故障剔除的完整语义(含两个时延)
「把坏实例摘掉」这件事其实分两段,一段是服务端的,一段是客户端的:
- 服务端剔除:实例心跳停止后,Eureka 等待
lease-expiration-duration-in-seconds(生产默认 90s,本文演示调成 6s),再由每eviction-interval-timer-in-ms一次(默认 60s)的扫描真正踢出注册表; - 客户端缓存:消费者(order-service 的 eureka-client)默认每
registry-fetch-interval-seconds(30s)才拉一次注册表。即使服务端已剔除,消费者本地缓存仍可能把请求打向已死实例,直到下一次刷新。
实例宕机 t=0 ──心跳停──▶ 服务端 6s/90s 后剔除注册
│(这一跳只影响"新拉取注册表的人")
└─▶ 消费者每 30s 才刷新本地缓存
→ 剔除后最多还有约一个刷新周期会打到死实例所以生产上心跳超时和消费者刷新周期都要主动调优,并配合调用侧超时 + 重试 + 熔断(见微服务 §6 韧性),单靠注册中心「剔除」不能保证一次都不打坏实例。注册中心视角的健康(status=UP)≠ 真实可用(能处理请求)——精细做法是用就绪探针/心跳把「不可用但进程活着」的实例先摘掉(容器环境直接走 K8s 就绪探针,见容器与部署)。
四、Spring Cloud Gateway:统一入口
Gateway 基于 WebFlux(响应式),路由规则由 Route = id + uri + predicates(匹配) + filters(加工) 组成。
配置:lb:// 前缀把负载均衡接进网关
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # lb:// + 服务名:LoadBalancer 从注册中心选实例
predicates:
- Path=/api/users/** # 命中该路径才转发
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/实测:经网关两次调用落到不同实例
$ curl -s http://127.0.0.1:8080/api/users/9
{"name":"alice-9","instance":"user-service:8101","id":9}
$ curl -s http://127.0.0.1:8080/api/users/9
{"name":"alice-9","id":9,"instance":"user-service:8102"}网关同样做了负载均衡(8080 → 8101/8102 交替),业务服务无需感知网关存在;把路由、限流、鉴权等横切关注点收口到网关,符合微服务 §4 网关职责的边界(横切可以放,业务编排不能放)。
常用 Predicates / Filters 速查
| Predicates(匹配) | Filters(加工) |
|---|---|
Path 路径、Method 方法、Header/Query/Cookie 携带条件、Host 域名 | StripPrefix 去前缀、AddRequestHeader/RewritePath、Retry、RequestRateLimiter 限流、CircuitBreaker 熔断、SetPath 改写 |
五、家族其余成员的定位
治理链路的其余环节大多不依赖具体业务代码,认识「它解决什么问题 + 怎么接」即可:
| 成员 | 解决什么 | 一句话要点 |
|---|---|---|
| Spring Cloud Config | 配置外置 + 多环境/多服务统一管理 | Config Server 从 Git 仓库出配置,客户端 spring.config.import=configserver: 启动时拉取;变更走 /actuator/refresh 或 Bus 广播 |
| Micrometer Tracing | 分布式链路追踪 | 面向观测的 API 抽象,对接 Zipkin/OTel;traceparent 随调用透传,对应微服务 §7 三支柱 |
| spring-cloud-circuitbreaker | 熔断 + 降级(Resilience4j) | 给 Feign/Gateway 配 @CircuitBreaker 或 fallback,状态机语义见微服务 §6.1 熔断实测 |
| Spring Cloud Stream | 消息驱动、发布订阅 | 抽象 Kafka/RabbitMQ 成 binder,对应消息队列的投递语义与幂等 |
| Nacos / Consul / K8s Service | 注册发现 + 配置中心的替代 | Eureka 已停止重大更新;新项目 Java 生态常选 Nacos,云原生直接复用 K8s DNS/Service(对照表见微服务 §3) |
六、检查清单
- Boot 与 Spring Cloud 版本对齐(Release Train 兼容表),别用 Boot 3.x 配 2021.0 系 BOM;
- 服务名全局唯一、有语义(
spring.application.name),注册发现、Feign、网关全按名字走,禁止写死 IP; - 注册中心端口与地址用配置外置,多环境(dev/prod)不同 defaultZone;
- 心跳/剔除周期按业务容忍度配置:默认 30s/90s 偏慢,缩短要评估对注册中心的请求压力;
- 明确自我保护开关:生产保持默认 true,网络分区时不误清注册表;
- Feign 接口路径、参数、返回类型与生产者契约一致(契约测试见微服务 §8),生产用 OpenAPI/gRPC 定义先行;
- 所有服务间调用配超时、只对幂等操作重试;结合熔断与降级,别裸调;
- 敏感服务不直接暴露,流量收口到网关统一做鉴权/限流/审计;
- Gateway 是 WebFlux 应用,别引入 spring-boot-starter-web(两者争抢 Servlet 容器会启动冲突);
- 链路追踪 + 结构化日志带上 traceId,跨服务排障才不靠猜(三支柱见微服务 §7)。
七、常见坑速查
- 版本不匹配:Cloud 2023.0.x 配 Boot 3.3 没问题,但 Cloud 2022.0/2021.0 配 Boot 3.x 直接
NoClassDefFoundError: javax/...或 Bean 装配失败; - 把 Ribbon 配置当回事:2021.0 起 Ribbon 已移除,负载均衡统一走 Spring Cloud LoadBalancer,旧
ribbon.*配置全部失效; @FeignClient忘写name或拼错服务名:启动不报错,首次调用才UnknownHostException(它真的把服务名当域名去解析了);- Feign 默认超时是连接 10s / 读取 60s(Feign Options 默认值):下游慢接口会拖住调用线程,生产要显式配置 connect/read timeout;
- Feign 默认只记录 NONE 级别日志:排查要开
logging.level.<客户端接口>=DEBUG并在@Configuration里声明Logger.Level.FULL; - 服务注册进注册中心 ≠ 服务健康:进程活着但依赖 DB 挂了仍会
status=UP,真实健康要看业务健康检查并把失败的实例摘掉; - 默认 30s 才拉一次注册表:摘除/新增实例的感知有延迟,调用侧一定要配合超时+重试+熔断兜底;
- 生产盲目关自我保护:网络分区时可能把活实例当死实例清空,引发连锁失败(演示才关);
- Gateway 工程加了
spring-boot-starter-web:WebFlux 与 Servlet 双容器冲突,启动报错或路由全部失效; - 把鉴权写在每个服务里:应收口到网关/公共过滤器,否则新服务容易漏(鉴权细节见鉴权认证);
- 配置中心密钥明文进 Git:密钥单独存放、可轮换(见容器与部署 与微服务 §8 配置治理);
- Feign 直接透传 JPA 实体:懒加载序列化异常 + 内部字段泄露,进出口一律 DTO(同 Spring Boot 数据访问坑)。
状态与参考
- 状态:已收录(2026-09-04,后端领域新增 Spring Cloud 主题,完成主干链路实测)。
- 运行环境:OpenJDK 17.0.20 / Maven 3.9.16 / Spring Boot 3.3.5 / Spring Cloud 2023.0.3(Leyton)。四模块多进程实测:eureka-server:8761、user-service:8101/8102、order-service:8201、gateway:8080。注册 XML、轮询输出、网关输出、启动期注册重试日志均来自本机实际运行;「剔除时间线」按 lease 配置与客户端刷新周期给出语义拆解(本会话终止子进程的权限受限,未做进程级拔线录屏)。
- 关联:Spring Boot(单进程基础)、微服务(模式总览与韧性实测)、鉴权认证、系统设计、容器与部署、后端概览。
- 参考:Spring Cloud 官方文档、Spring Cloud Gateway、Spring Cloud OpenFeign、Spring Cloud Netflix、Resilience4j。
下一步
- [ ] 补 Config Server +
/actuator/refresh实测:Git 后端配置 + 运行时刷新 - [ ] Resilience4j 接入 Feign 的 fallback 实测(与微服务 §6.1 状态机 对照)
- [ ] Micrometer Tracing + Zipkin 的最小链路实测,抓一张跨服务 trace 截图
- [ ] 同工程改用 Nacos(注册 + 配置)跑一遍,沉淀「Eureka vs Nacos」迁移要点
写作规范请参阅领域概览。