SpringCloud之Feign讲解

  • Post author:
  • Post category:其他


1 Feign

1.1 定义

1.1.1 简介


Feign

是一个声明式的

Web Service

客户端,通过声明

RESTful

请求客户端


Spring Cloud

集成了

Ribbon



Eureka

,可在使用

Feign

时提供负载均衡的

http

客户端

微服务直接调用使用

RestTemplate

进行远程调用,非常方便,那么有了

RestTemplate

为什么还要有

Feign

,因为

RestTemplate

有一个致命的问题:

硬编码




点击了解Spring之RestTemplate




RestTemplate

调用中,我们每个调用远程接口的方法,都将远程接口对应的 ip、端口,或 service-id 硬编码到了 URL 中,如果远程接口的 ip、端口、service-id 有修改的话,需要将所有的调用都修改一遍,这样难免会出现漏改、错改等问题,且代码不便于维护。为了解决这个问题,

Netflix

推出了

Feign

来统一管理远程调用

1.1.2 属性介绍


@FeignClient

标签的常用属性如下:


  • name

    :指定

    FeignClient

    的名称,如果项目使用了

    Ribbon



    name

    属性会作为微服务的名称,用于服务发现

  • value

    : 调用服务名称,和

    name

    属性相同

  • url

    :

    url

    一般用于调试,可以手动指定

    @FeignClient

    调用的地址

  • decode404

    : 当发生

    http 404

    错误时,如果该字段位

    true

    ,会调用

    decoder

    进行解码,否则抛出

    FeignException

  • configuration

    :

    Feign

    配置类,可以自定义

    Feign



    Encoder、Decoder、LogLevel、Contract

  • fallback

    : 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,

    fallback

    指定的类必须实现

    @FeignClient

    标记的接口

    熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。底层依赖

    hystrix

    ,启动类要加上

    @EnableHystrix

  • fallbackFactory

    : 工厂类,用于生成

    fallback

    类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

  • path

    : 定义当前

    FeignClient

    的统一前缀

  • primary

    : 默认值

    true



    Feign

    会自动生成

    FeignClient

    ,也就是接口的jdk代理实现


注意



name/value



url

同时存在生效问题


  • name/value

    属性:这两个的作用是一样的,指定的是调用服务的微服务名称

  • url

    :指定调用服务的全路径,经常用于本地测试
  • 如果同时指定

    name



    url

    属性: 则以

    url

    属性为准,

    name

    属性指定的值便当做客户端的名称

1.1.3 原理解析


Feign

调用步骤:

  • 程序启动时,扫描所有的

    @FeignClient

    注解
  • 当接口方法被调用时,通过

    JDK

    代理来生成

    RequestTemplate

    模板
  • 根据

    RequestTemplate

    模板生成

    Http

    请求的

    Request

    对象

  • Request

    对象交给

    Client

    去处理,其中

    Client

    的网络请求框架可以是

    HttpURLConnection、HttpClient、OKHttp
  • 最后

    client

    封装成

    LoadBaLanceClient

    ,结合

    ribbon

    负载均衡地发起调用

1.2 Feign准备工作

1.2.1 引入依赖



Spring Cloud

项目中引入

Feign

依赖,但是因为

feign

底层是使用了

ribbon

作为负载均衡的客户端,而

ribbon

的负载均衡也是依赖于

eureka

获得各个服务的地址,所以要引入

eureka-client

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    随便写的版本号
    <version>2.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    随便写的版本号
    <version>2.0.2.RELEASE</version>
</dependency>

1.2.2 启动类和yml文件

需要在启动类上添加注解

@EnableFeignClients

以及

@EnableDiscoveryClient

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ProductApplication {

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

server:
  port: 8082

#配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    status-page-url-path: /info
    health-check-url-path: /health

#服务名称
spring:
  application:
    name: product
  profiles:
    active: ${boot.profile:dev}
#feign的配置,连接超时及读取超时配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

1.3 使用Feign

1.3.1 简单使用@FeignClient

@FeignClient(value = "CART",fallback=Hysitx.class,configuration = FeignConfiguration.class)
public interface CartFeignClient {

    @PostMapping("/cart/{productId}")
    Long addCart(@PathVariable("productId")Long productId);
}

编写熔断类,发生错误时回调

import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class Hysitx implements IRemoteCallService{
    @Override
    public List<String> test(String[] names) {
        System.out.println("接口调用失败");
        return null;
    }
}

引入

FeignAutoConfiguration

配置

@Import(FeignAutoConfiguration.class)
@Configuration
public class FeignConfiguration{
    ...
}

上面是最简单的feign client的使用,声明完为feign client后,其他spring管理的类,如service就可以直接注入使用了,例如:

//这里直接注入feign client
@Autowired
private CartFeignClient cartFeignClient;

@PostMapping("/toCart/{productId}")
public ResponseEntity addCart(@PathVariable("productId") Long productId){
    Long result = cartFeignClient.addCart(productId);
    return ResponseEntity.ok(result);
}

1.3.2 @RequestLine


Feign

为什么用的是

@RequestLine

  • 这和

    open-feign



    Contract

    设计有关系,

    Contract

    是一个注解解析接口,它决定了接口可以使用什么注解转换到

    http

    请求。

    open-feign

    在使用

    @FeignClient

    的情况下,使用的是

    SpringMvcContract

    ,它使得被

    @FeignClient

    修饰的接口,可以使用

    @GetMapping,@PostMapping

    等Spring Mvc注解。

    如果我们要使用

    @RequestLine

    ,则需要替换

    open-Feign



    MVC

    解析器
// 在feign上写上配置
@FeignClient(name = "test-center", configuration = TestFeignConfig .class)
@Configuration// 配置类
public class TestFeignConfig {    
    @Bean
    public Contract feignContract() {
    // 配置feign的注释解析器为feign默认解析器而不是mvc解析器
        return new feign.Contract.Default();
    }
}

如果我们不单独配置,则会使用

FeignClientsConfiguration

中默认配置的

SpringMvcContract

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}


@RequestLine

与其它请求不同,只需要简单写请求方式和路径就能达到请求其它服务的目的

@FeignClient(value = "feign-server",configuration = FeignConfig.class)  //需要一个配置文件
public interface TestService {
    @RequestLine("POST /feign/test")    //对应请求方式和路径
    String feign(@RequestBody UserDO userDO);
}

配置文件

@EnableFeignClients
@SpringBootConfiguration
public class FeignConfig {
    @Bean
    public Contract contract(){
        return new feign.Contract.Default();
    }
}

@SpringBootConfiguration

  • 标注这个类是一个配置类;
  • 它只是

    @Configuration

    注解的派生注解;
  • 它与

    @Configuration

    注解的功能一致;
  • 只不过

    @SpringBootConfiguration



    springboot

    的注解,而

    @Configuration



    spring

    的注解

1.4 OpenFeign添加header信息

在微服务间使用

Feign

进行远程调用时需要在

header

中添加信息,那么

springcloud open feign

如何设置

header

呢?有5种方式可以设置请求头信息:



  • @RequestMapping

    注解里添加

    headers

    属性
  • 在方法参数前面添加

    @RequestHeader

    注解
  • 在方法或者类上添加

    @Headers

    的注解
  • 在方法参数前面添加

    @HeaderMap

    注解
  • 实现

    RequestInterceptor

    接口

1.4.1 在@RequestMapping注解里添加headers属性



application.yml

中配置

app.secret: appSecretVal

@PostMapping(value = "/book/api", headers = {"Content-Type=application/json;charset=UTF-8", "App-Secret=${app.secret}"})
void saveBook(@RequestBody BookDto condition);

1.4.2 在方法参数前面添加@RequestHeader注解

设置单个

header

属性

@GetMapping(value = "/getStuDetail")
public StudentVo getStudentDetail(@RequestBody StudentDto condition, @RequestHeader("Authorization") String token);

设置多个header属性

@PostMapping(value = "/card")
public CardVo createCard(@RequestBody CardDto condition, @RequestHeader MultiValueMap<String, String> headers);

1.4.3 在方法或者类上添加@Headers的注解

使用

feign

自带契约

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }
}


FeignClient

使用

@RequestLine

注解, 而未配置

feign

自带契约

Contract

时,

@Headers

不会起作用, 而且启动项目会报错:

Method xxx not annotated with HTTP method type (ex. GET, POST)

@RequestLine is a core Feign annotation, but you are using the Spring Cloud @FeignClientwhich uses Spring MVC annotations.

配置

@Headers

注解

@FeignClient(url = "${user.api.url}", name = "user", configuration = FooConfiguration.class)
public interface UserFeignClient {
    @RequestLine("GET /simple/{id}")
    @Headers({"Content-Type: application/json;charset=UTF-8", "Authorization: {token}"})
    public User findById(@Param("id") String id, @Param("token") String token);
}

使用

@Param

可以动态配置Header属性

网上很多在说

@Headers

不起作用,其实

@Headers

注解没有生效的原因是:官方的

Contract

没有生效导致的

1.4.4 在方法参数前面添加@HeaderMap注解

使用

feign

自带契约

配置

@HeaderMap

注解

@FeignClient(url = "${user.api.url}", name = "user", configuration = FooConfiguration.class)
public interface UserFeignClient {
    @RequestLine("GET /simple/{id}")
    public User findById(@Param("id") String id, @HeaderMap HttpHeaders headers);
}

1.4.5 实现RequestInterceptor接口

值得注意的一点是:

如果

FeignRequestInterceptor

注入到

spring

容器的话就会全局生效, 就是说即使在没有指定

configuration

属性的

FeignClient

该配置也会生效

配置

@Component



@Service



@Configuration

就可以将该配置注入

spring

容器中, 即可实现全局配置, 从而该项目中的所有

FeignClient



feign

接口都可以使用该配置.

如果只想给指定

FeignClient



feign

接口使用该配置, 请勿将该类配置注入spring中.

@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        template.header(HttpHeaders.AUTHORIZATION, "tokenVal");
    }

}

1.5 其他操作

1.5.1 使用OKhttp


Feign

底层默认是使用

jdk

中的

HttpURLConnection

发送

HTTP

请求,

feign

也提供了

OKhttp

来发送请求,具体配置如下

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
  okhttp:
    enabled: true
  hystrix:
    enabled: true

1.5.2 开启GZIP压缩


Spring Cloud Feign

支持对请求和响应进行

GZIP

压缩,以提高通信效率。


application.yml

配置信息如下:

feign:
  compression:
    request: #请求
      enabled: true #开启
      mime-types: text/xml,application/xml,application/json #开启支持压缩的MIME TYPE
      min-request-size: 2048 #配置压缩数据大小的下限
    response: #响应
      enabled: true #开启响应GZIP压缩


注意



由于开启

GZIP

压缩之后,

Feign

之间的调用数据通过二进制协议进行传输,返回值需要修改为

ResponseEntity<byte[]>

才可以正常显示,否则会导致服务之间的调用乱码。

示例如下:

@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);

2 Feign拦截器RequestInterceptor

2.1 定义

在使用

feign

做服务间调用的时候,如何修改请求的头部或编码信息呢,可以通过实现

RequestInterceptor

接口的

apply

方法,

feign

在发送请求之前都会调用该接口的

apply

方法,所以我们也可以通过实现该接口来记录请求发出去的时间点

2.2 RequestInterceptor


public interface RequestInterceptor {
 
  /**
   * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}


RequestInterceptor

接口定义了

apply

方法,其参数为

RequestTemplate

;它有一个抽象类为

BaseRequestInterceptor

,还有几个实现类分别为

BasicAuthRequestInterceptor、FeignAcceptGzipEncodingInterceptor、FeignContentGzipEncodingInterceptor


  • BasicAuthRequestInterceptor

    : 实现了

    RequestInterceptor

    接口,其

    apply

    方法往

    RequestTemplate

    添加名为

    Authorization



    header

  • BaseRequestInterceptor

    : 定义了

    addHeader

    方法,往

    requestTemplate

    添加非重名的

    header

  • FeignAcceptGzipEncodingInterceptor

    : 继承了

    BaseRequestInterceptor

    ,它的

    apply

    方法往

    RequestTemplate

    添加了名为

    Accept-Encoding

    ,值为

    gzip,deflate



    header

  • FeignContentGzipEncodingInterceptor

    : 继承了

    BaseRequestInterceptor

    ,其

    apply

    方法先判断是否需要

    compression

    ,即

    mimeType

    是否符合要求以及

    content

    大小是否超出阈值,需要

    compress

    的话则添加名为

    Content-Encoding

    ,值为

    gzip,deflate



    header

2.3 配置RequestInterceptor

可以通过配置类和配置文件两种方式注册

RequestInterceptor

  • 通过配置类配置时,通过

    FeignContext

    获取

    RequestInterceptor bean

  • 通过配置文件注册时,通过

    ApplicationContext

    获取

    RequestInterceptor bean


    通过配置类进行配置

    设置配置类有两种方式。

2.3.1 通过@EnableFeignClients的defaultConfiguration属性配置


@EnableFeignClients

注解的

defaultConfiguration

属性值为

@Configuration

注解的配置类,配置类内定义的

bean

会注册为所有

@FeignClient

的默认配置。

配置信息会保存为

FeignClientSpecification

对象。

2.3.2 通过@FeignClient的configuration属性配置


@FeignClient

注解的

configuration

属性值为

@Configuration

注解的配置类,其定义的

bean

为每个

FeignClient

的专有

bean


FeignContext

为每一个

@FeignClient

注解的接口提供独立的

AnnotationConfigApplicationContext

,其中包含

FeignClient

的私有

bean



EnableFeignClient

的默认

bean

2.3.3 通过配置文件配置

@Component
public class DpAuthFeignReqInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String jwtToken = request.getHeader(JWTTokenConstant.TOKEN_NAME);
            //传递token  给下游 
            template.header(JWTTokenConstant.TOKEN_NAME, jwtToken);
            
        }
    }
}



转载


作者:景宗会

链接:https://www.jianshu.com/p/92d0537de737

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。