icon-cookie
The website uses cookies to optimize your user experience. Using this website grants us the permission to collect certain information essential to the provision of our services to you, but you may change the cookie settings within your browser any time you wish. Learn more
I agree
blank_error__heading
blank_error__body
Text direction?

【Dalston】【第三章】声明式服务调用(Feign)

当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率低下,并且显得好傻。那么有没有更好的解决方案呢?答案是确定的有,Netflix已经为我们提供了一个框架:Feign。

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。

Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。此外,Spring Cloud还对Feign提供了Spring MVC注解的支持,也使得我们在Web中可以使用同一个HttpMessageConverter

总起来说,Feign具有如下特性:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解;
  • 支持可插拔的HTTP编码器和解码器;
  • 支持Hystrix和它的Fallback;
  • 支持Ribbon的负载均衡;
  • 支持HTTP请求和响应的压缩。

1. 示例代码

在我们编写代码之前先看看我们这次构建系统的系统框架是怎么的。

Feign-010

本示例将构建一个电子商城项目。其中Product-Service提供商品服务,是一个Eureka Client,角色是一个服务提供者。Mall则是一个Web项目,同样也是一个Eureka Client,作为服务消费者,同时也是本篇重点Feign Client。而Service-discover保持不变。

标注:Product-Service在项目中起名为FeignClient,Service-discover为Eureka-Server

1.1 构建Product-Service

编写pom.xml文件

Product-Service是一个标准的Eureka Client,所以pom.xml和之前Service-Hello一样,pom.xml:

<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>
    <parent>
        <groupId>com.sunny</groupId>
        <artifactId>SpringCloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>FeignClient</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    
</project>

编写启动类

package com.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class Application {

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

}

这里和之前也没有什么不同

商品(Product)实体

package com.product.entity;

import java.io.Serializable;

public class Product implements Serializable{
    private static final long serialVersionUID = 4668175645476846099L;
    /**产品货号*/
    private String itemCode;
    /**产品名称*/
    private String name;
    /**产品品牌名称*/
    private String brandName;
    /**产品价格(分)*/
    private int price;

    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 int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public String getBrandName() {
        return brandName;
    }
    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }

}

编写具体商品服务

package com.product.controller;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.product.entity.Product;

@RestController
@RequestMapping("/products")
public class ProductEndPoint {
    protected Logger logger = LoggerFactory.getLogger(ProductEndPoint.class);

    @RequestMapping(method = RequestMethod.GET)
    public List<Product> list() {
        logger.info("===do products ===");
        return this.buildProducts();
    }

    @RequestMapping(value = "/{itemCode}", method = RequestMethod.GET)
    public Product detail(@PathVariable String itemCode) {
        logger.info("===do detail ===");
        List<Product> products = this.buildProducts();
        for (Product product : products) {
            if (product.getItemCode().equalsIgnoreCase(itemCode))
                return product;
        }
        return null;
    }

    /**
     * @Description 创建数据
     * @return
     * @return List<Product>
     * @author SUNBIN
     * @date 2017年12月13日
     */
    protected List<Product> buildProducts() {
        List<Product> products = new ArrayList<>();
        products.add(new Product("1", "测试商品-1", "xx品牌", 100));
        products.add(new Product("2", "测试商品-2", "xx品牌", 200));
        products.add(new Product("3", "测试商品-3", "xx品牌", 300));
        products.add(new Product("4", "测试商品-4", "xx品牌", 400));
        products.add(new Product("5", "测试商品-5", "xx品牌", 500));
        products.add(new Product("6", "测试商品-6", "xx品牌", 600));
        return products;
    }

}

该服务提供下面两个接口:

  • list: 获取商品列表;
  • detail: 获取指定商品的详细数据。

编写配置文件

server.port=2100

spring.application.name=PRODUCT-SERVICE

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

启动测试

运行serviceclient后,在Eureka服务器控制台中我们可以看到下面的界面:

可以看到PRODUCT-SERVICE已经注册成功。

1.2 构建Mall

编写pom.xml文件

我们的Mall项目需要引入对Feign的依赖,如下:

<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>
    <parent>
        <groupId>com.sunny</groupId>
        <artifactId>SpringCloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>FeignConsumer</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>com.sunny</groupId>
            <artifactId>FeignClient</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
    </dependencies>

</project>

编写启动类

package com.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@EnableFeignClients/*(basePackages = "com.product.**")*///开启Feign相关功能,打成jar包时必须指定包的路径
@EnableDiscoveryClient
@SpringBootApplication
public class Application {

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

}

这里我们在启动类中增加了@EnableFeignClients注解,用来开启Feign相关功能。

编写服务调用类

服务调用类是一个我们之前常写接口,由Spring Cloud根据该接口创建可供程序直接调用的代理类。为了完成代理,我们需要添加一些注解,其中@FeignClient中的name为商品服务所定义的PRODUCT-SERVICE。而@RequestMapping是SpringMVC的注解,其对应商品服务中所提供的两个API接口:

package com.product.service;

import java.util.List;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.product.entity.Product;

@FeignClient("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);

}

说明: 假如你的Service是单独编译一个jar包,那么在使用@EnableFeignClients注解时需要指定basePackages的值,否则,自动织入时就会报找不到ProductServicebean的错误.

编写Controller

package com.product.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.product.entity.Product;
import com.product.service.ProductService;

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

这里我们自动织入了之前的服务调用接口ProductService

编写配置文件

server.port=8800

spring.application.name=MALL-WEB

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

1.3 启动测试

启动后我们可以访问:http://localhost:8080/products,可以看到如下界面:

然后,我们在浏览器中继续输入http://localhost:8080/products/1,可以看到如下界面:

到这里,看到我们已经成功通过Feign框架访问到PRODUCT-SERVICE所提供的服务。

2. 参数绑定

Feign支持多种注解,在使用Feign时,我们可以根据需要使用Feign自带的注解或者JAX-RS注解。Spring Cloud对Feign进行了增强,使得Feign支持了Spring MVC注解。如上面的代码:

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

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

我们使用的都是Spring MVC的注解。在Spring MVC中我们常用的注解有:

  • @RequestParam 绑定单个请求参数值;
  • @PathVariable 绑定URI模板变量值;
  • @RequestHeader 绑定请求头数据;
  • @RequestBody 绑定请求的内容区数据并能进行自动类型转换等。

使用@RequestParam、@PathVariable和@RequestHeader样例如下:

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

这几个样例都是同样的功能加载指定产品的详情。不过,这里需要注意的是不论@RequestParam、@PathVariable还是@RequestHeader它们的value都是不可以少的,也就是说我们不能像Spring MVC Controller中这样声明:

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

因为,在Spring MVC中这些注解会根据参数名称来作为默认值,而Feign则不会,必须声明。

当然,我们也可以在参数绑定中使用多个或者混用,如下:

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

2.1 传递对象

我们在定义接口的时候,在参数中往往会包含一个对象,比如我们的服务接口如下:

User register(User user);

这个接口提供一个用户注册功能,那么如何把复杂的用户信息传递过去呢?答案也非常简单,就是使用上面列出来的@RequestBody注解,使用方法如下:

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

同样,在使用的时候需要注意User对象必须要有默认构造函数,不然Feign会无法根据传递过来的JSON字符串转换为User对象,从而抛出异常,造成调用不成功。

Spring MVC中注解还有一些如:@CookieValue、@SessionAttributes、@RequestPart等,笔者并没有一个个进行测试。也许你会使用这,到时不妨告诉笔者是否可以。

3. 继承

在读之前的代码时会发现,Client工程中的ProductEndpointConsumer工程中ProductService中的方法声明几乎都是一样的,那么是否可以定义一个共同的父类来消除代码的重复呢?

答案是肯定的,Feign是支持继承的。比如,我们在增加一个公用的工程:FeignApi,然后将父类:ProductApiService定义到该工程中,代码如下:

package com.product.api;

import java.util.List;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.product.entity.Product;

public interface ProductApiService {

    @RequestMapping(value = "/products", method = RequestMethod.GET)
    List<Product> list();

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

将Product实体类挪到Api工程下

然后Consumer和Client工程分别依赖Api工程,pom.xml中增加

<dependency>
    <groupId>com.sunny</groupId>
    <artifactId>FeignClient</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

Client中的ProductEndPoint.java代码:

package com.product.controller;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.product.api.ProductApiService;
import com.product.entity.Product;

@RestController
//@RequestMapping("/products")
public class ProductEndPoint implements ProductApiService{
    protected Logger logger = LoggerFactory.getLogger(ProductEndPoint.class);

//    @RequestMapping(method = RequestMethod.GET)
    public List<Product> list() {
        logger.info("===do products ===");
        return this.buildProducts();
    }

//    @RequestMapping(value = "/{itemCode}", method = RequestMethod.GET)
    public Product detail(@PathVariable String itemCode) {
        logger.info("===do detail ===");
        List<Product> products = this.buildProducts();
        for (Product product : products) {
            if (product.getItemCode().equalsIgnoreCase(itemCode))
                return product;
        }
        return null;
    }

    /**
     * @Description 创建数据
     * @return
     * @return List<Product>
     * @author SUNBIN
     * @date 2017年12月13日
     */
    protected List<Product> buildProducts() {
        List<Product> products = new ArrayList<>();
        products.add(new Product("1", "测试商品-1", "xx品牌", 100));
        products.add(new Product("2", "测试商品-2", "xx品牌", 200));
        products.add(new Product("3", "测试商品-3", "xx品牌", 300));
        products.add(new Product("4", "测试商品-4", "xx品牌", 400));
        products.add(new Product("5", "测试商品-5", "xx品牌", 500));
        products.add(new Product("6", "测试商品-6", "xx品牌", 600));
        return products;
    }

}

删除Client中的Product实体类

Consumer中的ProductService代码:

package com.product.service;

import org.springframework.cloud.netflix.feign.FeignClient;

import com.product.api.ProductApiService;

@FeignClient("PRODUCT-SERVICE")
public interface ProductService extends ProductApiService{

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

最后修改下Consumer中的Controller请求路径和方法名称

package com.product.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.product.entity.Product;
import com.product.service.ProductService;

@RestController
@RequestMapping("/prod")
public class ProductController {
    @Autowired
    private ProductService productService;

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

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

完毕

最后通过页面请求prod路径测试,与之前结果是一样的

从修改的代码上可以看到,通过继承的确帮我们简化了Feign的开发,但Spring Cloud的是不推荐这种做法的,也不建议在服务提供方和服务消费方共享接口,因为这种方式会造成服务提供方与服务消费方之间代码的紧耦合。当然,是否能够使用这种特性还得具体到你自己的产品如何去权衡。

4. 与Swagger集成冲突

假如你的项目中使用了Swagger,那么可能会造成无法启动的错误:

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2017-07-06 16:25:10 656 [restartedMain] ERROR o.s.boot.SpringApplication - Application startup failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'productController': Unsatisfied dependency expressed through field 'productService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'io.twostepsfromjava.cloud.web.mall.service.ProductService': FactoryBean threw exception on object creation; nested exception is java.lang.NullPointerException
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    ...

这个是因为所使用的Swagger过低造成的,只需要将Swagger升级即可,如:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.6.1</version>
</dependency>

原文地址:http://www.jianshu.com/p/a0d50385e598

Measure
Measure
Related Notes
Get a free MyMarkup account to save this article and view it later on any device.
Create account

End User License Agreement

Summary | 4 Annotations
@EnableFeignClients/*(basePackages = "com.product.**")*///开启Feign相关功能,打成jar包时必须指定包的路径
2018/06/21 15:14
这里我们在启动类中增加了@EnableFeignClients注解,用来开启Feign相关功能。
2018/06/21 15:14
@FeignClient("PRODUCT-SERVICE")
2018/06/21 15:15
@FeignClient中的name为商品服务所定义的PRODUCT-SERVICE
2018/06/21 15:16