Spring Cloud第三篇:通信篇 — OpenFeign 与负载均衡

目标 :掌握微服务间的声明式 HTTP 调用
学习时长:1~2 周


目录

  1. 服务间通信方式
  2. [OpenFeign 快速入门](#OpenFeign 快速入门)
  3. [Feign 核心注解](#Feign 核心注解)
  4. [Feign 日志配置](#Feign 日志配置)
  5. [Feign 超时配置](#Feign 超时配置)
  6. [Feign 请求拦截器](#Feign 请求拦截器)
  7. [Feign 降级(Fallback)](#Feign 降级(Fallback))
  8. [Spring Cloud LoadBalancer](#Spring Cloud LoadBalancer)
  9. RestTemplate(对比)
  10. [Feign vs RestTemplate](#Feign vs RestTemplate)
  11. 面试高频题

1. 服务间通信方式

复制代码
同步调用:
  OpenFeign(推荐)→ HTTP REST
  RestTemplate     → HTTP REST(命令式,已较少用)
  gRPC             → HTTP/2 + Protobuf(高性能、跨语言)

异步调用:
  RabbitMQ / Kafka → 消息队列(解耦、削峰、最终一致)

同步 vs 异步选择

场景 推荐方式
需要立即得到结果(下单查库存) OpenFeign 同步调用
不需要立即得到结果(发通知邮件) 消息队列异步
跨语言高性能 gRPC

2. OpenFeign 快速入门

引入依赖

xml 复制代码
<!-- spring-cloud-starter-openfeign 不包含 LoadBalancer,需一并引入 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

开启 Feign(启动类)

java 复制代码
@SpringBootApplication
@EnableFeignClients  // 扫描 @FeignClient 注解,生成代理对象
public class OrderServiceApplication { ... }

定义 Feign 客户端

java 复制代码
/**
 * @FeignClient(name = "user-service")
 * Spring 会:
 * 1. 从 Eureka/Nacos 中查找 "user-service" 的实例列表
 * 2. 使用 LoadBalancer 选择一个实例(默认轮询)
 * 3. 将方法调用转换为 HTTP 请求发出去
 * 4. 将响应反序列化为方法返回类型
 */
@FeignClient(name = "user-service")
public interface UserFeignClient {

    // 与 user-service 的 Controller 路径完全一致
    @GetMapping("/api/users/{id}")
    Result<User> getUserById(@PathVariable("id") Long id);

    @PostMapping("/api/users")
    Result<User> createUser(@RequestBody CreateUserDTO dto);

    @GetMapping("/api/users")
    Result<List<User>> listAll(@RequestParam(required = false) String keyword);
}

注入使用

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final UserFeignClient userFeignClient;  // 像本地 Bean 一样注入

    public void createOrder(Long userId) {
        Result<User> result = userFeignClient.getUserById(userId);  // 透明 HTTP 调用
        User user = result.getData();
        // ...
    }
}

3. Feign 核心注解

路径与参数

java 复制代码
@FeignClient(name = "user-service")
public interface UserFeignClient {

    // @PathVariable:路径参数(必须写 value)
    @GetMapping("/api/users/{id}")
    Result<User> getById(@PathVariable("id") Long id);

    // @RequestParam:查询参数
    @GetMapping("/api/users")
    Result<Page<User>> list(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "keyword", required = false) String keyword);

    // @RequestBody:JSON 请求体
    @PostMapping("/api/users")
    Result<User> create(@RequestBody CreateUserDTO dto);

    // @RequestHeader:请求头
    @GetMapping("/api/users/profile")
    Result<User> getProfile(@RequestHeader("Authorization") String token);
}

文件上传

java 复制代码
@PostMapping(value = "/api/files/upload",
             consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result<String> upload(@RequestPart("file") MultipartFile file);

4. Feign 日志配置

Feign 有四种日志级别:

级别 输出内容
NONE(默认) 不输出
BASIC 请求方法、URL、响应状态码、耗时
HEADERS BASIC + 请求/响应头
FULL HEADERS + 请求/响应体
java 复制代码
// 全局配置
@Configuration
public class FeignConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;  // 开发时用 FULL,生产用 BASIC
    }
}

// 针对单个客户端配置
@FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserFeignClient { ... }
yaml 复制代码
# application.yml 中开启 Feign 接口的日志(SLF4J 需要 DEBUG 级别)
logging:
  level:
    com.example.cloud.order.feign: DEBUG  # 指定 Feign 接口的包路径

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            loggerLevel: BASIC  # NONE/BASIC/HEADERS/FULL

5. Feign 超时配置

yaml 复制代码
spring:
  cloud:
    openfeign:
      client:
        config:
          # 全局默认配置
          default:
            connect-timeout: 5000    # 连接超时(ms)
            read-timeout: 10000      # 读取超时(ms)

          # 针对具体服务的配置(覆盖全局)
          user-service:
            connect-timeout: 3000
            read-timeout: 5000
          order-service:
            read-timeout: 30000      # 下单接口耗时长,设置更大超时

6. Feign 请求拦截器

常用于:统一传递 Token、TraceId、租户ID等

java 复制代码
@Component
public class FeignAuthInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 1. 从当前请求上下文获取 Token
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            String token = attrs.getRequest().getHeader("Authorization");
            if (token != null) {
                // 2. 透传到下游服务(解决 Feign 调用丢失 Token 问题)
                template.header("Authorization", token);
            }
        }

        // 3. 添加链路追踪 ID
        template.header("X-Trace-Id", UUID.randomUUID().toString());

        // 4. 添加服务标识
        template.header("X-Source-Service", "order-service");
    }
}

⚠️ 重要:Feign 调用默认不会传递当前请求的 Header,必须通过拦截器手动传递(Token、TraceId等)。


7. Feign 降级(Fallback)

当目标服务不可用时,返回默认值而不是抛异常。

方式一:fallback 类

java 复制代码
@FeignClient(name = "user-service", fallback = UserFallback.class)
public interface UserFeignClient {
    @GetMapping("/api/users/{id}")
    Result<User> getUserById(@PathVariable("id") Long id);
}

@Component  // 必须注册为 Bean
public class UserFallback implements UserFeignClient {
    @Override
    public Result<User> getUserById(Long id) {
        log.warn("user-service 不可用,触发降级");
        return Result.fail(503, "用户服务暂不可用,请稍后重试");
    }
}

方式二:fallbackFactory(可获取异常信息)

java 复制代码
@FeignClient(name = "user-service", fallbackFactory = UserFallbackFactory.class)
public interface UserFeignClient { ... }

@Component
public class UserFallbackFactory implements FallbackFactory<UserFeignClient> {
    @Override
    public UserFeignClient create(Throwable cause) {
        return id -> {
            log.error("调用 user-service 失败: {}", cause.getMessage());
            return Result.fail(503, "用户服务异常: " + cause.getMessage());
        };
    }
}

注意:使用 Feign Fallback 需要配合 Resilience4j 或 Sentinel 的断路器,需引入相关依赖并开启断路器。


8. Spring Cloud LoadBalancer

Spring Cloud LoadBalancer 是 Ribbon 的替代品,负责从服务实例列表中选择一个实例。

内置策略

java 复制代码
// 1. 轮询(默认):依次请求每个实例
// 2. 随机:每次随机选一个实例

// 自定义为随机策略:
@Configuration
@LoadBalancerClient(name = "user-service",
                    configuration = UserServiceLoadBalancerConfig.class)
public class UserServiceLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            LoadBalancerClientFactory factory) {
        return new RandomLoadBalancer(
                factory.getLazyProvider("user-service",
                        ServiceInstanceListSupplier.class), "user-service");
    }
}

区域感知路由(Zone Affinity)

yaml 复制代码
spring:
  cloud:
    loadbalancer:
      zone: shanghai  # 优先选择同 zone 的实例(需在实例 metadata 中配置 zone)

9. RestTemplate(对比)

java 复制代码
// 传统方式(不推荐,冗余代码多)
@Configuration
public class RestTemplateConfig {
    @Bean
    @LoadBalanced  // 开启负载均衡(可使用服务名代替 IP)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Service
@RequiredArgsConstructor
public class OrderService {
    private final RestTemplate restTemplate;

    public User getUser(Long id) {
        // 需手动处理序列化、错误码、类型转换
        ResponseEntity<Result<User>> response = restTemplate.exchange(
            "http://user-service/api/users/{id}",  // 使用服务名
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<Result<User>>() {},
            id
        );
        return response.getBody().getData();
    }
}

10. Feign vs RestTemplate

维度 OpenFeign RestTemplate
代码量 少(接口声明) 多(命令式)
类型安全 ✅(编译期检查) ❌(运行时)
日志/拦截 ✅ 内置支持 手动实现
超时配置 简单(配置文件) 需要定制
降级 ✅(fallback) 手动 try-catch
可读性 高(接口即文档)
推荐 ✅ 新项目首选 不推荐

11. 面试高频题

Q1:OpenFeign 的底层原理是什么?

Feign 通过 JDK 动态代理为 @FeignClient 接口生成代理对象。调用接口方法时,代理将方法签名转换为 HTTP 请求(使用 Encoder 序列化参数),通过 LoadBalancer 选择实例,然后使用 HTTP 客户端(默认 JDK HttpURLConnection,可换 OkHttp/Apache HttpClient)发起请求,最后用 Decoder 反序列化响应。

Q2:Feign 调用时如何传递 Header(如 Token)?

实现 RequestInterceptor 接口,在 apply() 方法中通过 RequestContextHolder 获取当前请求的 Header,然后通过 RequestTemplate.header() 添加到 Feign 请求中。

Q3:Feign 的超时时间和 Ribbon/LoadBalancer 的超时有什么区别?

Feign 自身有连接超时和读超时配置;LoadBalancer 控制负载均衡行为(选择哪个实例),不控制超时。Feign 的超时配置需要在 spring.cloud.openfeign.client.config 中设置,不要和 Ribbon 的配置混淆。

Q4:Feign 和 Dubbo 的区别?

Feign 基于 HTTP REST,轻量易用,与 Spring 生态无缝集成;Dubbo 基于自定义 RPC 协议,性能更高(序列化更高效),适合内部高频调用。现在 Dubbo 也支持 REST 模式,两者差距在缩小。

Q5:如何解决 Feign 调用丢失 ThreadLocal 数据的问题?

Feign 使用线程池异步执行请求,ThreadLocal 中的数据不会传递。解决方案:①使用 RequestInterceptor 将数据转移到 Header;②使用 TransmittableThreadLocal(阿里 TTL)实现线程池内的数据传递。


上一篇:02_注册中心篇 | 下一篇:04_配置中心篇


12. Feign 底层原理深度解析(专家必知)

知识点 1:Feign 代理对象创建链路

text 复制代码
@EnableFeignClients
    └── FeignClientsRegistrar(ImportBeanDefinitionRegistrar)
          └── 扫描 @FeignClient 注解
                └── 为每个接口注册 FeignClientFactoryBean 到 Spring 容器
                      └── 调用 getObject() 时:
                            ├── 从 FeignContext 获取该服务专属的配置(编解码器、拦截器等)
                            ├── 创建 Feign.Builder(配置 Logger、Encoder、Decoder、Contract)
                            └── 生成 JDK 动态代理(ReflectiveFeign.newInstance)
                                  └── 返回代理对象注入 Spring 容器

知识点 2:一次 Feign 调用的完整链路

text 复制代码
调用 userFeignClient.getUserById(1L)
    │
    ▼
JDK InvocationHandler.invoke()
    │
    ├── 根据方法签名找到 MethodHandler
    │
    ├── RequestTemplate 构建(Contract 解析 @GetMapping 等注解)
    │     ├── URL:/api/users/1
    │     ├── Method:GET
    │     └── Headers:从拦截器附加
    │
    ├── RequestInterceptor 链执行(添加 Token、TraceId 等)
    │
    ├── LoadBalancer 选择实例(从 Nacos/Eureka 缓存中选)
    │     └── http://192.168.1.100:9001/api/users/1
    │
    ├── HTTP Client 发起请求(默认 HttpURLConnection,推荐 OkHttp/Apache HC)
    │
    ├── 响应 Decoder 反序列化(JSON → Result<User>)
    │
    └── 若异常 → ErrorDecoder 处理 → 触发 Fallback

知识点 3:替换 Feign 底层 HTTP 客户端(生产优化)

默认使用 JDK 内置 HttpURLConnection,不支持连接池,高并发性能差

xml 复制代码
<!-- 替换为 Apache HttpClient 5(推荐,支持连接池) -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
</dependency>
yaml 复制代码
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true          # 启用 Apache HC5
        max-connections: 200     # 最大连接数
        max-connections-per-route: 50  # 每个目标的最大连接数

为什么要换:JDK HttpURLConnection 每次请求新建 TCP 连接(无连接池),高并发下开销极大;OkHttp/Apache HC5 有连接池,复用 TCP 连接,QPS 可提升 5-10 倍。


13. 自定义负载均衡策略(专家实战)

知识点:基于元数据的版本路由(灰度发布核心)

场景:同一服务有 v1(稳定版)和 v2(灰度版),需要根据请求 Header 中的版本标识路由到对应版本的实例。

java 复制代码
/**
 * 版本感知负载均衡器
 * 读取请求 Header 中的 X-Version,优先选择同版本实例
 */
public class VersionAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 从请求 Header 获取版本要求
        String requestVersion = getRequestVersion(request);

        return supplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new)
            .get(request).next()
            .map(instances -> selectInstance(instances, requestVersion));
    }

    private Response<ServiceInstance> selectInstance(
            List<ServiceInstance> instances, String requestVersion) {
        if (instances.isEmpty()) return new EmptyResponse();

        // 1. 优先选择版本匹配的实例
        if (requestVersion != null) {
            List<ServiceInstance> matched = instances.stream()
                .filter(i -> requestVersion.equals(i.getMetadata().get("version")))
                .collect(Collectors.toList());
            if (!matched.isEmpty()) {
                return new DefaultResponse(matched.get(
                    ThreadLocalRandom.current().nextInt(matched.size())));
            }
        }

        // 2. 无版本匹配,降级到轮询
        return new DefaultResponse(instances.get(
            ThreadLocalRandom.current().nextInt(instances.size())));
    }

    private String getRequestVersion(Request request) {
        if (request.getContext() instanceof RequestDataContext ctx) {
            return ctx.getClientRequest().getHeaders()
                .getFirst("X-Version");
        }
        return null;
    }
}

// 注册自定义负载均衡器
@Configuration
@LoadBalancerClient(name = "user-service",
                    configuration = VersionLoadBalancerConfig.class)
public class VersionLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> versionAwareLoadBalancer(
            LoadBalancerClientFactory factory) {
        return new VersionAwareLoadBalancer("user-service",
            factory.getLazyProvider("user-service", ServiceInstanceListSupplier.class));
    }
}

实例元数据配置

yaml 复制代码
# v1 版本实例(稳定版)
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v1

# v2 版本实例(灰度版)
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2

14. Feign 错误解码器与重试机制

知识点 1:ErrorDecoder ------ 统一处理下游错误响应

text 复制代码
问题:当下游服务返回 4xx/5xx 时,Feign 默认抛出 FeignException
       FeignException 中只有原始响应体字符串,不便于业务处理

解决:自定义 ErrorDecoder,将下游错误响应解析为业务异常
java 复制代码
@Component
public class FeignErrorDecoder implements ErrorDecoder {

    private final ObjectMapper objectMapper;
    private final ErrorDecoder defaultDecoder = new ErrorDecoder.Default();

    public FeignErrorDecoder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Exception decode(String methodKey, Response response) {
        int status = response.status();

        try {
            // 读取响应体
            String body = "";
            if (response.body() != null) {
                body = new String(response.body().asInputStream().readAllBytes(),
                    StandardCharsets.UTF_8);
            }

            // 尝试解析为统一响应格式
            if (body.startsWith("{")) {
                Result<?> result = objectMapper.readValue(body, Result.class);
                // 按状态码做不同处理
                return switch (status) {
                    case 400 -> new BusinessException(400, result.getMessage());
                    case 401 -> new UnauthorizedException(result.getMessage());
                    case 403 -> new ForbiddenException(result.getMessage());
                    case 404 -> new BusinessException(404,
                        "下游资源不存在 [" + methodKey + "]: " + result.getMessage());
                    case 409 -> new BusinessException(409, result.getMessage());
                    default -> new RemoteServiceException(status,
                        "下游服务异常 [" + methodKey + "]: " + result.getMessage());
                };
            }

        } catch (Exception e) {
            // 解析失败,使用默认处理
        }

        return defaultDecoder.decode(methodKey, response);
    }
}

// 注册 ErrorDecoder
@Configuration
public class FeignConfig {

    @Bean
    public FeignErrorDecoder feignErrorDecoder(ObjectMapper objectMapper) {
        return new FeignErrorDecoder(objectMapper);
    }
}

知识点 2:Feign 重试机制

text 复制代码
重试策略选择:
  GET 请求(查询):安全,可以重试(幂等)
  POST 请求(创建):不安全,不应自动重试(可能重复创建)
  PUT/DELETE:通常幂等,可以重试

配置重试时只对 GET 请求重试,其他方法不重试
java 复制代码
@Configuration
public class FeignRetryConfig {

    /**
     * 自定义重试器:只重试连接超时,不重试读超时(读超时可能是业务慢)
     */
    @Bean
    public Retryer feignRetryer() {
        // 参数:重试间隔起始(ms), 最大间隔(ms), 最大尝试次数
        return new Retryer.Default(100, 1000, 3);
    }

    /**
     * 更精细的重试控制(不同服务不同策略)
     */
    @Bean
    @LoadBalancerClient(name = "user-service")
    public Retryer userServiceRetryer() {
        return new Retryer() {
            private int attempt = 1;
            private static final int MAX_ATTEMPTS = 2;

            @Override
            public void continueOrPropagate(RetryableException e) {
                if (attempt++ >= MAX_ATTEMPTS) throw e;
                // 只重试连接超时(connect timeout)
                if (e.getCause() instanceof ConnectTimeoutException) {
                    try { Thread.sleep(100L * attempt); } catch (InterruptedException ignore) {}
                    return;
                }
                throw e; // 其他异常不重试
            }

            @Override
            public Retryer clone() { return this; }
        };
    }
}

15. Feign 文件上传与二进制传输

知识点:跨服务传输文件

java 复制代码
// 文件服务的 Feign 客户端
@FeignClient(name = "file-service", configuration = FeignMultipartConfig.class)
public interface FileServiceClient {

    // 文件上传(multipart/form-data)
    @PostMapping(value = "/api/files/upload",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    Result<String> uploadFile(@RequestPart("file") MultipartFile file,
                              @RequestPart("category") String category);

    // 批量上传
    @PostMapping(value = "/api/files/batch",
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    Result<List<String>> uploadFiles(@RequestPart("files") List<MultipartFile> files);

    // 下载(字节数组)
    @GetMapping("/api/files/{fileId}")
    Response download(@PathVariable String fileId);
}

// 支持 Multipart 的 Feign 配置
@Configuration
public class FeignMultipartConfig {

    @Bean
    public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> converters) {
        return new SpringFormEncoder(new SpringEncoder(converters));
    }
}

// 使用示例
@Service
@RequiredArgsConstructor
public class ProductService {

    private final FileServiceClient fileClient;

    public String uploadProductImage(MultipartFile image) {
        // 直接传递 MultipartFile,Feign 帮你转成 multipart 请求
        Result<String> result = fileClient.uploadFile(image, "product");
        return result.getData();
    }

    public byte[] downloadFile(String fileId) throws IOException {
        // 下载文件(Feign Response)
        Response response = fileClient.download(fileId);
        return response.body().asInputStream().readAllBytes();
    }
}

16. 服务间鉴权:JWT 透传

知识点:微服务间调用的身份传递

text 复制代码
问题:Gateway 验证了用户 JWT,把 userId 放入 Header 转发给下游
      但 order-service 调用 user-service 时,如何携带用户身份?

方案A:下游服务信任 Gateway 的 X-User-Id Header
  order-service 从 Header 取 userId → 调用 user-service 时携带
  Feign 拦截器:从当前请求 Header 取 userId,放入下游请求

方案B:下游服务内部使用内部 Token(M2M)
  服务间调用使用独立的内部 Token(不是用户 JWT)
  内部 Token 拥有服务权限,不代表任何用户

推荐:A 方案(简单)+ B 方案(安全),组合使用
java 复制代码
// 用户上下文(线程级别)
public class UserContextHolder {
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    private static final ThreadLocal<String> USER_ROLES = new ThreadLocal<>();

    public static void setUserId(String userId) { USER_ID.set(userId); }
    public static String getUserId() { return USER_ID.get(); }
    public static void clear() { USER_ID.remove(); USER_ROLES.remove(); }
}

// Gateway 解析 JWT,透传到 Header
// (在 Gateway 全局过滤器中已实现,见第5章)
// X-User-Id: 123
// X-User-Roles: ADMIN,USER

// 下游服务拦截器:从 Request Header 读取,存入 ThreadLocal
@Component
public class UserContextInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) {
        String userId = request.getHeader("X-User-Id");
        String roles = request.getHeader("X-User-Roles");
        if (userId != null) {
            UserContextHolder.setUserId(userId);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
                                Object handler, Exception ex) {
        UserContextHolder.clear(); // 必须清理,防止线程池复用时污染
    }
}

// Feign 拦截器:调用下游时透传 Header
@Component
public class UserContextFeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从 ThreadLocal 取当前用户 ID,放入下游请求
        String userId = UserContextHolder.getUserId();
        if (userId != null) {
            template.header("X-User-Id", userId);
        }

        // 透传原始请求中的其他追踪 Header
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            String traceId = attrs.getRequest().getHeader("X-Trace-Id");
            if (traceId != null) template.header("X-Trace-Id", traceId);
        }
    }
}

17. OpenFeign 接口契约测试

知识点:确保服务提供者和消费者接口一致

text 复制代码
问题:user-service 修改了接口(参数、返回值),
      order-service 的 FeignClient 没有更新 → 运行时报错

解决:契约测试(Contract Testing)
  提供者发布接口契约(Spring Cloud Contract)
  消费者基于契约生成测试用例
  CI/CD 中自动验证契约

简单方案:共享 API 模块
java 复制代码
// user-service-api 模块(单独的 jar,提供者和消费者都依赖)
// 这样接口变更时,编译期就能发现不兼容
public interface UserApi {

    @GetMapping("/api/users/{id}")
    Result<UserDTO> getUserById(@PathVariable Long id);

    @PostMapping("/api/users/batch")
    Result<List<UserDTO>> getUsersByIds(@RequestBody List<Long> ids);
}

// user-service 实现 UserApi
@RestController
public class UserController implements UserApi {

    @Override
    public Result<UserDTO> getUserById(Long id) { ... }

    @Override
    public Result<List<UserDTO>> getUsersByIds(List<Long> ids) { ... }
}

// order-service 的 FeignClient 继承 UserApi(接口共享,类型安全)
@FeignClient(name = "user-service")
public interface UserFeignClient extends UserApi {
    // 直接继承,无需重复定义方法签名
    // user-service 修改接口时,这里编译期就报错
}
相关推荐
JAVA面经实录9172 小时前
Spring AI 高频开发万能 Prompt 合集 + 生产级工具类
java·人工智能·spring·prompt
INosdfgs2 小时前
HAProxy 入门:高性能开源负载均衡
运维·其他·开源·负载均衡
JAVA面经实录9172 小时前
如何选择适合项目的「限流 / 熔断 / 降级」方案
java·spring·kafka·sentinel·guava
曹牧13 小时前
Spring:@RequestMapping注解,匹配的顺序与上下文无关
java·后端·spring
Cry丶16 小时前
架构师实战:Spring Authorization Server 落地企业级“无感” SSO(附设计映射与源码级接口剖析)
spring·spring security·oauth2.0·authorization·sso·无感登录
敖正炀16 小时前
Spring 深度内核-核心容器与扩展机制-Spring 循环依赖终极剖析:三级缓存与 AOP 代理的纠缠
spring
超梦dasgg17 小时前
Spring AI 智能航空助手项目实战
java·人工智能·后端·spring·ai编程
敖正炀17 小时前
Spring 深度内核-核心容器与扩展机制-声明式事务的内部 AOP 实现:TransactionInterceptor 全解
spring
counting money17 小时前
Spring框架基础(配置篇)
java·后端·spring