spring-cloud-openfeign

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Spring Cloud OpenFeign - Quick Reference

Spring Cloud OpenFeign - 快速参考

Deep Knowledge: Use
mcp__documentation__fetch_docs
with technology:
spring-cloud-openfeign
for comprehensive documentation.
深入学习:使用
mcp__documentation__fetch_docs
工具,指定技术为
spring-cloud-openfeign
以获取完整文档。

Dependencies

依赖

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- For load balancing -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
xml
<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>

Enable Feign Clients

启用Feign客户端

java
@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")
java
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

// 或扫描指定包
@EnableFeignClients(basePackages = "com.example.clients")

Basic Feign Client

基础Feign客户端

java
@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);
}
java
@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);
}

Feign with URL (No Service Discovery)

带URL的Feign客户端(无需服务发现)

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

    @GetMapping("/data")
    DataResponse getData(@RequestHeader("Authorization") String token);
}
java
@FeignClient(name = "external-api", url = "${external.api.url}")
public interface ExternalApiClient {

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

Configuration

配置

application.yml

application.yml

yaml
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
yaml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:  # 应用于所有客户端
            connect-timeout: 5000
            read-timeout: 10000
            logger-level: BASIC

          user-service:  # 指定客户端
            connect-timeout: 3000
            read-timeout: 5000
            logger-level: FULL

      circuitbreaker:
        enabled: true

      micrometer:
        enabled: true

Logging

日志配置

logging: level: com.example.clients: DEBUG
undefined
logging: level: com.example.clients: DEBUG
undefined

Java Configuration

Java配置

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 { }
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);
    }
}

// 应用于指定客户端
@FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserClient { }

Request/Response Interceptors

请求/响应拦截器

Request Interceptor

请求拦截器

java
@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"));
    }
}
java
@Component
public class AuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 为所有请求添加认证头
        String token = SecurityContextHolder.getContext()
            .getAuthentication().getCredentials().toString();
        template.header("Authorization", "Bearer " + token);

        // 添加关联ID
        template.header("X-Correlation-Id", MDC.get("correlationId"));
    }
}

Client-Specific Interceptor

客户端专属拦截器

java
@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);
        };
    }
}
java
@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);
        };
    }
}

Error Handling

错误处理

Custom Error Decoder

自定义错误解码器

java
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 "";
        }
    }
}
java
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(
                    "资源未找到: " + methodKey);
            case BAD_REQUEST:
                return new BadRequestException(
                    "请求错误: " + getBody(response));
            case UNAUTHORIZED:
                return new UnauthorizedException("未授权");
            case SERVICE_UNAVAILABLE:
                return new ServiceUnavailableException(
                    "服务不可用");
            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 "";
        }
    }
}

Global Exception Handler

全局异常处理器

java
@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()
            ));
    }
}
java
@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(),
                "下游服务错误",
                e.getMessage()
            ));
    }
}

Fallback

降级处理

With Fallback Class

使用降级类

java
@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();
    }
}
java
@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("未知用户")
            .status("FALLBACK")
            .build();
    }
}

With FallbackFactory (Access to Exception)

使用降级工厂(可获取异常信息)

java
@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);
            }
        };
    }
}
java
@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("用户{}触发降级: {}", id, cause.getMessage());

                if (cause instanceof FeignException.ServiceUnavailable) {
                    return UserResponse.cached(id);  // 返回缓存版本
                }

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

Circuit Breaker Integration

断路器集成

With Resilience4j

与Resilience4j集成

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

Headers and Parameters

请求头与参数

java
@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);
}
java
@FeignClient(name = "api-service")
public interface ApiClient {

    // 路径变量
    @GetMapping("/items/{id}")
    Item getItem(@PathVariable("id") String id);

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

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

    // 请求体
    @PostMapping("/items")
    Item createItem(@RequestBody CreateItemRequest request);

    // 表单数据
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    void uploadFile(@RequestPart("file") MultipartFile file);

    // 多部分表单
    @PostMapping(value = "/submit", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    void submitForm(@RequestBody Map<String, ?> formData);
}

Query Map

查询参数映射

java
@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
java
@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;
}

// 使用示例
SearchCriteria criteria = new SearchCriteria();
criteria.setQuery("test");
criteria.setPage(0);
criteria.setSize(20);
searchClient.search(criteria);
// 生成请求: /search?query=test&page=0&size=20

Testing

测试

Mock with WireMock

使用WireMock模拟

java
@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": "john@example.com"}
                """)));

        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);
    }
}
java
@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": "john@example.com"}
                """)));

        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);
    }
}

Best Practices

最佳实践

DoDon't
Use service discovery namesHardcode URLs
Configure timeoutsUse infinite timeouts
Implement fallbacksLet failures cascade
Use error decodersIgnore error responses
Add request interceptorsDuplicate auth logic
建议不建议
使用服务发现名称硬编码URL
配置超时时间使用无限超时
实现降级逻辑让错误级联传播
使用错误解码器忽略错误响应
添加请求拦截器重复编写认证逻辑

Production Checklist

生产环境检查清单

  • Timeouts configured
  • Circuit breaker enabled
  • Fallbacks implemented
  • Error decoder configured
  • Logging level appropriate
  • Auth interceptor added
  • Retry policy set
  • Metrics enabled
  • Load balancer configured
  • Connection pool tuned
  • 已配置超时时间
  • 已启用断路器
  • 已实现降级逻辑
  • 已配置错误解码器
  • 日志级别设置合理
  • 已添加认证拦截器
  • 已设置重试策略
  • 已启用指标监控
  • 已配置负载均衡
  • 已调优连接池

When NOT to Use This Skill

不适用场景

  • External APIs - Consider WebClient, RestClient
  • Reactive - Use WebClient instead
  • Simple calls - RestClient may be simpler
  • File uploads - May need custom config
  • 外部API——建议使用WebClient、RestClient
  • 响应式场景——使用WebClient替代
  • 简单HTTP调用——RestClient可能更简单
  • 文件上传——可能需要自定义配置

Anti-Patterns

反模式

Anti-PatternProblemSolution
No timeout configuredHanging requestsSet connectTimeout, readTimeout
Missing error decoderSwallowed errorsImplement ErrorDecoder
No circuit breakerCascading failuresIntegrate Resilience4j
Blocking threadThread exhaustionUse async or circuit breaker
Hardcoded URLsNot using discoveryUse service name
反模式问题解决方案
未配置超时请求挂起设置connectTimeout、readTimeout
缺少错误解码器错误被吞掉实现ErrorDecoder
未启用断路器级联故障集成Resilience4j
阻塞线程线程耗尽使用异步或断路器
硬编码URL未使用服务发现使用服务名称

Quick Troubleshooting

快速故障排查

ProblemDiagnosticFix
Service not foundCheck discoveryVerify Eureka registration
TimeoutCheck timeout configIncrease or fix service
404 on callCheck pathVerify @RequestMapping path
Serialization errorCheck content typeConfigure Jackson converter
Auth failingCheck interceptorAdd RequestInterceptor
问题诊断方法修复方案
服务未找到检查服务发现验证Eureka注册状态
请求超时检查超时配置增加超时时间或修复服务
调用返回404检查请求路径验证@RequestMapping路径
序列化错误检查内容类型配置Jackson转换器
认证失败检查拦截器添加RequestInterceptor

Reference Documentation

参考文档