Edited at

インターフェースだけでAPIクライアントを実装するFeignがすごく便利!

More than 1 year has passed since last update.


1. Feignとは

Feignとは非常にシンプルなJavaのHTTPクライアント用ライブラリです。

(2018/4/2時点)ので、かなり使われているライブラリのようです。

とても短いコードでHTTPリクエストを発行することができます。Feignを利用する際の流れは以下のような感じです。

デメリットとしてはテキストベースのHTTPリクエストしか発行できない、HTTPレスポンスはBODYにしかアクセスできない等の制限があります。

(2018/4/8 追記)

Feignのデフォルトでは対応していませんが、別モジュールや独自の実装でバイナリのHTTPリクエストを扱えます。


2. ライブラリの用意

Feginは利用するコンポーネントを入れ替えることが可能な構成になっています。

今回はOkHttpJacksonLogbackを利用する構成とします。


pom.xml

<dependency>

<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>9.5.1</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>9.5.1</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>9.5.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>


3. APIのインターフェースを定義

呼び出したいAPIをインターフェースとして定義します。



  • @RequestLineで発行したいHTTPリクエストのHTTPメソッドとパスを設定

  • パスの一部を{変数名}でパラメータを参照できる

  • パラメータはメソッド引数に@Paramを付与して定義

  • HTTPヘッダに設定したい内容があれば@Headersで設定

今回は以下に示すAPIを呼び出す場合を考えてみたいと思います。

項番
パス
HTTPメソッド
説明

1
/todos
GET
全てのTODOを取得する

2
/todos/{todoId}
GET
todoIdで指定されたTODOを取得する

3
/todos
POST
送信されたデータでTODOを作成する
コンテントタイプはapplication/jsonとする

4
/todos/{todoId}
PUT
todoIdで指定されたTODOを更新する
コンテントタイプはapplication/jsonとする

5
/todos/{todoId}
DELETE
todoIdで指定されたTODOを削除する

APIのエンドポイント(URL)はhttp://localhost:8090/todo-rest/api/v1とします。


TodoApi.java

package com.example.feign.demo;

import java.util.List;

import feign.Headers;
import feign.Param;
import feign.RequestLine;

public interface TodoApi {

@RequestLine("GET /todos")
List<Todo> findAll();

@RequestLine("GET /todos/{todoId}")
Todo getTodo(@Param("todoId") String todoId);

@RequestLine("POST /todos")
@Headers("Content-Type: application/json")
Todo createTodo(Todo todo);

@RequestLine("PUT /todos/{todoId}")
@Headers("Content-Type: application/json")
Todo updateTodo(@Param("todoId") String todoId, Todo todo);

@RequestLine("DELETE /todos/{todoId}")
void deleteTodo(@Param("todoId") String todoId);
}



4. FeignでAPIのインスタンスを生成して実行

APIのインターフェースは定義しましたが、その実装クラスは定義していません。APIのインスタンスはFeignで生成します。

APIの呼び出しはインターフェースのメソッドを実行するだけです。


TodoApiDemo.java

package com.example.feign.demo;

import java.util.List;

import feign.Feign;
import feign.Logger;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.okhttp.OkHttpClient;
import feign.slf4j.Slf4jLogger;

public class TodoApiDemo {

public static void main(String[] args) {
// 1. create instance of api interface with feign
TodoApi todoApi = Feign.builder() // builder call at first
.client(new OkHttpClient()) // use OkHttpClient of feign
.encoder(new JacksonEncoder()) // use Jackson of feign
.decoder(new JacksonDecoder()) // use Jackson of feign
.logger(new Slf4jLogger()) // use Slf4j of feign
.logLevel(Logger.Level.FULL) // setting log level to most detail
.target(TodoApi.class,
"http://localhost:8090/todo-rest/api/v1");

// 2. call api [GET /todos]
List<Todo> todos = todoApi.findAll();
System.out.println(todos);
}
}


Builderパターンでインスタンスを生成するため、設定のための専用メソッドが用意されています。



  • clientメソッド


    • HTTPを発行する際に利用するクライアントを指定する


    • ApacheHttpClientOkHttpClient等がある




  • encoderメソッド


    • HTTPリクエストのBODYにデータを設定する際のコンバート処理(シリアライズ)で利用するオブジェクトを指定する


    • JacksonEncoderGsonEncoder等がある




  • decoderメソッド


    • HTTPレスポンスのBODYのデータをJavaのオブジェクトにコンバート処理(デシリアライズ)で利用するオブジェクトを指定する。


    • JacksonDecoderGsonDecoder等がある




  • loggerメソッド


    • ロガーを指定する


    • Feignはログ出力にSLF4Jを利用しているため、ログ出力の実装はSLF4Jに対応しているものであれば差し替え可能である。




  • logLevelメソッド


    • ログレベルを指定する

    • 最も詳細なログを出力したい場合、Logger.Level.FULLを指定する




  • targetメソッド


    • APIのインスタンス化に必要となる情報を指定する

    • 第1引数 : 3. APIのインターフェースを定義で定義したAPIのインターフェースのclass

    • 第2引数 : 実際にアクセスするAPIのエンドポイント(URL)




5. APIテストのサンプル(おまけ)

APIのインスタンスのメソッドを呼び出すだけなので、呼び出し方法に難しいことがありませんが、参考までにサンプルのAPIのテストを記載します。

Feignの使い方を目的としているため、テストの内容自体は適当です。


TodoApiTest.java

package com.example.feign.demo;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import java.util.Date;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import feign.Feign;
import feign.Logger;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.okhttp.OkHttpClient;
import feign.slf4j.Slf4jLogger;

public class TodoApiTest {

// test api
TodoApi todoApi;

private <T> T factory(Class<T> apiType, String url) {
return Feign.builder() // builder call at first
.client(new OkHttpClient()) // use OkHttpClient of feign
.encoder(new JacksonEncoder()) // use Jackson of feign
.decoder(new JacksonDecoder()) // use Jackson of feign
.logger(new Slf4jLogger()) // use Slf4j
.logLevel(Logger.Level.FULL) // setting log level to most detail
.target(apiType, url);
}

@Before
public void setUp() throws Exception {
todoApi = factory(TodoApi.class,
"http://localhost:8090/todo-rest/api/v1");
}

@Test
public void testFindAll() {
List<Todo> todos = todoApi.findAll();
System.out.println(todos);
assertThat(todos.size(), is(2));
}

@Test
public void testCreateTodo() {
Todo todo = new Todo();
todo.setTodoTitle("hello");
Todo registeredTodo = todoApi.createTodo(todo);
System.out.println(registeredTodo);
}

@Test
public void testGetTodo() {
String todoId = "36e06987-ef33-436a-a2c6-d215096ee902";
Todo todo = todoApi.getTodo(todoId);
System.out.println(todo);
assertThat(todo.getTodoId(), is(todoId));
}

@Test
public void testUpdateTodo() {
String todoId = "36e06987-ef33-436a-a2c6-d215096ee902";
String title = "update......";
Todo todo = new Todo();
todo.setTodoId(todoId);
todo.setTodoTitle(title);
todo.setCreatedAt(new Date());
Todo updatedTodo = todoApi.updateTodo(todoId, todo);
System.out.println(updatedTodo);
assertThat(updatedTodo.getTodoId(), is(todoId));
assertThat(updatedTodo.getTodoTitle(), is(title));
assertThat(updatedTodo.isFinished(), is(true));
}

@Test
public void testDeleteTodo() {
String todoId = "36e06987-ef33-436a-a2c6-d215096ee902";
todoApi.deleteTodo(todoId);
System.out.println("ok");
}
}



6. さいごに

今回はJavaのHTTPクライアント用ライブラリであるFeignについて説明しました。

ほぼAPIのインターフェースを定義するだけで実装が済んでしまうので、とても簡単に使えるライブラリではないでしょうか。

なお、今回は説明していませんがFeignJAXBJAX-RSにも対応しています。

テキストベースのHTTPリクエストしか扱えないという制約はありますが、その影響がないAPIでは積極的に利用していきたいと思いました。