[JAVA] Implement API client with only annotations using Feign (OpenFeign)

(Java story after a long time: relaxed :)

Introduction

How do you communicate with a service that consists of multiple servers?

The project I'm in charge of right now uses Spring Boot to provide REST-like APIs for each server. I think that RestTemplate is often used when calling API in Spring Boot, On the client side, there are many things to consider, such as pulling the correspondence between the path and the request / response model from the specifications, or judging from the Controller if the source code can be seen. (Experience

"It would be really easy if I could write on the client side as well as Controller ... " "Furthermore, if the same interface as Controller can be used, maintenance will be much easier ... "

You can do that with Feign (OpenFeign): sunglasses:

What is Feign (Open Feign)?

https://github.com/OpenFeign/feign

Feign makes writing java http clients easier

Feign is a framework for HTTP clients inspired by Retrofit, JAXRS-2.0, and WebSocket. As mentioned above, I am trying to write more concisely.

To use it with Spring Boot, use Spring Cloud OpenFeign in Spring Cloud.

How to use

Try to get the weather information using Livedoor Weather Web Service: partly_sunny:

It runs on Java9, Maven (3.5.2), SpringBoot (2.0.0.RELEASE).

Install Spring Cloud Open Feign

pom.xml looks like this. Specify the latest (as of 03/02/2018) Finchley.M7 for spring-cloud.version. lombok is your choice

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>

Model implementation

For simplicity, we'll only deal with titles and descriptions.

WeatherInfo.java


package com.example.ofc.model;

import lombok.Data;

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

WeatherDescription.java


package com.example.ofc.model;

import lombok.Data;

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

Client implementation (?)

Just declare the interface with annotations while calling it an implementation.

@FeignClient is paired with @ RestController on the server side. Pass the name and base path ʻurl to @FeignClient`. This time it's hard code, but in the actual code it's better to go out to the properties file. Then you can go to see different paths for each environment.

The method is declared with @RequestMapping, @RequestParam, etc. as in the controller implementation.

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);

}

Startup class

Call the API in run by @Autowired the above client. (This time, specify Tokyo) Other than that, it is a normal startup class.

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 {
		//Tokyo: 130010
		ResponseEntity<WeatherInfo> response = weatherClient.getWeatherInfo(130010L);
		log.info(response.getBody().toString());
	}
}

When started, the weather information will be output to the log.

.
.
.
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=Tokyo Tokyo weather, description=WeatherDescription(text=Winter-type pressure distribution around Japan
It has become.

[Kanto Koshin region]
The Kanto Koshin region is generally sunny, but along the mountains in Nagano prefecture and the northern part of the Kanto region.
Then there are places where it is cloudy and snowy.

On the 2nd, the winter-type pressure distribution gradually loosens and becomes covered with high pressure, so it is generally
It will be fine, but in the northern part of the Kanto region and along the mountains in Nagano prefecture, due to the influence of cold air at first
There will be places where it will be cloudy and snowy.

It is expected to be mostly sunny on the 3rd due to the high pressure.

In the sea near Kanto, there are places where swells can occur on the 2nd, and where the waves are high on the 3rd.
There will be. Beware of high waves on ships.

[Tokyo District]
It will be fine from 2 to 3 days., publicTime=2018-03-02T10:40:00+0900))
 .
 .
 .

I could do it with almost no implementation! : yum:

If you want to use the same interface for FeignClient and RestController

The previous example was an external API, but I think there is more internal API communication. As I wrote at the beginning, it would be easier if the same interface could be used for FeignClient on the client side and RestController on the server side.

In such a case, you can create a FeignClient that inherits the interface on the client and a RestController that implements the interface on the server. Then, only the interface (and model) needs to be published, and if you write the information such as the status code using annotations such as @ ApiResponse, you do not need the specifications. (If you want a specification, Swagger will create it automatically)

Take the weather information above as an example. (Import etc. omitted)

public interface WeatherApi {
    @RequestMapping(method = RequestMethod.GET, value = "/forecast/webservice/json/v1")
    ResponseEntity<WeatherInfo> getWeatherInfo(@RequestParam("city") Long city);
}
//The destination of the url is appropriate
@FeignClient(name = "weather", url="localhost:8888")
public interface WeatherApiClient extends WeatherApi {
}
@RestController
public class WeatherController implements WeatherApi {

    @Override
    ResponseEntity<WeatherInfo> getWeatherInfo(@RequestParam("city") Long city) {
      //Weather information creation...
      return responseEntity;
    }
}

How easy it is! : dizzy_face:

Summary

I showed you how to create an API client using Spring Cloud Open Feign. Considering the amount of implementation and availability, I would like to actively use it.

Click here for this sample https://github.com/totto357/open-feign-client-example

Recommended Posts

Implement API client with only annotations using Feign (OpenFeign)
Retry with Feign (OpenFeign)
[Java EE] Implement Client with WebSocket
Feign, which implements an API client with just an interface, is very convenient!
API integration from Java with Jersey Client
Interact with LINE Message API using Lambda (Java)
Implement search function with Rakuten Books (DVD) API