Feign(OpenFeign)を使ってアノテーションだけでAPIクライアントを実装する

(久しぶりにJavaの話 :relaxed: )

# はじめに

みなさん複数のサーバで構成されているサービスってどのように通信してますかね。

今私が担当しているプロジェクトではSpringBootを使って、RESTっぽいAPIを各サーバ用意しています。
SpringBootでAPIを呼ぶときにはよく`RestTemplate`が使われると思うのですが、
パスとリクエスト・レスポンスモデルの対応を仕様書等から引っ張ってきたり、ソースコードが見られるなら`Controller`から判断してみたりと、何かとクライアント側では考慮することが多いです。（経験上

「`Controller`と同じようにクライアント側も書けたらものすごい楽なのに。。。」
「さらに`Controller`と同じインターフェースが使えたらメンテナンスももっともっと楽になるのに。。。」

それ`Feign(OpenFeign)`を使えばできますよ :sunglasses:

# Feign(OpenFeign)とは

https://github.com/OpenFeign/feign

> Feign makes writing java http clients easier

Feignは`Retrofit`や`JAXRS-2.0`、`WebSocket`からインスパイアされたHTTPクライアントためのフレームワークです。上記のように、より簡潔に書けることをウリにしています。

SpringBootで使用するにはSpringCloudの中の[Spring Cloud OpenFeign](https://github.com/spring-cloud/spring-cloud-openfeign)を使います。

※ 昔は`Feign`という名前のプロジェクトでしたが、Spring Cloudの`Finchley.M7`からは`OpenFeign`という別のプロジェクトが使われているので注意（使い方は前の時と一緒です）
[Spring Cloud Finchley.M7での変更内容](https://spring.io/blog/2018/02/27/spring-cloud-finchley-m7-has-been-released)

# 使い方
[Livedoorお天気Webサービス](http://weather.livedoor.com/weather_hacks/webservice)を使って天気情報を取得してみます:partly_sunny:

Java9、Maven(3.5.2)、SpringBoot(2.0.0.RELEASE)で動かします。

## Spring Cloud OpenFeignのインストール

`pom.xml`はこんな感じ。
`spring-cloud.version`に最新(2018/03/02時点)の`Finchley.M7`を指定します。
lombokは好みで

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example.ofc</groupId>
	<artifactId>open-feign-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>open-feign-client</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.0.RELEASE</version>
		<relativePath />
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>9</java.version>
		<spring-cloud.version>Finchley.M7</spring-cloud.version>
	</properties>

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

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>
	</dependencies>

</project>
```

## モデルの実装
簡略化するために、タイトルとディスクリプションのみを扱います。

```java:WeatherInfo.java
package com.example.ofc.model;

import lombok.Data;

@Data
public class WeatherInfo {
	private String title;
	private WeatherDescription description;
}
```

```java:WeatherDescription.java
package com.example.ofc.model;

import lombok.Data;

@Data
public class WeatherDescription {
	private String text;
	private String publicTime;
}
```

## クライアントの実装(?)

実装といいつつアノテーションでインターフェースを宣言するだけです。

サーバ側の`@RestController`と対になるのが`@FeignClient`です。
`@FeignClient`には名前とベースパスとなる`url`を渡します。
今回はハードコードしていますが、実際のコードではプロパティファイルに外出ししたほうが良いです。
そうすれば環境毎に違うパスを見に行くこともできます。

メソッドはコントローラの実装と同じように`@RequestMapping`や`@RequestParam`等をつけて宣言します。

```java:WeatherClient.java
package com.example.ofc.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.ofc.model.WeatherInfo;

@FeignClient(name = "weather", url = "weather.livedoor.com")
public interface WeatherClient {

	@RequestMapping(method = RequestMethod.GET, value = "/forecast/webservice/json/v1") 
	ResponseEntity<WeatherInfo> getWeatherInfo(@RequestParam("city") Long city);

}
```

## 起動クラス

上記のクライアントを`@Autowired`して`run`の中でAPIを呼びます。(今回は東京を指定)
それ以外は普通の起動クラスです。

```java:Application.java
package com.example.ofc;

import com.example.ofc.model.WeatherInfo;
import com.example.ofc.api.WeatherClient;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.http.ResponseEntity;

@SpringBootApplication
@EnableFeignClients
@EnableAutoConfiguration
@Slf4j
public class Application implements CommandLineRunner {

	@Autowired
	private WeatherClient weatherClient;

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Override
	public void run(String... strings) throws Exception {
		// 東京都: 130010
		ResponseEntity<WeatherInfo> response = weatherClient.getWeatherInfo(130010L);
		log.info(response.getBody().toString());
	}
}
```

起動するとログに天気情報が出力されます。

```
.
.
.
2018-03-02 16:00:22.536  INFO 8604 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'refreshScope': registering with JMX server as MBean [org.springframework.cloud.context.scope.refresh:name=refreshScope,type=RefreshScope]
2018-03-02 16:00:22.550  INFO 8604 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located managed bean 'configurationPropertiesRebinder': registering with JMX server as MBean [org.springframework.cloud.context.properties:name=configurationPropertiesRebinder,context=2098d877,type=ConfigurationPropertiesRebinder]
2018-03-02 16:00:22.582  INFO 8604 --- [           main] com.example.ofc.Application              : Started Application in 2.997 seconds (JVM running for 6.875)
2018-03-02 16:00:22.753  INFO 8604 --- [           main] com.example.ofc.Application              : WeatherInfo(title=東京都 東京 の天気, description=WeatherDescription(text= 日本付近は冬型の気圧配置
となっています。

【関東甲信地方】
 関東甲信地方はおおむね晴れていますが、長野県や関東地方北部の山沿い
では曇りで雪の降っている所があります。

 2日は、冬型の気圧配置は次第に緩み高気圧に覆われてくるため、おおむ
ね晴れますが、関東地方北部や長野県の山沿いでは、はじめ寒気の影響によ
り曇りで雪の降る所があるでしょう。

 3日は、高気圧に覆われるため、おおむね晴れる見込みです。

 関東近海では、2日はうねりを伴ってしける所があり、3日は波が高い所
があるでしょう。船舶は高波に注意してください。

【東京地方】
 2日から3日にかけて、晴れるでしょう。, publicTime=2018-03-02T10:40:00+0900))
 .
 .
 .
 ```

ほとんど実装せずにできちゃいました！ :yum:


# FeignClientとRestControllerで同一のインターフェースを使いたい場合

先ほどの例は外部のAPIでしたが、内部でのAPI通信のほうが多いのではないでしょうか。
はじめににも書きましたが、クライアント側のFeignClientとサーバ側のRestControllerで同一のインターフェースが使えたらいろいろと楽になりますよね。

そんなときには、クライアントではインターフェースを継承したFeignClientを、サーバではインターフェースを実装したRestControllerを作成すれば良いです。
すると、公開するのはインターフェース(とモデル)のみでよくなり、`@ApiResponse`等のアノテーションを使ってステータスコード等の情報も書けば仕様書もいらなくなります。(仕様書が欲しければSwaggerで自動作成する)

先ほどの天気情報を例にします。(import等は省略)

```java
public interface WeatherApi {
    @RequestMapping(method = RequestMethod.GET, value = "/forecast/webservice/json/v1")
    ResponseEntity<WeatherInfo> getWeatherInfo(@RequestParam("city") Long city);
}
```

```java
// urlの向き先は適当
@FeignClient(name = "weather", url="localhost:8888")
public interface WeatherApiClient extends WeatherApi {
}
```

```java
@RestController
public class WeatherController implements WeatherApi {

    @Override
    ResponseEntity<WeatherInfo> getWeatherInfo(@RequestParam("city") Long city) {
      // 天気情報作成...
      return responseEntity;
    }
}
```

なんて簡単なんでしょう！:dizzy_face:


# まとめ

Spring Cloud Open Feignを使ってApiクライアントを作成する方法を紹介しました。
実装量や可用性を考えたら積極的に使っていきたいですね。

今回のサンプルはこちら
https://github.com/totto357/open-feign-client-example