外部APIを呼び出すコードを書いていると、RestTemplateのボイラープレートにうんざりすることありませんか?HttpHeadersを作って、HttpEntityで包んで、exchange()を呼んで……という繰り返し。マイクロサービス構成で複数サービスを呼ぶ場合は特に顕著ですよね。

そこで使いたいのが OpenFeign です。インターフェースを定義するだけでHTTPクライアントが完成するので、呼び出し側のコードが劇的にすっきりします。この記事では依存追加から実際の実装、エラーハンドリング、本番に必要な設定まで一通り紹介します。

OpenFeignとは

OpenFeign (spring-cloud-openfeign)は、Netflix Feignを元にしたSpring Cloud統合ライブラリです。Javaのインターフェースに@FeignClientを付け、Spring MVCのアノテーション(@GetMappingなど)でメソッドを定義するだけでHTTPクライアントが自動生成されます。

RestTemplateは命令的(どう呼ぶかをコードで記述)、WebClientはリアクティブ対応、OpenFeignは 宣言的(何を呼ぶかをインターフェースで記述) というのが大きな違いです。同期のREST呼び出しが中心なら、OpenFeignが最もコードをシンプルに保てます。RestTemplate・WebClientとの詳しい比較はこちらの記事も参考にしてください。

依存関係の追加

本記事はSpring Boot 3.2.x以上を前提としています。

Mavenの場合はSpring Cloud BOMでバージョンを管理するのが定石です。

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>2023.0.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
</dependencies>

Gradleの場合はio.spring.dependency-managementプラグインが必要です。dependencyManagementブロックはこのプラグインがないと動作しないので忘れずに追加してください。

plugins {
  id 'org.springframework.boot' version '3.2.5'
  id 'io.spring.dependency-management' version '1.1.4'
  id 'java'
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.3"
  }
}

dependencies {
  implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

次にメインクラスへ@EnableFeignClientsを追加します。これを忘れると@FeignClientインターフェースがBeanとして登録されないので注意です。

@SpringBootApplication
@EnableFeignClients
public class MyApplication {
  public static void main(String[] args) {
    SpringApplication.run(MyApplication.class, args);
  }
}

スキャン対象を絞りたい場合は@EnableFeignClients(basePackages = "com.example.client")のように指定できます。

@FeignClientインターフェースの基本定義

最小構成はこんな感じです。

@FeignClient(name = "user-service", url = "${external.api.url}")
public interface UserClient {

  @GetMapping("/users/{id}")
  UserResponse getUser(@PathVariable("id") Long id);
}

nameはBean名として使われます。urlapplication.ymlで外部化しておくと環境ごとに切り替えやすいです。@Componentを付けなくてもSpring Beanとして自動登録されるので、あとはコンストラクタインジェクションで使えます。

GETリクエストの実装

パスパラメータとクエリパラメータの典型パターンを見てみましょう。

@FeignClient(name = "product-service", url = "${product.api.url}")
public interface ProductClient {

  @GetMapping("/products/{id}")
  ProductResponse getProduct(@PathVariable("id") Long id);

  @GetMapping("/products")
  List<ProductResponse> searchProducts(
      @RequestParam("category") String category,
      @RequestParam("page") int page);

  @GetMapping("/products/{id}")
  ResponseEntity<ProductResponse> getProductWithStatus(@PathVariable("id") Long id);
}

レスポンスボディはJacksonで自動デシリアライズされます。ステータスコードも取得したい場合はResponseEntity<T>で返すといいです。

POSTリクエストの実装

JSONボディを送るPOSTはこのように書きます。

@FeignClient(name = "order-service", url = "${order.api.url}")
public interface OrderClient {

  @PostMapping("/orders")
  OrderResponse createOrder(@RequestBody CreateOrderRequest request);

  @PostMapping("/orders")
  OrderResponse createOrderWithHeader(
      @RequestHeader("X-Request-Id") String requestId,
      @RequestBody CreateOrderRequest request);
}

Content-Type: application/jsonは自動で設定されます。任意のHTTPヘッダーを追加したい場合は@RequestHeaderを使いましょう。

FeignExceptionを使ったエラーハンドリング

OpenFeignはデフォルトで4xx・5xxレスポンスを受け取るとFeignExceptionをスローします。基本的なtry-catchはこのようになります。

@Service
public class OrderService {

  private final OrderClient orderClient;

  public OrderResponse createOrder(CreateOrderRequest request) {
    try {
      return orderClient.createOrder(request);
    } catch (FeignException.BadRequest e) {
      throw new IllegalArgumentException("リクエストが不正です");
    } catch (FeignException e) {
      if (e.status() >= 500) {
        throw new ServiceUnavailableException("外部サービスエラー: " + e.status()); // カスタム例外クラス
      }
      throw e;
    }
  }
}

ステータスコードは e.status()FeignException#status())で取得できます。@ExceptionHandlerとの連携については例外ハンドリングの記事も参考にしてください。

カスタムErrorDecoderでエラー処理を共通化する

複数のFeignClientでエラー処理を統一したい場合はカスタムErrorDecoderが便利です。

public class FeignErrorDecoder implements ErrorDecoder {

  private final ErrorDecoder defaultDecoder = new Default();

  @Override
  public Exception decode(String methodKey, Response response) {
    return switch (response.status()) {
      case 404 -> new ResourceNotFoundException("リソースが見つかりません"); // カスタム例外クラス
      case 403 -> new ForbiddenException("アクセス権限がありません");         // カスタム例外クラス
      case 503 -> new ServiceUnavailableException("サービスが利用できません"); // カスタム例外クラス
      default -> defaultDecoder.decode(methodKey, response);
    };
  }
}

このクラスは後述のConfigクラスで@Bean登録します。

タイムアウトとログの設定

application.ymlでタイムアウトとログレベルをまとめて設定できます。

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connect-timeout: 3000
            read-timeout: 10000
          order-service:
            connect-timeout: 2000
            read-timeout: 5000

logging:
  level:
    com.example.client: DEBUG

defaultでデフォルト値を設定し、name属性の値で個別に上書きできます。タイムアウト超過時はRetryableExceptionがスローされますが、デフォルトのRetryer.NEVER_RETRYではリトライは行われず、そのまま呼び出し元に伝播します。

ログレベルとErrorDecoderはJavaのConfigクラスで@Bean登録します。

@Configuration
public class FeignConfig {

  @Bean
  public Logger.Level feignLoggerLevel() {
    // 本番はBASIC、開発中はFULL
    return Logger.Level.BASIC;
  }

  @Bean
  public ErrorDecoder errorDecoder() {
    return new FeignErrorDecoder();
  }
}

このFeignConfigクラスがコンポーネントスキャン対象にある場合、全FeignClientにグローバル適用されます。特定クライアントにのみ適用したい場合は@FeignClient(configuration = FeignConfig.class)で個別指定し、クラス自体には@Configurationを付けないようにしましょう。

ログレベルはNONE(デフォルト)、BASIC(リクエスト・レスポンス概要)、HEADERS(ヘッダー含む)、FULL(ボディも含む全情報)の4段階です。logging.levelでDEBUG以上を設定することで出力されます。

まとめ

OpenFeignを使うと、RestTemplateの命令的な記述がインターフェース定義に置き換わり、コードが大幅に整理されます。特にマイクロサービスで複数サービスを呼び出す場面で効果が高いです。

WebFlux・リアクティブが必要な場合はWebClientを選択しましょう。サーキットブレーカーやリトライが必要になったらResilience4j連携を検討してください。OpenFeignのテスト方法についてはWireMockを使った外部APIテストの記事も合わせてどうぞ。