SpringCloud之Feign,OpenFeign和Feign拦截器讲解

1 Feign

1.1 定义

1.1.1 简介

Feign是一个声明式的Web Service客户端,通过声明RESTful请求客户端
Spring Cloud集成了RibbonEureka,可在使用Feign时提供负载均衡的http客户端

Java当中常见的Http客户端有很多,除了Feign,类似的还有ApacheHttpClient 以及OKHttp3,还有SpringBoot自带的RestTemplate这些都是Java当中常用的HTTP 请求工具

微服务直接调用使用RestTemplate进行远程调用,非常方便,那么有了RestTemplate为什么还要有Feign,因为RestTemplate有一个致命的问题:硬编码
点击了解Spring之RestTemplate
RestTemplate 调用中,我们每个调用远程接口的方法,都将远程接口对应的 ip、端口,或 service-id 硬编码到了 URL 中,如果远程接口的 ip、端口、service-id 有修改的话,需要将所有的调用都修改一遍,这样难免会出现漏改、错改等问题,且代码不便于维护。为了解决这个问题,Netflix 推出了 Feign 来统一管理远程调用

1.1.2 OpenFeign和Feign的区别

Feign:

FeignSpring Cloud组件中的一个轻量级RESTfulHTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。


    org.springframework.cloud
    spring-cloud-starter-feign
   

OpenFeign:

OpenFeignSpring CloudFeign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign@FeignClient可以解析SpringMVC@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。


    org.springframework.cloud
    spring-cloud-starter-openfeign

Feign是在2019就已经不再更新了,通过maven网站就可以看出来,随之取代的是OpenFeign,从名字上就可以知道,它是Feign的升级版

1.1.3 属性介绍

@FeignClient标签的常用属性如下:

  • name:指定FeignClient的名称,如果项目使用了Ribbonname属性会作为微服务的名称,用于服务发现
  • value: 调用服务名称,和name属性相同
  • url: url一般用于调试,可以手动指定@FeignClient调用的地址
  • decode404: 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
  • configuration: Feign配置类,可以自定义FeignEncoder、Decoder、LogLevel、Contract
  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
    熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。底层依赖hystrix,启动类要加上@EnableHystrix
  • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
    另外,fallbackFactory 可以捕获异常信息并返回默认降级结果,可以打印堆栈信息,但是fallback不能打印堆栈信息,不利于问题排查
  • path: 定义当前FeignClient的统一前缀
  • primary: 默认值trueFeign会自动生成FeignClient,也就是接口的jdk代理实现

注意name/valueurl同时存在生效问题

  • name/value属性:这两个的作用是一样的,指定的是调用服务的微服务名称
  • url:指定调用服务的全路径,经常用于本地测试
  • 如果同时指定nameurl属性,则以url属性为准,name属性指定的值便当做客户端的名称

1.1.4 原理解析

Feign调用步骤:

  • 程序启动时,扫描所有的@FeignClient注解
  • 当接口方法被调用时,通过JDK代理来生成RequestTemplate模板
  • 根据RequestTemplate模板生成Http请求的Request对象
  • Request对象交给Client去处理,其中Client的网络请求框架可以是HttpURLConnection、HttpClient、OKHttp
  • 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用
在这里插入图片描述

1.2 OpenFeign准备工作

1.2.1 引入依赖

Spring Cloud项目中引入Feign依赖,但是因为feign底层是使用了ribbon作为负载均衡的客户端,而ribbon的负载均衡也是依赖于eureka获得各个服务的地址,所以要引入eureka-client


    org.springframework.cloud
    spring-cloud-starter-openfeign
    随便写的版本号
    2.0.2.RELEASE


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client
    随便写的版本号
    2.0.2.RELEASE

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

注意:如果在@EnableFeignClients注解中不指定basePackages属性,Feign会扫描当前包及其子包下的所有接口,如果接口不在这些包中,则会报错。因此,为了避免这种情况,需要在@EnableFeignClients注解中指定basePackages属性,以明确指定需要扫描的包

1.3 使用OpenFeign

1.3.1 简单使用@FeignClient

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

//@PostMapping是调用 目标服务的controller的方法,和对应controller路径保持一致
    @PostMapping("/cart/{productId}")
    Long addCart(@PathVariable("productId")Long productId);

    @GetMapping(value = "/payment/selectPaymentList")
    CommonResult selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);

    @GetMapping(value = "/payment/selectPaymentListByQuery")
    CommonResult selectPaymentListByQuery(@SpringQueryMap Payment payment);

    @PostMapping(value = "/payment/create", consumes = "application/json")
    CommonResult create(@RequestBody Payment payment);

    @GetMapping("/payment/getPaymentById/{id}")
    CommonResult getPaymentById(@PathVariable("id") String id);

}

@SpringQueryMap
spring cloud项目使用feign的时候都会发现一个问题,就是get方式无法解析对象参数。其实feign是支持对象传递的,但是得是Map形式,而且不能为空,与spring在机制上不兼容,因此无法使用。spring cloud2.1.x版本中提供了@SpringQueryMap注解,可以传递对象参数,框架自动解析。

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

import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class Hysitx implements IRemoteCallService{
    @Override
    public List 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-feignContract设计有关系,Contract是一个注解解析接口,它决定了接口可以使用什么注解转换到http请求。open-feign在使用@FeignClient的情况下,使用的是SpringMvcContract,它使得被@FeignClient修饰的接口,可以使用@GetMapping,@PostMapping等Spring Mvc注解。
    如果我们要使用@RequestLine,则需要替换open-FeignMVC解析器
// 在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注解的功能一致;
  • 只不过@SpringBootConfigurationspringboot的注解,而@Configurationspring的注解

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 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容器中, 即可实现全局配置, 从而该项目中的所有FeignClientfeign接口都可以使用该配置.
如果只想给指定FeignClientfeign接口使用该配置, 请勿将该类配置注入spring中.

@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

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

}

1.5 OpenFeign日志打印

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 FeignHttp 请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出

日志级别:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

配置日志Bean:

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

YML文件里需要开启日志的Feign客户端

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.gzl.cn.service.PaymentFeignService: debug

后台日志查看:


在这里插入图片描述

1.6 手动创建 Feign 客户端

@FeignClient无法支持同一service具有多种不同配置的FeignClient,因此,在必要时需要手动build FeignClient

@FeignClient(value = “CLOUD-PAYMENT-SERVICE”)
以这个为例,假如出现两个服务名称为CLOUD-PAYMENT-SERVICEFeignClient,项目直接会启动报错,但是有时候我们服务之间调用的地方较多,不可能将所有调用都放到一个FeignClient下,这时候就需要自定义来解决这个问题!

官网当中也明确提供了自定义FeignClient,以下是在官网基础上对自定义FeignClient的一个简单封装

首先创建FeignClientConfigurer类,这个类相当于build FeignClient的工具类

import feign.*;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.slf4j.Slf4jLogger;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;

@Import(FeignClientsConfiguration.class)
public class FeignClientConfigurer {

    private Decoder decoder;
    private Encoder encoder;
    private Client client;
    private Contract contract;

    public FeignClientConfigurer(Decoder decoder, Encoder encoder, Client client, Contract contract) {
        this.decoder = decoder;
        this.encoder = encoder;
        this.client = client;
        this.contract = contract;
    }

    public RequestInterceptor getUserFeignClientInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 添加header
            }
        };
    }

    public  T buildAuthorizedUserFeignClient(Class clazz, String serviceName) {
        return getBasicBuilder().requestInterceptor(getUserFeignClientInterceptor())
                //默认是Logger.NoOpLogger
                .logger(new Slf4jLogger(clazz))
                //默认是Logger.Level.NONE(一旦手动创建FeignClient,全局配置的logger就不管用了,需要在这指定)
                .logLevel(Logger.Level.FULL)
                .target(clazz, buildServiceUrl(serviceName));
    }

    private String buildServiceUrl(String serviceName) {
        return "http://" + serviceName;
    }

    protected Feign.Builder getBasicBuilder() {
        return Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract);
    }
}

使用工具类的方法创建多个FeignClient配置

import com.gzl.cn.service.FeignTest1Service;
import feign.Client;
import feign.Contract;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignClientConfiguration extends FeignClientConfigurer {
    public FeignClientConfiguration(Decoder decoder, Encoder encoder, Client client, Contract contract) {
        super(decoder, encoder, client, contract);
    }

    @Bean
    public FeignTest1Service feignTest1Service() {
        return super.buildAuthorizedUserFeignClient(FeignTest1Service.class, "CLOUD-PAYMENT-SERVICE");
    }

    // 假如多个FeignClient在这里定义即可
}

其中,super.buildAuthorizedUserFeignClient()方法中,第一个参数为调用别的服务的接口类,第二个参数为被调用服务在注册中心的service-id。

public interface FeignTest1Service {

    @GetMapping(value = "/payment/get/{id}")
    CommonResult getPaymentById(@PathVariable("id") Long id);
}

使用的时候正常注入使用即可

@Resource
private FeignTest1Service feignTest1Service;

1.7 Feign配置

Feign底层默认是使用jdk中的HttpURLConnection发送HTTP请求(默认不支持线程池,若请求多了会响应超时),feign也提供了OKhttp来发送请求
feign.client.*:支持按实例进行配置,feign.httpclient.*:全局共享一套配置,包含线程池配置,但只影响HttpClientOkHttp,不影响HttpURLConnection

所谓按实例进行配置,就是指每个FeignClient实例都可以通过feign.client..*来单独进行配置,注意首字母小写。而 feign.client.default.* 表示默认配置

1.7.1 使用OKhttp

具体配置如下

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

1.7.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才可以正常显示,否则会导致服务之间的调用乱码。
示例如下:

@PostMapping("/order/{productId}")
ResponseEntity 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添加名为Authorizationheader
  • BaseRequestInterceptor : 定义了addHeader方法,往requestTemplate添加非重名的header
  • FeignAcceptGzipEncodingInterceptor : 继承了BaseRequestInterceptor,它的apply方法往RequestTemplate添加了名为Accept-Encoding,值为gzip,deflateheader
  • FeignContentGzipEncodingInterceptor : 继承了BaseRequestInterceptor,其apply方法先判断是否需要compression,即mimeType是否符合要求以及content大小是否超出阈值,需要compress的话则添加名为Content-Encoding,值为gzip,deflateheader

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 的私有beanEnableFeignClient 的默认 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);
            
        }
    }
}

你可能感兴趣的:(SpringCloud之Feign,OpenFeign和Feign拦截器讲解)