Spring Framework 6の新機能「HTTP Interface」を使ってRESTクライアントを作成してみた

こんにちは、情報システム部エンジニアの小坂です。
2022年11月にリリースされたSpring Framework 6から、新機能「HTTP Interface」で下記のようなインタフェースを定義することでシンプルにRESTクライアントを作成できるようになりました。

@HttpExchange(url = "/book")
public interface BookApiClientService {

    @GetExchange
    List<Book> listBooks();

    @GetExchange("/{id}")
    Book getBook(@PathVariable("id") Integer id);

}

アノテーションを使ってREST APIの呼び出し処理をシンプルに実現できるのでなかなか良さそうです。早速詳しく見ていきましょう。
Spring Bootで利用する場合、2022年11月にリリースされたバージョン3から対応しています。

SpringでRESTクライアントを実現する手段

Springでは、これまでRESTクライアントを実現する手段としてRestTemplate※とWebClientがありましたが、RestTemplateもWebClientも、開発者がRESTクライアントとしての実装を行う必要がありました。

今回登場した「HTTP Interface」を利用することで、実装周りをSpring Frameworkに任せて簡潔にRESTクライアントを準備できるようになります。

どういうことかというと。。。
RestTemplateやWebClientでRESTクライアントを実現する方法はいくつかありますが、オーソドックスに実現すると、下記のようにRESTクライアントの役割を持つインタフェースと実装クラスを作成し、インタフェースを呼び出して利用します。

「HTTP Interface」を利用すると、実装クラスの作成が不要になります。

「HTTP Interface」は内部では非同期処理のWebClientが使用されていますが、同期処理を行うことも可能です。

※RestTemplateはSpring Framework 5からメンテナンスモードに入っており、必要最小限の変更しか行われないため、WebClientへの移行が推奨されています。

HTTP Interfaceの使い方

HTTP Interfaceの使い方を見ていくために、簡単なAPIとそのAPIを利用するRESTクライアントを作成します。

今回作成するアプリ

RESTクライアントのアプリから、デモ用のAPIを呼び出してみます。
デモ用のAPIとして、次の機能を実装したものを用意します。

  • /bookへGETでアクセスすると本のリストを返却
  • /book/{id}へGETでアクセスすると対象の本の情報のみ返却

デモ用APIの内容は上記を実現したシンプルなもので、次のコードで準備しました。

@RestController
public class BookController {

    private record Book(Integer id, String name, Integer price) {};
    
    private List<Book> bookList = new ArrayList<Book>();
    {
        bookList.add(new Book(1,"foo book",1000));
        bookList.add(new Book(2,"bar book",1100));
        bookList.add(new Book(3,"baz book",1200));
    }
    
    
    @GetMapping(path = "/book")
    public List<Book> listBook() {
        
        return this.bookList;
    }

    @GetMapping(path = "/book/{id}")
    public Book getBook(@PathVariable("id") Integer id) {
        return this.bookList
                .stream()
                .filter(book -> book.id == id)
                .findFirst()
                .get();
    }

}

RESTクライアントアプリケーションの作成準備

まずはhttps://start.spring.ioへアクセスし、プロジェクトを作成します。 この時に、下記の条件で設定する必要があります。

  • Spring Bootのバージョンは3以上
  • Dependenciesに「Spring Reactive Web」を追加

「Spring Reactive Web」はSpring WebFluxのプロジェクトを作成する際に必要となるものですが、HTTP Interfaceの内部で利用されているWebClientが「Spring Reactive Web」に内包されています。

RESTクライアントのインタフェースを作成

クライアントのプロジェクトができたら、まずはインタフェースを作成します。 このインタフェースを定義することで、APIへのアクセスの実装はSpring Framework側でやってくれるようになります。

@HttpExchange(url = "/book")
public interface BookApiClientService {

    @GetExchange
    List<Book> listBooks();

    @GetExchange("/{id}")
    Book getBook(@PathVariable("id") Integer id);

}

今回からインタフェース用のアノテーション@HttpExchangeが追加されました。

@HttpExchangeとそのファミリーのアノテーションを使うことでRESTクライアントのインタフェースが定義されます。 直感的にもどのような役割のインタフェースか分かりやすいですね。

  • @HttpExchangeはインタフェース共通のエンドポイントの定義などができ、省略可能です。
  • @GetExchangeや@PostExchangeなど各HTTPメソッドに特化したアノテーションでHTTPメソッド毎にアノテーションが用意されています。

JavaConfigの設定

次に、作成したインタフェースをJavaConfigへ設定します。

今回はデモ用APIをlocalhost:8080で起動しています。
APIのエンドポイントの情報、デフォルトのヘッダ情報などを設定してHttpServiceProxyFactoryでProxyを生成し、Beanに登録します。
この設定により、インタフェースを介してRESTクライアントを利用できるようになります。

実装コードを書かずにインタフェース定義とJavaConfigの設定だけでRESTクライアントが作成できました。簡単ですね!

@Configuration
public class AppConfig {

    @Bean
    public BookApiClientService bookApiClientService(){
        
        WebClient client = WebClient.builder()
                .baseUrl("http://localhost:8080/")
//                .defaultHeader(HttpHeaders.AUTHORIZATION, "xxxx") ←ヘッダー定義も可能。
                .build();

        HttpServiceProxyFactory proxyFactory =
                HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();

        return proxyFactory.createClient(BookApiClientService.class);
    }
}

インタフェースの利用

あとは各プログラムでインタフェースを利用すれば、APIの呼び出しができます。
次の図のような構成で、APIを呼び出してみます。

@RestController
public class ClientSampleController {

    // インタフェースをDI
    private final BookApiClientService bookApiClientService;
    public ClientSampleController(BookApiClientService bookApiClientService) {
        this.bookApiClientService = bookApiClientService;
    } 

    // APIで取得したリストをそのまま返却
    @GetMapping(path = "/getList")
    public List<Book> getList() {
        return bookApiClientService.listBooks();
    }

    // APIで取得したBookオブジェクトをそのまま返却
    @GetMapping(path = "/get")
    public Book get(@RequestParam("id") Integer id) {
        return bookApiClientService.getBook(id);
    }
}

まずはリストを取得する/getListを実行してみます。今回はlocalhost上で8081ポートに起動したクライアントから8080ポートのAPIを呼び出しました。 ちゃんと返ってきました。

次に、指定したBookオブジェクトを取得します。 こちらも問題なく実行できました。

今回同期処理を行うクライアントを想定してインタフェースの各メソッドの戻り値をList<Book>Bookといった型で定義しました。 APIを別スレッドで実行する非同期処理を行いたい場合は下記のように戻り値にFluxMonoを使うことで非同期処理が実行できます。

@HttpExchange(url = "/book")
public interface BookApiClientService {


    @GetExchange
    Flux<Book> listBooks();

    @GetExchange("/{id}")
    Mono<Book> getBook(@PathVariable("id") Integer id);

}

また、HTTPヘッダだけあればいいケースや戻り値が不要ということもあるかと思います。その場合は、下記のように記載ができます。

   // HTTPヘッダだけあればいい場合
    @PostExchange
    HttpHeaders saveBook1(@RequestBody Book book);

    // 戻り値が不要な場合
    @PostExchange
    void saveBook2(@RequestBody Book book);

まとめ

「HTTP Interface」を導入することで、直感的でシンプルにRESTクライアントの作成ができることが分かりました。 カスタマイズも柔軟にできるので、用途に応じたRESTクライアントの準備ができそうです。

参考

Integration

Declarative Clients in Spring - YouTube

HttpExchange (Spring Framework 6.0.0 API)

一緒に働く仲間を募集しています

弥生では一緒に働く仲間を募集しています!
弥生で働くことに興味がありましたら、求人一覧をぜひご覧ください。

herp.careers