spring-cloud-openfeign
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpring Cloud OpenFeign - Quick Reference
Spring Cloud OpenFeign - 快速参考
Deep Knowledge: Usewith technology:mcp__documentation__fetch_docsfor comprehensive documentation.spring-cloud-openfeign
深入学习:使用工具,指定技术为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: trueyaml
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: trueLogging
日志配置
logging:
level:
com.example.clients: DEBUG
undefinedlogging:
level:
com.example.clients: DEBUG
undefinedJava 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: 5syaml
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: 5sHeaders 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=20java
@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=20Testing
测试
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
最佳实践
| Do | Don't |
|---|---|
| Use service discovery names | Hardcode URLs |
| Configure timeouts | Use infinite timeouts |
| Implement fallbacks | Let failures cascade |
| Use error decoders | Ignore error responses |
| Add request interceptors | Duplicate 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-Pattern | Problem | Solution |
|---|---|---|
| No timeout configured | Hanging requests | Set connectTimeout, readTimeout |
| Missing error decoder | Swallowed errors | Implement ErrorDecoder |
| No circuit breaker | Cascading failures | Integrate Resilience4j |
| Blocking thread | Thread exhaustion | Use async or circuit breaker |
| Hardcoded URLs | Not using discovery | Use service name |
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 未配置超时 | 请求挂起 | 设置connectTimeout、readTimeout |
| 缺少错误解码器 | 错误被吞掉 | 实现ErrorDecoder |
| 未启用断路器 | 级联故障 | 集成Resilience4j |
| 阻塞线程 | 线程耗尽 | 使用异步或断路器 |
| 硬编码URL | 未使用服务发现 | 使用服务名称 |
Quick Troubleshooting
快速故障排查
| Problem | Diagnostic | Fix |
|---|---|---|
| Service not found | Check discovery | Verify Eureka registration |
| Timeout | Check timeout config | Increase or fix service |
| 404 on call | Check path | Verify @RequestMapping path |
| Serialization error | Check content type | Configure Jackson converter |
| Auth failing | Check interceptor | Add RequestInterceptor |
| 问题 | 诊断方法 | 修复方案 |
|---|---|---|
| 服务未找到 | 检查服务发现 | 验证Eureka注册状态 |
| 请求超时 | 检查超时配置 | 增加超时时间或修复服务 |
| 调用返回404 | 检查请求路径 | 验证@RequestMapping路径 |
| 序列化错误 | 检查内容类型 | 配置Jackson转换器 |
| 认证失败 | 检查拦截器 | 添加RequestInterceptor |