spring-cloud-openfeign について
サービスクライアントを構築する開発者向けの Spring Cloud OpenFeign クイックリファレンススキルです。クライアント設定、エラー処理、フォールバック、耐障害性のパターンを扱います。
主な機能
- 01Feign クライアントの有効化と定義
- 02タイムアウトとログの設定
- 03リクエスト・レスポンスインターセプターの追加
- 04カスタムデコーダーによるエラー処理
- 05フォールバックとサーキットブレーカーの利用
- 06WireMock によるクライアントテスト
活用シーン
- 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 - クイックリファレンス
詳細な知識:包括的なドキュメントについては、技術を
spring-cloud-openfeignに指定してmcp__documentation__fetch_docsを使用してください。
依存関係
<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()
));
}
}
フォールバック
フォールバッククラスを使用
@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 をハードコードする |
| タイムアウトを設定する | 無限のタイムアウトを使用する |
| フォールバックを実装する | 障害を連鎖させる |
| エラーデコーダーを使用する | エラーレスポンスを無視する |
| リクエストインターセプターを追加する | 認証ロジックを重複させる |
本番環境のチェックリスト
- タイムアウトを設定済み
- サーキットブレーカーを有効化済み
- フォールバックを実装済み
- エラーデコーダーを設定済み
- ログレベルが適切
- 認証インターセプターを追加済み
- リトライポリシーを設定済み
- メトリクスを有効化済み
- ロードバランサーを設定済み
- コネクションプールを調整済み
このスキルを使用すべきでない場合
- 外部 API - WebClient、RestClient を検討してください
- リアクティブ処理 - 代わりに WebClient を使用してください
- 単純な呼び出し - RestClient のほうがシンプルな場合があります
- ファイルアップロード - カスタム設定が必要な場合があります
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| タイムアウト未設定 | リクエストが停止する | connectTimeout、readTimeout を設定する |
| エラーデコーダーの欠落 | エラーが握りつぶされる | ErrorDecoder を実装する |
| サーキットブレーカーなし | 障害が連鎖する | Resilience4j を統合する |
| スレッドをブロックする | スレッドが枯渇する | 非同期処理またはサーキットブレーカーを使用する |
| URL のハードコード | ディスカバリーを使用していない | サービス名を使用する |
トラブルシューティングクイックガイド
| 問題 | 診断 | 修正方法 |
|---|---|---|
| サービスが見つからない | ディスカバリーを確認する | Eureka の登録を確認する |
| タイムアウト | タイムアウト設定を確認する | 値を増やすか、サービスを修正する |
| 呼び出し時に 404 | パスを確認する | @RequestMapping のパスを確認する |
| シリアライズエラー | コンテンツタイプを確認する | Jackson コンバーターを設定する |
| 認証に失敗する | インターセプターを確認する | RequestInterceptor を追加する |
リファレンスドキュメント
コメント
Markdown 対応