关于 spring-cloud-openfeign
面向构建服务客户端的开发者的 Spring Cloud OpenFeign 快速参考技能,涵盖客户端设置、配置、错误处理、回退及弹性模式。
主要功能
- 01启用并定义 Feign 客户端
- 02配置超时和日志
- 03添加请求与响应拦截器
- 04使用自定义解码器处理错误
- 05使用回退和断路器
- 06通过 WireMock 测试客户端
适用场景
- 01为服务发现的服务创建客户端
- 02设置对外部 API 的调用
- 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