mcpField
S
spring-cloud-openfeign

spring-cloud-openfeign

github.com/claude-dev-suite開発4650 回インストール2 日前に更新
リポジトリ

spring-cloud-openfeign について

サービスクライアントを構築する開発者向けの Spring Cloud OpenFeign クイックリファレンススキルです。クライアント設定、エラー処理、フォールバック、耐障害性のパターンを扱います。

主な機能

  1. 01Feign クライアントの有効化と定義
  2. 02タイムアウトとログの設定
  3. 03リクエスト・レスポンスインターセプターの追加
  4. 04カスタムデコーダーによるエラー処理
  5. 05フォールバックとサーキットブレーカーの利用
  6. 06WireMock によるクライアントテスト

活用シーン

  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 - クイックリファレンス

詳細な知識:包括的なドキュメントについては、技術を 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 を追加する

リファレンスドキュメント

コメント