/ Spring Cloud  

Spring Cloud 5: Declarative REST Client - Feign

In the previous blog, we can find that when we call the API through RestTemplate, the parameters must be spliced in the request URL. If there are only a few parameters, we may still be able to tolerate it. Once there are multiple parameters, then splicing the request string would be inefficient and seem silly. So is there a better solution? The answer is yes, Netflix has provided us with a framework: Feign.

Feign is a declarative Web Service client, and its purpose is to make Web Service calls easier. Feign provides a template for HTTP requests. By writing a simple interface and annotations, you can define the parameters, format, and address of the HTTP request. Feign will proxy HTTP requests, and we only need to call it like a method to complete the service request.

Feign integrates Ribbon and Hystrix (I will talk about Hystrix later), so that we no longer need to use these two components explicitly. In addition, Spring Cloud also provides Spring MVC annotation support for Feign, which also allows us to use the same HttpMessageConverter in the Web.

In summary, Feign has the following characteristics:

  • Annotation support, including Feign annotations and JAX-RS annotations;
  • Support HTTP encoder and decoder;
  • Support Hystrix and its Fallback;
  • Support Ribbon load balancing;
  • Supports compression of HTTP requests and responses.

Example Project

In this example we will build an e-mall project. We have a Product-Service provides product services. It is an Eureka Client, and is a service provider. Product-Service-Consumer is also an Eureka Client. As a service consumer, it uses Feign. The Service-Discovery remains unchanged (we can directly reuse the code in previous article).



Product-Service

1. pom.xml

Product-Service is a standard Eureka Client, so pom.xml is the same as the previous Service-Hello

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

2. Main class

1
2
3
4
5
6
7
@EnableDiscoveryClient
@SpringBootApplication
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}

This is same as previous blogs

3. Product entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Product {
// ========================================================================
// fields =================================================================
private String itemCode;
private String name;
private String brandName;
private int price;

// ========================================================================
// constructor ============================================================
public Product() {
}

public Product(String itemCode, String name, String brandName, int price) {
this.itemCode = itemCode;
this.name = name;
this.brandName = brandName;
this.price = price;
}

public String getItemCode() {
return itemCode;
}

public void setItemCode(String itemCode) {
this.itemCode = itemCode;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getBrandName() {
return brandName;
}

public void setBrandName(String brandName) {
this.brandName = brandName;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}
}

4. Product service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
@RequestMapping("/products")
public class ProductEndpoint {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductEndpoint.class);

private List<Product> products;

@Value("${server.port}")
private int serverPort = 0;

public ProductEndpoint() {
products = new ArrayList<>();
products.add(new Product("item-1", "test-1", "brand1", 100));
products.add(new Product("item-2", "test-2", "brand2", 200));
products.add(new Product("item-3", "test-3", "brand3", 300));
products.add(new Product("item-4", "test-4", "brand4", 400));
products.add(new Product("item-5", "test-5", "brand5", 500));
products.add(new Product("item-6", "test-6", "brand6", 600));
}

@RequestMapping(method = RequestMethod.GET)
public List<Product> list() {
LOGGER.info("Server port {}", serverPort);

return this.products;
}

@RequestMapping(value = "/{itemCode}", method = RequestMethod.GET)
public Product detail(@PathVariable String itemCode) {
LOGGER.info("Server port {}", serverPort);

for (Product product : products) {
if (product.getItemCode().equalsIgnoreCase(itemCode))
return product;
}
return null;
}
}

This service provides the following two interfaces:

  • list: get product list;
  • detail: get detailed data of the specified product.

5. application.properties

1
2
3
4
5
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

server.port=2100

spring.application.name=PRODUCT-SERVICE

6. Launch Testing

Start Service-discovery and Product-Service, we can see that PRODUCT-SERVICE has registered in the Eureka server.



Product-Service-Consumer

1. pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

2. Main class

1
2
3
4
5
6
7
8
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}

We added the @EnableFeignClients annotation to the startup class to enable Feign related functions.

3. Service call class

Spring Cloud creates a proxy class that can be called directly by the application according to this interface. In order to complete the proxy, we need to add some annotations, where @FeignClient is the PRODUCT-SERVICE defined by the Product-Service. @RequestMapping is an annotation of SpringMVC, which corresponds to two API interfaces provided in the Product-Service.

1
2
3
4
5
6
7
8
@FeignClient(value = "PRODUCT-SERVICE")
public interface ProductService {
@RequestMapping(value = "/products", method = RequestMethod.GET)
List<Product> findAll();

@RequestMapping(value = "/products/{itemCode}", method = RequestMethod.GET)
Product loadByItemCode(@PathVariable("itemCode") String itemCode);
}

Note: if your service compiles a jar package separately, you need to specify the value of basePackages when using the @EnableFeignClients annotation. Otherwise, there will be an error reporting ProductService bean cannot be found when autowiring.

1
2
3
4
5
6
7
8
@EnableFeignClients(basePackages = "com.feigndemo.**")
@EnableDiscoveryClient
@SpringBootApplication
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}

4. Controller

The Controller is a standard SpingMVC Controller, which is used to provide users with specific services.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;

@RequestMapping(method = RequestMethod.GET)
public List<Product> list() {
return this.productService.findAll();
}

@RequestMapping(value = "/{itemCode}", method = RequestMethod.GET)
public Product detail(@PathVariable String itemCode) {
return this.productService.loadByItemCode(itemCode);
}
}

5. application.properties

1
2
3
4
5
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

server.port=9090

spring.application.name=PRODUCT-SERVICE-CONSUMER

6. Launch Testing

Start Product-Service-Consumer, and visit: http://localhost:9090/products



And visiting http://localhost:9090/products/item-3



Parameter Binding

Feign supports a variety of annotations. When using Feign, we can use Feign‘s own annotations or JAX-RS annotations as needed. Spring Cloud has enhanced Feign so that Feign supports Spring MVC annotations. As above code:

1
2
3
4
5
@RequestMapping(value = "/products", method = RequestMethod.GET)
List<Product> findAll();

@RequestMapping(value = "/products/{itemCode}", method = RequestMethod.GET)
Product loadByItemCode(@PathVariable("itemCode") String itemCode);

We are using Spring MVC annotations. The annotations we commonly use in Spring MVC are:

  • @RequestParam binds a single request parameter value;
  • @PathVariable binds the URI template variable value;
  • @RequestHeader binds the request header data;
  • @RequestBody binds the requested content area data and can perform automatic type conversion, etc.

Example of using @RequestParam, @PathVariable, and @RequestHeader:

1
2
3
4
5
6
7
8
@RequestMapping(value = "/products/detail", method = RequestMethod.GET)
Product loadByItemCode(@RequestParam("itemCode") String itemCode);

@RequestMapping(value = "/products/{itemCode}", method = RequestMethod.GET)
Product loadByItemCode(@PathVariable("itemCode") String itemCode);

@RequestMapping(value = "/products/detail", method = RequestMethod.GET)
Product loadByItemCode(@RequestHeader("itemCode") String itemCode);

Of course, we can also use multiple parameter binding, as follows:

1
2
@RequestMapping(value = "/products/{itemCode}", method = RequestMethod.POST)
Product changPrice(@PathVariable("itemCode") String itemCode, @RequestParam("price") int price);

Pass object as parameter

When we define an interface, we often include an object in the parameters. For example, our service interface is as follows:

1
User register(User user);

So how to pass complex user object? Just use the @RequestBody as follows:

1
2
@RequestMapping(value = "/users/register", method = RequestMethod.POST)
User register(@RequestBody User user);

When using it, you need to pay attention that User object must have a default constructor, otherwise Feign will not be able to convert to the User object according to the JSON string, thereby throwing an exception, causing the call to fail.

Check out the source code here: Feign demo