Skip to content

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 BootSpring 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:

xml
<!-- 父工程只需用 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 一个注解即可把应用变成注册中心,自身不注册、不拉取

java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
yaml
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,唯一增量是注册配置:

yaml
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=8101PORT=8102 各启动一个 user-service 实例后,/eureka/apps/user-service 的实例节点:

xml
<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 日志先打失败后自动成功:

text
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

java
@SpringBootApplication
@EnableFeignClients            // 扫描 @FeignClient 接口并生成 HTTP 客户端实现
public class OrderServiceApplication { ... }

代码:调用方只写接口,不写 HTTP

@FeignClient(name = "user-service") 表示「按服务名找实例」——地址解析交给 LoadBalancer + Eureka,不写 IP

java
@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 实例:

text
$ 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 代码。

故障剔除的完整语义(含两个时延)

「把坏实例摘掉」这件事其实分两段,一段是服务端的,一段是客户端的

  1. 服务端剔除:实例心跳停止后,Eureka 等待 lease-expiration-duration-in-seconds(生产默认 90s,本文演示调成 6s),再由每 eviction-interval-timer-in-ms 一次(默认 60s)的扫描真正踢出注册表;
  2. 客户端缓存:消费者(order-service 的 eureka-client)默认每 registry-fetch-interval-seconds30s)才拉一次注册表。即使服务端已剔除,消费者本地缓存仍可能把请求打向已死实例,直到下一次刷新。
text
实例宕机 t=0 ──心跳停──▶ 服务端 6s/90s 后剔除注册
                        │(这一跳只影响"新拉取注册表的人")
                        └─▶ 消费者每 30s 才刷新本地缓存
                          → 剔除后最多还有约一个刷新周期会打到死实例

所以生产上心跳超时和消费者刷新周期都要主动调优,并配合调用侧超时 + 重试 + 熔断(见微服务 §6 韧性),单靠注册中心「剔除」不能保证一次都不打坏实例。注册中心视角的健康(status=UP)≠ 真实可用(能处理请求)——精细做法是用就绪探针/心跳把「不可用但进程活着」的实例先摘掉(容器环境直接走 K8s 就绪探针,见容器与部署)。

四、Spring Cloud Gateway:统一入口

Gateway 基于 WebFlux(响应式),路由规则由 Route = id + uri + predicates(匹配) + filters(加工) 组成。

配置:lb:// 前缀把负载均衡接进网关

yaml
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/

实测:经网关两次调用落到不同实例

text
$ 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/RewritePathRetryRequestRateLimiter 限流、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

六、检查清单

  1. Boot 与 Spring Cloud 版本对齐(Release Train 兼容表),别用 Boot 3.x 配 2021.0 系 BOM;
  2. 服务名全局唯一、有语义(spring.application.name),注册发现、Feign、网关全按名字走,禁止写死 IP
  3. 注册中心端口与地址用配置外置,多环境(dev/prod)不同 defaultZone;
  4. 心跳/剔除周期按业务容忍度配置:默认 30s/90s 偏慢,缩短要评估对注册中心的请求压力;
  5. 明确自我保护开关:生产保持默认 true,网络分区时不误清注册表;
  6. Feign 接口路径、参数、返回类型与生产者契约一致(契约测试见微服务 §8),生产用 OpenAPI/gRPC 定义先行;
  7. 所有服务间调用配超时、只对幂等操作重试;结合熔断与降级,别裸调;
  8. 敏感服务不直接暴露,流量收口到网关统一做鉴权/限流/审计;
  9. Gateway 是 WebFlux 应用,别引入 spring-boot-starter-web(两者争抢 Servlet 容器会启动冲突);
  10. 链路追踪 + 结构化日志带上 traceId,跨服务排障才不靠猜(三支柱见微服务 §7)。

七、常见坑速查

  1. 版本不匹配:Cloud 2023.0.x 配 Boot 3.3 没问题,但 Cloud 2022.0/2021.0 配 Boot 3.x 直接 NoClassDefFoundError: javax/... 或 Bean 装配失败;
  2. 把 Ribbon 配置当回事:2021.0 起 Ribbon 已移除,负载均衡统一走 Spring Cloud LoadBalancer,旧 ribbon.* 配置全部失效;
  3. @FeignClient 忘写 name 或拼错服务名:启动不报错,首次调用才 UnknownHostException(它真的把服务名当域名去解析了);
  4. Feign 默认超时是连接 10s / 读取 60s(Feign Options 默认值):下游慢接口会拖住调用线程,生产要显式配置 connect/read timeout;
  5. Feign 默认只记录 NONE 级别日志:排查要开 logging.level.<客户端接口>=DEBUG 并在 @Configuration 里声明 Logger.Level.FULL
  6. 服务注册进注册中心 ≠ 服务健康:进程活着但依赖 DB 挂了仍会 status=UP,真实健康要看业务健康检查并把失败的实例摘掉;
  7. 默认 30s 才拉一次注册表:摘除/新增实例的感知有延迟,调用侧一定要配合超时+重试+熔断兜底;
  8. 生产盲目关自我保护:网络分区时可能把活实例当死实例清空,引发连锁失败(演示才关);
  9. Gateway 工程加了 spring-boot-starter-web:WebFlux 与 Servlet 双容器冲突,启动报错或路由全部失效;
  10. 把鉴权写在每个服务里:应收口到网关/公共过滤器,否则新服务容易漏(鉴权细节见鉴权认证);
  11. 配置中心密钥明文进 Git:密钥单独存放、可轮换(见容器与部署微服务 §8 配置治理);
  12. 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 GatewaySpring Cloud OpenFeignSpring Cloud NetflixResilience4j

下一步

  • [ ] 补 Config Server + /actuator/refresh 实测:Git 后端配置 + 运行时刷新
  • [ ] Resilience4j 接入 Feign 的 fallback 实测(与微服务 §6.1 状态机 对照)
  • [ ] Micrometer Tracing + Zipkin 的最小链路实测,抓一张跨服务 trace 截图
  • [ ] 同工程改用 Nacos(注册 + 配置)跑一遍,沉淀「Eureka vs Nacos」迁移要点

写作规范请参阅领域概览

基于 VitePress 构建 · 内容以知识共享方式沉淀