mcpField
S
spring-cloud-openfeign

spring-cloud-openfeign

github.com/claude-dev-suite开发4650 次安装2 天前更新
代码仓库

关于 spring-cloud-openfeign

面向构建服务客户端的开发者的 Spring Cloud OpenFeign 快速参考技能,涵盖客户端设置、配置、错误处理、回退及弹性模式。

主要功能

  1. 01启用并定义 Feign 客户端
  2. 02配置超时和日志
  3. 03添加请求与响应拦截器
  4. 04使用自定义解码器处理错误
  5. 05使用回退和断路器
  6. 06通过 WireMock 测试客户端

适用场景

  1. 01为服务发现的服务创建客户端
  2. 02设置对外部 API 的调用
  3. 03为服务调用添加故障处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill spring-cloud-openfeign

安装后在 Claude Code 中输入/spring-cloud-openfeign确认已加载。

详细介绍

Spring Cloud OpenFeign — 快速参考

深入了解:使用 mcp__documentation__fetch_docs 并将 technology 设为 spring-cloud-openfeign,获取完整文档。

依赖

<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 客户端

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

// Or scan specific packages
@EnableFeignClients(basePackages = "com.example.clients")

基本 Feign 客户端

@FeignClient(name = "user-service")
public interface UserClient {

    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable("id") Long id);

    @GetMapping("/api/users")
    List<UserResponse> getAllUsers();

    @GetMapping("/api/users")
    List<UserResponse> getUsersByStatus(@RequestParam("status") String status);

    @PostMapping("/api/users")
    UserResponse createUser(@RequestBody CreateUserRequest request);

    @PutMapping("/api/users/{id}")
    UserResponse updateUser(@PathVariable("id") Long id, @RequestBody UpdateUserRequest request);

    @DeleteMapping("/api/users/{id}")
    void deleteUser(@PathVariable("id") Long id);
}

使用 URL 的 Feign(无服务发现)

@FeignClient(name = "external-api", url = "${external.api.url}")
public interface ExternalApiClient {

    @GetMapping("/data")
    DataResponse getData(@RequestHeader("Authorization") String token);
}

配置

application.yml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:  # Apply to all clients
            connect-timeout: 5000
            read-timeout: 10000
            logger-level: BASIC

          user-service:  # Specific client
            connect-timeout: 3000
            read-timeout: 5000
            logger-level: FULL

      circuitbreaker:
        enabled: true

      micrometer:
        enabled: true

# Logging
logging:
  level:
    com.example.clients: DEBUG
Java 配置
@Configuration
public class FeignConfig {

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;  // NONE, BASIC, HEADERS, FULL
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }

    @Bean
    public Retryer retryer() {
        return new Retryer.Default(100, 1000, 3);
    }

    @Bean
    public Request.Options options() {
        return new Request.Options(5, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true);
    }
}

// Apply to specific client
@FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserClient { }

请求/响应拦截器

请求拦截器
@Component
public class AuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // Add auth header to all requests
        String token = SecurityContextHolder.getContext()
            .getAuthentication().getCredentials().toString();
        template.header("Authorization", "Bearer " + token);

        // Add correlation ID
        template.header("X-Correlation-Id", MDC.get("correlationId"));
    }
}
客户端专用拦截器
@FeignClient(
    name = "payment-service",
    configuration = PaymentClientConfig.class
)
public interface PaymentClient { }

@Configuration
public class PaymentClientConfig {

    @Bean
    public RequestInterceptor paymentAuthInterceptor() {
        return template -> {
            template.header("X-Api-Key", apiKey);
        };
    }
}

错误处理

自定义错误解码器
public class CustomErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

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

        switch (status) {
            case NOT_FOUND:
                return new ResourceNotFoundException(
                    "Resource not found: " + methodKey);
            case BAD_REQUEST:
                return new BadRequestException(
                    "Bad request: " + getBody(response));
            case UNAUTHORIZED:
                return new UnauthorizedException("Unauthorized");
            case SERVICE_UNAVAILABLE:
                return new ServiceUnavailableException(
                    "Service unavailable");
            default:
                return defaultDecoder.decode(methodKey, response);
        }
    }

    private String getBody(Response response) {
        try {
            return Util.toString(response.body().asReader(StandardCharsets.UTF_8));
        } catch (Exception e) {
            return "";
        }
    }
}
全局异常处理器
@ControllerAdvice
public class FeignExceptionHandler {

    @ExceptionHandler(FeignException.class)
    public ResponseEntity<ErrorResponse> handleFeignException(FeignException e) {
        HttpStatus status = HttpStatus.valueOf(e.status());

        return ResponseEntity
            .status(status)
            .body(new ErrorResponse(
                status.value(),
                "Downstream service error",
                e.getMessage()
            ));
    }
}

Fallback

使用 Fallback 类
@FeignClient(
    name = "user-service",
    fallback = UserClientFallback.class
)
public interface UserClient {
    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable Long id);
}

@Component
public class UserClientFallback implements UserClient {

    @Override
    public UserResponse getUserById(Long id) {
        return UserResponse.builder()
            .id(id)
            .name("Unknown User")
            .status("FALLBACK")
            .build();
    }
}
使用 FallbackFactory(可访问异常)
@FeignClient(
    name = "user-service",
    fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
    @GetMapping("/api/users/{id}")
    UserResponse getUserById(@PathVariable Long id);
}

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public UserResponse getUserById(Long id) {
                log.error("Fallback triggered for user {}: {}", id, cause.getMessage());

                if (cause instanceof FeignException.ServiceUnavailable) {
                    return UserResponse.cached(id);  // Return cached version
                }

                return UserResponse.unknown(id);
            }
        };
    }
}

熔断器集成

使用 Resilience4j
spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10000
        permittedNumberOfCallsInHalfOpenState: 3
    instances:
      user-service:
        baseConfig: default
        failureRateThreshold: 30

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

请求头和参数

@FeignClient(name = "api-service")
public interface ApiClient {

    // Path variable
    @GetMapping("/items/{id}")
    Item getItem(@PathVariable("id") String id);

    // Query parameters
    @GetMapping("/items")
    List<Item> searchItems(
        @RequestParam("q") String query,
        @RequestParam(value = "page", defaultValue = "0") int page,
        @RequestParam(value = "size", defaultValue = "20") int size);

    // Headers
    @GetMapping("/secure/data")
    Data getData(
        @RequestHeader("Authorization") String auth,
        @RequestHeader("X-Request-Id") String requestId);

    // Request body
    @PostMapping("/items")
    Item createItem(@RequestBody CreateItemRequest request);

    // Form data
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    void uploadFile(@RequestPart("file") MultipartFile file);

    // Multiple parts
    @PostMapping(value = "/submit", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    void submitForm(@RequestBody Map<String, ?> formData);
}

查询映射

@FeignClient(name = "search-service")
public interface SearchClient {

    @GetMapping("/search")
    SearchResult search(@SpringQueryMap SearchCriteria criteria);
}

@Data
public class SearchCriteria {
    private String query;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String sortOrder;
}

// Usage
SearchCriteria criteria = new SearchCriteria();
criteria.setQuery("test");
criteria.setPage(0);
criteria.setSize(20);
searchClient.search(criteria);
// Generates: /search?query=test&page=0&size=20

测试

使用 WireMock 进行模拟
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class UserClientTest {

    @Autowired
    private UserClient userClient;

    @Test
    void shouldGetUser() {
        stubFor(get(urlPathEqualTo("/api/users/1"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {"id": 1, "name": "John", "email": "[email protected]"}
                """)));

        UserResponse user = userClient.getUserById(1L);

        assertThat(user.getName()).isEqualTo("John");
    }

    @Test
    void shouldHandleError() {
        stubFor(get(urlPathEqualTo("/api/users/999"))
            .willReturn(aResponse().withStatus(404)));

        assertThatThrownBy(() -> userClient.getUserById(999L))
            .isInstanceOf(ResourceNotFoundException.class);
    }
}

最佳实践

应做不应做
使用服务发现名称硬编码 URL
配置超时使用无限超时
实现 Fallback任由故障级联
使用错误解码器忽略错误响应
添加请求拦截器重复编写身份验证逻辑

生产环境检查清单

  • 已配置超时
  • 已启用熔断器
  • 已实现 Fallback
  • 已配置错误解码器
  • 日志级别适当
  • 已添加身份验证拦截器
  • 已设置重试策略
  • 已启用指标
  • 已配置负载均衡器
  • 已调整连接池

不适合使用此技能的情况

  • 外部 API — 可考虑使用 WebClient、RestClient
  • 响应式场景 — 改用 WebClient
  • 简单调用 — RestClient 可能更简单
  • 文件上传 — 可能需要自定义配置

反模式

反模式问题解决方案
未配置超时请求挂起设置 connectTimeout、readTimeout
缺少错误解码器错误被吞掉实现 ErrorDecoder
无熔断器故障级联集成 Resilience4j
阻塞线程线程耗尽使用异步处理或熔断器
硬编码 URL未使用服务发现使用服务名称

快速故障排查

问题诊断修复
找不到服务检查服务发现确认已在 Eureka 注册
超时检查超时配置增加超时时间或修复服务
调用时返回 404检查路径确认 @RequestMapping 路径
序列化错误检查内容类型配置 Jackson 转换器
身份验证失败检查拦截器添加 RequestInterceptor

参考文档

评论

支持 Markdown