1. Feignとは
Feignとは非常にシンプルなJavaのHTTPクライアント用ライブラリです。
githubで
2745の評価(2018/4/2時点)ので、かなり使われているライブラリのようです。
とても短いコードでHTTPリクエストを発行することができます。Feignを利用する際の流れは以下のような感じです。
デメリットとしてはテキストベースのHTTPリクエストしか発行できない、HTTPレスポンスはBODYにしかアクセスできない等の制限があります。
(2018/4/8 追記)
Feignのデフォルトでは対応していませんが、別モジュールや独自の実装でバイナリのHTTPリクエストを扱えます。
2. ライブラリの用意
Feginは利用するコンポーネントを入れ替えることが可能な構成になっています。
今回はOkHttp、Jackson、Logbackを利用する構成とします。
<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とします。
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の呼び出しはインターフェースのメソッドを実行するだけです。
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を発行する際に利用するクライアントを指定する
-
ApacheHttpClient、OkHttpClient等がある -
encoderメソッド - HTTPリクエストのBODYにデータを設定する際のコンバート処理(シリアライズ)で利用するオブジェクトを指定する
-
JacksonEncoder、GsonEncoder等がある -
decoderメソッド - HTTPレスポンスのBODYのデータをJavaのオブジェクトにコンバート処理(デシリアライズ)で利用するオブジェクトを指定する。
-
JacksonDecoder、GsonDecoder等がある -
loggerメソッド - ロガーを指定する
-
Feignはログ出力にSLF4Jを利用しているため、ログ出力の実装はSLF4Jに対応しているものであれば差し替え可能である。 -
logLevelメソッド - ログレベルを指定する
- 最も詳細なログを出力したい場合、
Logger.Level.FULLを指定する -
targetメソッド - APIのインスタンス化に必要となる情報を指定する
- 第1引数 : 3. APIのインターフェースを定義で定義したAPIのインターフェースの
class - 第2引数 : 実際にアクセスするAPIのエンドポイント(URL)
5. APIテストのサンプル(おまけ)
APIのインスタンスのメソッドを呼び出すだけなので、呼び出し方法に難しいことがありませんが、参考までにサンプルのAPIのテストを記載します。
Feignの使い方を目的としているため、テストの内容自体は適当です。
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のインターフェースを定義するだけで実装が済んでしまうので、とても簡単に使えるライブラリではないでしょうか。
なお、今回は説明していませんがFeignはJAXBやJAX-RSにも対応しています。
テキストベースのHTTPリクエストしか扱えないという制約はありますが、その影響がないAPIでは積極的に利用していきたいと思いました。