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的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>	

OpenFeign:

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

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

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.1.5 负载均衡策略

主要有七种;

  • RoundRobinRule:轮询策略,按照服务顺序依次循环调用
  • WeightedResponseTimeRule:权重比策略,优先选择权重比高的服务,也就是服务响应时间比较短的,响应时间越长权重比越低
  • RandomRule:随机策略,服务提供者列表随机选择一个服务
  • BestAvailableRule:最小连接数策略,获取服务列表中连接数最小的服务实例
  • RetryRule:重试策略,重试获取已经失效的服务,指定时间没有获取到返回NULL
  • AvailabilityFilteringRule:可用性敏感策略,过滤非健康服务实例,选择lianji
  • ZoneAvoidanceRule:区域敏感策略

1.1.6 饥饿加载

Ribbon-eager-load(饥饿加载)模式
Ribbon 对于负载 Client 是在服务启动后,发生调用的时候才会去创建 Client,所以在第一次发生 http 请求调用的时候,不光要算上 http 的请求时间,还要算上 Client 创建时间,所以第一次调用的时候才会很慢
开启Ribbon饥饿加载

ribbon:
	nacos:
  		enabled: true # 开启naocos轮询
	eager-load:
		enabled: true  # 开启Ribbon的饥饿加载模式(防止第一次请求超时的问题)
		clients: Lxlxxx-system2 # 指定需要开启的服务(需要开启Ribbon的饥饿加载模式)
		ReadTimeout: 10000
		ConnectTimeout: 10000
		MaxAutoRetries: 0
		MaxAutoRetriesNextServer: 1
		OkToRetryOnAllOperations: false

其实这种饥饿加载模式,类似于客户端负载预热的一个操作,项目启动的时候进行加载,防止服务之间调用可以因为数据量、业务逻辑处理复杂性导致接口超时

1.2 OpenFeign准备工作

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

注意:如果在@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<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);

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

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

    @GetMapping("/payment/getPaymentById/{id}")
    CommonResult<Payment> 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<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-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<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容器中, 即可实现全局配置, 从而该项目中的所有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> T buildAuthorizedUserFeignClient(Class<T> 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<Payment> 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<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添加名为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);
            
        }
    }
}
  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值