SpringCloud-OpenFeign服务调用

765 阅读20分钟

从Spring Cloud 2020版本开始,Spring Cloud移除了 Ribbon,使用Spring Cloud Loadbalancer作为客户端的负载均衡组件:

Spring Cloud版本如果Hoxton.M2 RELEASED版本之前的,Nacos Discovery默认是集成了Ribbon的,但是最新Alibaba-Nacos-DiscoveryHoxton.M2 RELEASED版本之后弃用了Ribbon,使用Spring Cloud Loadbalancer作为客户端的负载均衡组件。从Spring Cloud 2020版本开始,Spring Cloud移除了 Ribbon,使用Spring Cloud Loadbalancer作为客户端的负载均衡组件。

  1. Nacos 2021版本已经没有自带Ribbon的整合,所以需要引入另一个支持的jarloadbalancer;
  2. Nacos 2021版本已经取消了对Ribbon的支持,所以无法通过修改Ribbon负载均衡的模式来实现Nacos提供的负载均衡模式。

2020.0.X版本开始的OpenFeign底层不再使用Ribbon,使用Spring Cloud Loadbalancer作为客户端的负载均衡组件;

Feign和OpenFeign区别和特点:

  1. Feign:(2020版集成Spring Cloud Loadbalancer做负载均衡获取可用服务地址,Feign用于封装服务之前的HTTP通信)

    • Feign是Spring Cloud组件中的一个轻量级Restful的HTTP服务客户端,Feign内置了Spring Cloud Loadbalancer,用来做客户端的负载均衡
    • Feign允许您使用简单的注解方式定义和调用REST API接口,隐藏了底层HTTP通信的复杂性。
    • Feign需要手动配置和定义接口的方法以及与服务提供方的交互。
    • 使用Feign的注解接口,调用这个接口,就可以调用服务注册中心的服务
  2. OpenFeign

    • OpenFeign是Spring Cloud对Feign的增强和改进版本。
    • OpenFeign基于Feign构建,提供了更多的功能和扩展性。
    • OpenFeign可以与Eureka等服务注册中心无缝集成,自动进行服务发现和负载均衡。
    • OpenFeign支持动态代理,可以自动创建接口的实现类,无需手动编写实现类代码。
    • OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理产生实现类,实现类中做负载均衡并调用其他服务

在使用Ribbon和RestTemplate时,通过对RestTemplate进行封装处理,可以形成一套模版化的调用方法。然而,在实际开发中,一个接口可能会被多处调用,每个微服务都需要自行封装一些客户端类来处理对依赖服务的调用。这种方式增加了编码量和复杂度。

为了简化这个过程,Feign在Ribbon和RestTemplate的基础上进行了进一步的封装。Feign允许我们仅需创建一个接口并使用注解的方式来定义依赖服务的接口,而无需手动封装客户端类。Feign负责将接口的定义和服务提供方的实现进行绑定,从而简化了使用Spring Cloud Ribbon时自动封装服务调用客户端的开发工作量。

因此,Feign的出现使得定义和实现依赖服务接口变得更加简单,减少了重复的封装工作,提高了开发效率。

Ribbon是一个专注于负载均衡的库,Feign是一个用于定义和调用REST API的客户端库,而OpenFeign是对Feign的增强和改进版本,提供了更多的功能和集成能力。Feign和OpenFeign目前已经弃用Ribbon,使用Spring Cloud Loadbalancer做负载均衡,从注册中心获取服务地址轮询,然后OpenFeign对该服务地址进行远程调用。

image.png

  • Feign 是种声明式、模板化的 HTTP 客户端(仅在 consumer 中使用)。
  • 声明式调用就像调用 本地方法一 样调用远程方法;无感知远程 http 请求。

Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用,只不过对原有的方式(Ribbon+RestTemplate)进行了封装,开发者不必手动使用RestTemplate调服务,而是定义一个接口,在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,简化了开发。

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

image.png

OpenFeign简介

Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。

Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。

Feign可以与nacos和Spring Cloud Loadbalancer组合使用以支持负载均衡。ribbon已被弃用

OpenFeign利用Spring Cloud Loadbalancer维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要在接口上添加@FeignClient注解即可,优雅而简单的实现了服务调用。

Springboot集成OpenFeign

  • 主启动类:开启openfeign: @EnableFeignClients
  • 接口+注解:微服务调用接口+@FeignClient
  • 内置引入Spring Cloud Loadbalancer,支持负载均衡

1、引入starter

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

2、在启动类或者配置类上加@EnableFeignClients注解:

@SpringBootApplication
@EnableFeignClients
public class Application {

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

3、声明Feign接口

业务逻辑接口+@FeignClient配置调用provider服务

@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")//服务提供者服务名:CLOUD-PAYMENT-SERVICE
public interface PaymentFeignService
{
    @GetMapping(value = "/payment/get/{id}")  //和服务提供者提供的接口保持一致
    CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

//stores服务
@FeignClient(value = "stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    Page<Store> getStores(Pageable pageable);

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);

    @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
    void delete(@PathVariable Long storeId);
}
@RestController
public class OrderFeignController
{
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
    {
        return paymentFeignService.getPaymentById(id);
    }
}

@FeignClient的value值为客户端的名称(此时可以做到负载均衡),当然也可以写完整的主机名或者是ip端口值。

4、@EnableFeignClients属性解析

@EnableFeignClients用户开启Feign自动配置。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	// basePackages的别名,允许更简洁的注释声明,例如:@ComponentScan("org.my.pkg")而不是@ComponentScan(basePackages="org.my.pkg")
	String[] value() default {};

	// 用户扫描Feign客户端的包,也就是@FeignClient标注的类,与value同义,并且互斥
	String[] basePackages() default {};

	// basePackages()的类型安全替代方案,用于指定要扫描带注释的组件的包。每个指定类别的包将被扫描。 考虑在每个包中创建一个特殊的无操作标记类或接口,除了被该属性引用之外没有其他用途。
	Class<?>[] basePackageClasses() default {};

	// 为所有假客户端定制@Configuration,默认配置都在FeignClientsConfiguration中,可以自己定制
	Class<?>[] defaultConfiguration() default {};

	// 可以指定@FeignClient标注的类,如果不为空,就会禁用类路径扫描
	Class<?>[] clients() default {};

}

5、@FeignClient属性解析

@FeignClient用于标注Feign客户端。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {

	// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性
	@AliasFor("name")
	String value() default "";

	// 该类的Bean名称
	String contextId() default "";

	// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性
	@AliasFor("value")
	String name() default "";

	// 弃用 被qualifiers()替代。
	@Deprecated
	String qualifier() default "";

	// 模拟客户端的@Qualifiers值。如果qualifier()和qualifiers()都存在,我们将使用后者,除非qualifier()返回的数组为空或只包含空值或空白值,在这种情况下,我们将首先退回到qualifier(),如果也不存在,则使用default = contextId + "FeignClient"。
	String[] qualifiers() default {};

	// 绝对URL或可解析主机名
	String url() default "";

	// 是否应该解码404而不是抛出FeignExceptions
	boolean decode404() default false;

	// 用于模拟客户端的自定义配置类。可以包含组成客户端部分的覆盖@Bean定义,默认配置都在FeignClientsConfiguration类中,可以指定FeignClientsConfiguration类中所有的配置
	Class<?>[] configuration() default {};

	// 指定失败回调类
	Class<?> fallback() default void.class;

	// 为指定的假客户端接口定义一个fallback工厂。fallback工厂必须生成fallback类的实例,这些实例实现了由FeignClient注释的接口。
	Class<?> fallbackFactory() default void.class;

	// 所有方法级映射使用的路径前缀
	String path() default "";

	// 是否将虚拟代理标记为主bean。默认为true。
	boolean primary() default true;
}

可以通过以下任何一种方式向Feign客户端提供URL:

image.png

三、覆盖默认配置

1、覆盖默认配置

在FeignClientsConfiguration类中,OpenFeign为我们做了很多默认配置,其中所有的配置我们都可以自定义并且覆盖。

@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
    //..
}

在指定了我们自定义的FooConfiguration配置类之后,FooConfiguration配置类中自定义的配置会覆盖FeignClientsConfiguration中的配置。

注意!FooConfiguration类并不需要@Configuration注释,如果加上了@Configuration,就会全局生效。如果只在@FeignClient中指定,那么就会只在该@FeignClient标注的类中生效。

注意!@FeignClient4.0.2以版本前,使用url属性时,不需要name属性。现在name属性是必需的。

// name属性和url属性支持表达式
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
    //..
}

2、配置列表

Spring Cloud OpenFeign默认为Feign提供了以下bean配置:

  • Decoder feign解码器: 是一个ResponseEntityDecoder (被包装成了SpringDecoder)
  • Encoder feign编码器: 是一个SpringEncoder
  • Logger feign的Logger: 是一个Slf4jLogger
  • MicrometerObservationCapability micrometerObservationCapability: 如果feign-micrometer在类路径中并且ObservationRegistry可用
  • MicrometerCapability micrometerCapability: 如果feign-micrometer在类路径中,则MeterRegistry可用,而ObservationRegistry不可用
  • CachingCapability cachingCapability:如果使用了@EnableCaching批注会使用。可以通过spring.cloud.openfeign.cache.enabled配置禁用。
  • Contract feignContract: 是一个SpringMvcContract
  • Feign.Builder feignBuilder: 是一个FeignCircuitBreaker.Builder
  • Client feignClient: 如果Spring Cloud LoadBalancer在类路径上,则使用FeignBlockingLoadBalancerClient。如果它们都不在类路径中,则使用默认的feign客户端。

Spring Cloud OpenFeign没有为Feign默认提供以下bean,但仍然从应用程序上下文中查找这些类型的bean来创建feign客户端:

  • Logger.Level
  • Retryer
  • ErrorDecoder
  • Request.Options
  • Collection<RequestInterceptor>
  • SetterFactory
  • QueryMapEncoder
  • Capability (MicrometerObservationCapability and CachingCapability are provided by default)

其中Retryer 默认是Retryer.NEVER_RETRY,这将禁止重试。请注意,这种重试行为不同于openfeign默认行为,它将自动重试IOExceptions,将它们视为暂时的网络相关异常,以及从ErrorDecoder抛出的任何RetryableException。

我们可以自定义以上任意一个Bean,来覆盖默认的配置:

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

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}

3、使用配置文件进行配置

@FeignClient的配置也可以在配置文件中进行配置,其中feignName就是@FeignClient的value值、name值和contextId值

spring:
    cloud:
        openfeign:
            client:
                config:
                    feignName:
                        url: http://remote-service.com
                        connectTimeout: 5000
                        readTimeout: 5000
                        loggerLevel: full
                        errorDecoder: com.example.SimpleErrorDecoder
                        retryer: com.example.SimpleRetryer
                        defaultQueryParameters:
                            query: queryValue
                        defaultRequestHeaders:
                            header: headerValue
                        requestInterceptors:
                            - com.example.FooRequestInterceptor
                            - com.example.BarRequestInterceptor
                        responseInterceptor: com.example.BazResponseInterceptor
                        dismiss404: false
                        encoder: com.example.SimpleEncoder
                        decoder: com.example.SimpleDecoder
                        contract: com.example.SimpleContract
                        capabilities:
                            - com.example.FooCapability
                            - com.example.BarCapability
                        queryMapEncoder: com.example.SimpleQueryMapEncoder
                        micrometer.enabled: false

可以配置全局的配置,并且配置文件优先:

spring:
    cloud:
        openfeign:
            client:
                config:
                    default:
                        connectTimeout: 5000
                        readTimeout: 5000
                        loggerLevel: basic

4、创建多个相同名称客户端

如果我们想要创建多个具有相同名称或url的feign客户端,以便它们指向相同的服务器,但是每个客户端都具有不同的自定义配置,那么我们必须使用@FeignClient的contextId属性,以避免这些配置beans的名称冲突。

@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
    //..
}

@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
    //..
}

5、配置FeignClient不从父上下文继承beans

@Configuration
public class CustomConfiguration{

@Bean
public FeignClientConfigurer feignClientConfigurer() {
            return new FeignClientConfigurer() {

                @Override
                public boolean inheritParentConfiguration() {
                    return false;
                }
            };

        }
}

6、SpringEncoder 的配置

在我们提供的SpringEncoder中,我们为二进制内容类型设置空字符集,为所有其他内容类型设置UTF-8。 您可以通过将spring.cloud.openfeign.encoder.charset-from-content-type的值设置为true来修改此行为,以从Content-Type头字符集派生字符集。

7、Feign拦截器的配置及使用

feign:
  sentinel:
    enabled: true
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
        loggerLevel: full
        requestInterceptors: com.vctgo.common.security.feign.FeignRequestInterceptor

拦截器是OpenFeign可用的一种强大的工具,它可以被用来在请求和响应前后进行一些额外的处理。要使用OpenFeign拦截器,可以通过以下步骤进行配置:

public class MyInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 在这里添加额外的处理逻辑,添加请求头
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
            HttpServletRequest request = attributes.getRequest();
            String value = request.getHeader(headerName);
            template.header(headerName, headerValue);
        }
    }
}
/**
 * feign 请求拦截器
 */
@Slf4j
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{

    @Override
    public void apply(RequestTemplate requestTemplate)
    {
        HttpServletRequest httpServletRequest = ServletUtils.getRequest();

        if (StringUtils.isNotNull(httpServletRequest))
        {
            Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
            // 传递用户信息请求头,防止丢失
            log.debug(headers.toString());
            String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
            if (StringUtils.isNotEmpty(userId))
            {
                requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
            }
            String userKey = headers.get(SecurityConstants.USER_KEY);
            if (StringUtils.isNotEmpty(userKey))
            {
                requestTemplate.header(SecurityConstants.USER_KEY, userKey);
            }
            String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
            if (StringUtils.isNotEmpty(userName))
            {
                requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
            }
            String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
            if (StringUtils.isNotEmpty(authentication))
            {
                requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
            }
            Map<String,Object> remoteheader = SecurityContextHolder.get(SecurityConstants.REMOTE_HEADER, HashMap.class);
            if (remoteheader!=null && remoteheader.size()>0 && !remoteheader.isEmpty())
            {
                for (String key : remoteheader.keySet()) {
                    requestTemplate.removeHeader(key);
                    requestTemplate.header(key, (String) remoteheader.get(key));
                }
            }
            String tenantid = headers.get(SecurityConstants.DETAILS_TENANT_ID);
            if (StringUtils.isNotEmpty(tenantid))
            {
                requestTemplate.header(SecurityConstants.DETAILS_TENANT_ID, tenantid);
            }

            String deptid = headers.get(SecurityConstants.DETAILS_DEPT_ID);
            if (StringUtils.isNotEmpty(deptid))
            {
                requestTemplate.header(SecurityConstants.DETAILS_DEPT_ID, deptid);
            }

            // 配置客户端IP
            requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());
        }
    }
}

将拦截器注册到OpenFeign:

/**
 * Feign 配置注册
 *
 **/
@Configuration
public class FeignAutoConfiguration
{
    @Bean
    public RequestInterceptor requestInterceptor()
    {
        return new FeignRequestInterceptor();
    }
    
    // 非必须
    @Bean
    public Feign.Builder feignBuilder() {
        return Feign.builder()
                .requestInterceptor(myInterceptor());
    }
}

8、OpenFeign超时时间设置

feign:
  sentinel:
    enabled: true
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
        loggerLevel: full
        requestInterceptors: com.vctgo.common.security.feign.FeignRequestInterceptor

(1)使用配置文件配置

在应用程序的配置文件(application.yml或application.properties)中,可以使用以下属性设置超时时间:

# YAML
feign:
  client:
    config:
      default:
        connectTimeout: 5000  # 连接超时时间
        readTimeout: 10000    # 读取超时时间

# Properties
feign.client.config.default.connectTimeout=5000  # 连接超时时间
feign.client.config.default.readTimeout=10000    # 读取超时时间

上述代码中,我们使用feign.client.config.default属性来配置默认的超时时间。connectTimeout属性设置连接超时时间,readTimeout属性设置读取超时时间。单位是毫秒。

(2)通过Java代码设置超时时间

如果你更喜欢使用Java代码来配置openfeign,可以通过以下方式设置超时时间:

import feign.Request;

// 创建一个Request.Options对象来设置超时时间
Request.Options options = new Request.Options(connectTimeoutMillis, readTimeoutMillis);

// 在创建Feign客户端时指定Options对象
MyApi myApi = Feign.builder()
        .options(options)
        .target(MyApi.class, "https://example.com");

在上述代码中,我们创建了一个Request.Options对象,该对象包含连接超时时间和读取超时时间。然后将Options对象传递给Feign客户端。

(3)使用@FeignClient设置超时时间

使用@FeignClient注解的configuration属性来指定配置类。

首先,创建一个配置类,继承自feign.Request.Options类,并重写connectTimeoutMillis和readTimeoutMillis方法,以设置超时时间。

import feign.Request;
public class MyApiConfiguration extends Request.Options {
    public MyApiConfiguration(int connectTimeoutMillis, int readTimeoutMillis) {
        super(connectTimeoutMillis, readTimeoutMillis);
    }

    @Override
    public Integer connectTimeoutMillis() {
        return 5000;  // 设置连接超时时间为5秒
    }

    @Override
    public Integer readTimeoutMillis() {
        return 10000; // 设置读取超时时间为10秒
    }
}

然后,在使用@FeignClient注解进行声明时,使用configuration属性指定该配置类。

@FeignClient(name = "my-service", configuration = MyApiConfiguration.class)
public interface MyApi {
    // 接口定义
}

这样,只有针对MyApi接口的请求会使用这个配置类中的超时时间,级别更加细致。当然,你也可以在上述配置类中加入其它一些针对MyApi接口的配置,比如重试次数等等。

(4)使用拦截器设置超时时间

要为单独请求设置超时时间,可以通过实现RequestInterceptor接口,并在其中为请求添加超时时间信息。具体方法如下:

import feign.RequestInterceptor;
import feign.RequestTemplate;
public class TimeoutRequestInterceptor implements RequestInterceptor {
    private final int connectTimeoutMillis;
    private final int readTimeoutMillis;

    public TimeoutRequestInterceptor(int connectTimeoutMillis, int readTimeoutMillis) {
        this.connectTimeoutMillis = connectTimeoutMillis;
        this.readTimeoutMillis = readTimeoutMillis;
    }

    @Override
    public void apply(RequestTemplate template) {
        template.options(new Request.Options(connectTimeoutMillis, readTimeoutMillis));
    }
}

在上述代码中,我们创建了一个TimeoutRequestInterceptor类,实现了RequestInterceptor接口,并重写了其中的apply方法。在该方法中,将请求的超时时间信息添加到请求模板中。

然后,在实际使用Feign客户端时,创建该拦截器对象并加入到Feign客户端的拦截器链中。

例如,我们想要对一个名为MyApi的Feign客户端接口的某个请求设置超时时间,可以这样:

MyApi myApi = Feign.builder()
    .requestInterceptor(new TimeoutRequestInterceptor(3000, 5000)) // 为该客户端指定一个拦截器
    .target(MyApi.class, "https://example.com");

在上述代码中,我们创建了一个TimeoutRequestInterceptor对象,并使用requestInterceptor方法将其加入到Feign客户端的拦截器链中。这样,在名为MyApi的Feign客户端中发出的所有请求都会使用该超时时间。

如果只想为某些请求设置超时时间,而不是所有请求,可以在该拦截器中添加一些判断逻辑,根据请求的条件来判断是否要添加超时时间信息。

(5)使用@Headers设置超时时间

通过在接口方法上加上@Headers注解,将超时时间信息直接加在请求头中,从而实现为单独请求设置超时时间。

例如,我们想要针对MyApi接口的someMethod方法单独设置超时时间,可以这样:

@Headers({"connect-timeout:5000", "read-timeout:10000"})
@GET("/someMethod")
String someMethod();

在上述代码中,我们在@Headers注解中添加了connect-timeout和read-timeout两个请求头信息,用于设置连接超时时间和读取超时时间。这样,在调用someMethod方法时,会使用这些请求头信息中指定的超时时间设置。

需要注意的是,这种方法需要在每个接口方法上都进行设置,因此比较麻烦。但它的优点是灵活性比较高,可以为不同的接口方法设置不同的超时时间。同时,也可以在其他注解中添加相应的超时信息,如@PostMapping、@PutMapping等。

(6)为单独接口设置超时时间

在feign接口里加入Request.Options这个参数就可以单独为接口单独设置超时时间了

@PostMapping("test/")
ResponseVO<?> test(Request.Options options, @RequestBody TestRequestEntity entity);

调用的时候new 一下Options对象

 ResponseVO<?> resp = client.test(
        new Request.Options(70, TimeUnit.SECONDS, 70, TimeUnit.SECONDS, true),
        entity);

9、OpenFeign设置重试次数

(1)一般写法

public class CustomRetryer implements Retryer {

    private final int maxAttempts;
    private final long backoff;
    int attempt;

    public CustomRetryer() {
        this(5, 1000);
    }

    public CustomRetryer(int maxAttempts, long backoff) {
        this.maxAttempts = maxAttempts;
        this.backoff = backoff;
        this.attempt = 1;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e;
        }
        try {
            Thread.sleep(backoff);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }

    @Override
    public Retryer clone() {
        return new CustomRetryer();
    }
}

在 FeignClient 中使用上一步定义的重试器:

@FeignClient(name = "demo", url = "${demo.base-url}", configuration = CustomRetryer.class)
public interface DemoFeignClient {
    //...
}

在这个例子中,使用的是自定义的重试器 CustomRetryer,它重试 5 次,在每次重试之间休眠 1000 毫秒。如果重试次数超限,则抛出 RetryableException 异常。

(2)简单写法

除了使用自定义的 Retryer 之外,OpenFeign 还提供了另外一种设置重试次数的方式,那就是通过 Feign 的配置项进行设置。具体操作如下:

在 FeignClient 中引入 Feign 的默认配置:

@FeignClient(name = "demo", url = "${demo.base-url}", configuration = FeignConfiguration.class)
public interface DemoFeignClient {
    //...
}

自定义 FeignConfiguration 类:

@Configuration
public class FeignConfiguration {

    @Bean
    public Retryer retryer() {
        return new Retryer.Default(500, 5000, 3);
    }
}

在这里,我们使用 Retryer.Default 类生成一个默认的重试器,它会在当前请求失败后重试 3 次,并会在第一次重试前等待 500 毫秒,在第二次重试前等待 1000 毫秒,在第三次重试前等待 2000 毫秒,以此类推。

通过这两个步骤,我们就可以为每个 FeignClient 设置默认的重试次数了。

(3)为每个请求设置重试次数

如果我们需要为特定的请求设置不同的重试策略,则可以在对应的方法上加上 @Retryable 注解,并指定对应的 Retryer 类型,如下所示:

@FeignClient(name = "demo", url = "${demo.base-url}", configuration = FeignConfiguration.class)
public interface DemoFeignClient {

    @RequestMapping(method = RequestMethod.GET, value = "/get")
    @Retryable(maxAttempts = 2, value = { SomeRetryer.class })
    String getDemo();

}

在这个例子中,我们使用了自定义的重试器 SomeRetryer,并指定了最大重试次数为 2。注意,为了使用 @Retryable 注解,我们需要引入 Spring Retry 库的依赖:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.5.RELEASE</version>
</dependency>

使用上述方式,我们可以为每个请求设置不同的重试策略,从而更加灵活地处理重试问题。

10、Feign请求日志级别设置

feign:
  sentinel:
    enabled: true
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
        loggerLevel: full
        requestInterceptors: com.vctgo.common.security.feign.FeignRequestInterceptor

Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。默认显示的是DEBUG级别日志。

// 设置指定客户端的日志
logging.level.project.user.UserClient: DEBUG

就是对Feign接口的调用情况进行监控和输出。

总共有以下日志级别:

  • NONE:默认的,不显示任何日志。
  • BASIC:仅记录请求方法和URL以及响应状态代码和执行时间。
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应头的信息。
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
// 代码设置日志级别
@Configuration
public class FooConfiguration {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

image.png

四、超时处理

我们可以在默认客户端和指定客户端上配置超时。OpenFeign使用两个超时参数:
connectTimeout:连接超时时间。
readTimeout:从建立连接开始到返回响应的时间。 注意:feign接口调用默认你超时时间是1s,如果出现网络延迟等,会导致接口调用失败

2020版本OpenFeign默认支持loadbalancer

feign:
  sentinel:
    enabled: true
  client:
    config:
      default:
	ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
        ReadTimeout: 3000 #服务请求处理超时时间(毫秒)

五、手动创建feign客户端

可以使用Feign Builder API创建客户端来进行定制。

// 手动创建两个Feign客户端并配置其拦截器和name属性,FeignClientsConfiguration.class仍然是它们的默认配置
@Import(FeignClientsConfiguration.class)
class FooController {

    private FooClient fooClient;

    private FooClient adminClient;

    @Autowired
    public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerObservationCapability micrometerObservationCapability) {
        this.fooClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .addCapability(micrometerObservationCapability)
                .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
                .target(FooClient.class, "https://PROD-SVC");

        this.adminClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .addCapability(micrometerObservationCapability)
                .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
                .target(FooClient.class, "https://PROD-SVC");
    }
}

还可以使用Builder 来配置FeignClient不从父上下文继承beans。可以通过在生成器上重写调用“inheritParentContext(false)”来实现这一点。

六、OpenFeign集成Sentinel实现服务的熔断降级

服务降级推荐使用Sentinel

<dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

openFeign使用sentinel的降级功能,还需要在配置文件中开启,添加如下配置:

feign:
  sentinel:
    enabled: true

添加降级回调类,这个类一定要和openFeign接口实现同一个类

image.png OpenFeignFallbackService这个是降级回调的类,一旦OpenFeignService中对应得接口出现了异常则会调用这个类中对应得方法进行降级处理。

@FeignClient中添加fallback属性,属性值是降级回调的类,如下:

@FeignClient(value = "openFeign-provider",fallback = OpenFeignFallbackService.class)
public interface OpenFeignService {}

详细降级类参考下面。

六、Feign的SpringCloud断路器

如果Spring Cloud CircuitBreaker在classpath,并且spring.cloud.openfeign.circuitbreaker.enabled=true,Feign将使用断路器包装所有方法。

要在每个客户端的基础上禁用Spring Cloud CircuitBreaker支持,请创建一个普通的Feign.Builder。具有“prototype”范围的构建器,例如:

@Configuration
public class FooConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder() {
        return Feign.builder();
    }
}
// 通过提供CircuitBreakerNameResolver的bean,可以更改断路器名称模式。
@Configuration
public class FooConfiguration {
    @Bean
    public CircuitBreakerNameResolver circuitBreakerNameResolver() {
        return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
    }
}

要启用Spring Cloud CircuitBreaker组,请将spring.cloud.openfeign.circuitbreaker.group.enabled属性设置为true(默认为false)。

1、使用配置属性配置断路器(服务降级)

假如说有一个Feign客户端:

@FeignClient(url = "http://localhost:8080")
public interface DemoClient {

    @GetMapping("demo")
    String getDemo();
}

可以通过执行以下操作,使用配置属性对其进行配置:

spring:
  cloud:
    openfeign
      circuitbreaker:
        enabled: true
        alphanumeric-ids:
          enabled: true
resilience4j:
  circuitbreaker:
    instances:
      DemoClientgetDemo:
        minimumNumberOfCalls: 69
  timelimiter:
    instances:
      DemoClientgetDemo:
        timeoutDuration: 10s

2、fallback

Spring Cloud CircuitBreaker支持fallback的概念:当电路断开或出现错误时执行的默认代码路径。要为给定的@FeignClient启用回退,请将fallback属性设置为实现回退的类名。并且还需要将其实现声明为Spring bean。

如果不配置接口超时就会一直超时不会降级,用于接口超时异常做一些降级处理

@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {

    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello getHello();

    @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
    String getException();

}

@Component
static class Fallback implements TestClient {

    @Override
    public Hello getHello() {
        throw new NoFallbackAvailableException("Boom!", new RuntimeException());
    }

    @Override
    public String getException() {
        return "Fixed response";
    }

}

如果需要访问触发fallback触发器的原因,可以使用@FeignClient中的fallbackFactory属性。

@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
            fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {

    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello getHello();

    @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
    String getException();

}

@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {

    @Override
    public FallbackWithFactory create(Throwable cause) {
        return new FallbackWithFactory();
    }

}

static class FallbackWithFactory implements TestClientWithFactory {

    @Override
    public Hello getHello() {
        throw new NoFallbackAvailableException("Boom!", new RuntimeException());
    }

    @Override
    public String getException() {
        return "Fixed response";
    }

}

3、Feign客户端的primary属性

当使用Feign和Spring Cloud 断路器 fallback时,在ApplicationContext中有多个相同类型的beans。这将导致@Autowired不起作用,因为没有确切的一个bean,或者一个被标记为主bean。为了解决这个问题,Spring Cloud OpenFeign将所有的Feign实例标记为@Primary,因此Spring Framework将知道要注入哪个bean。在某些情况下,这可能并不理想。要关闭此行为,请将@FeignClient的primary属性设置为false(默认为true)。

@FeignClient(name = "hello", primary = false)
public interface HelloClient {
    // methods here
}

七、Feign的继承重用

Feign通过单一继承接口支持样板API。这允许将常见操作分组到方便的基本接口中。

// 共用接口实例
public interface UserService {

    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    User getUser(@PathVariable("id") long id);
}

@RestController
public class UserResource implements UserService {

}

@FeignClient("users")
public interface UserClient extends UserService {
}

但是!@FeignClient接口不应在服务器和客户端之间共享,并且不再支持在类级别上使用@RequestMapping注释@FeignClient接口。

八、Feign请求响应的压缩

可以考虑为您的feign请求启用请求或响应GZIP压缩。您可以通过启用以下属性之一来实现这一点:

spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true

Feign请求压缩为您提供了类似于您可能为web服务器设置的设置:

spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json
spring.cloud.openfeign.compression.request.min-request-size=2048

以上这些属性允许您选择压缩的媒体类型和最小请求阈值长度。

注意!由于OkHttpClient使用“透明”压缩,如果存在content-encoding或accept-encoding头,则该压缩将被禁用,因此当feign.okhttp.OkHttpClient存在于classpath中并且spring.cloud.openfeign.okhttp.enabled设置为true时,我们不启用压缩。

九、开启Feign的缓存

如果使用了@EnableCaching注释,将创建并注册一个CachingCapability bean,以便您的Feign客户端能够识别其接口上的@Cache*注解:

public interface DemoClient {

    @GetMapping("/demo/{filterParam}")
    @Cacheable(cacheNames = "demo-cache", key = "#keyParam")
    String demoEndpoint(String keyParam, @PathVariable String filterParam);
}

还可以通过属性spring.cloud.openfeign.cache.enabled=false禁用该功能。

十、@SpringQueryMap注解支持

openFeign提供了一个注解@SpringQueryMap完美解决POJO表单传参。

参考:SpringQueryMap的使用方式_@springquerymap_Strong小甲鱼的博客-CSDN博客

@SpringQueryMap是微服务之间调用,使用openfeign通过get请求方式来处理 多入参(也就是通过实体来传参) 情况的注解,多用于restful风格方式

作用:@SpringQueryMap,简单来说就是将实体转化为表单数据,比如

{
	"username" : "zhangsan",
	"passwd" : "******"
}

通过@SpringQueryMap标注之后呢,会变成这样子
url?username=zhangsan&passwd=******
注意:被@SpringQueryMap注解的对象只能有一个。因为不能保证多个对象中是否会存在相同的属性名,这是值得注意的一点。

Spring Cloud OpenFeign提供了一个等价的@SpringQueryMap注释,用于将POJO或Map参数注释为查询参数Map。

例如,Params类定义了参数param1和param2:

// Params.java
public class Params {
    private String param1;
    private String param2;

    // [Getters and setters omitted for brevity]
}
  1. 单实体入参数 下面的feign客户端通过使用@SpringQueryMap注解来使用Params类:
@FeignClient("demo")
public interface DemoTemplate {

    @GetMapping(path = "/demo")
    String demoEndpoint(@SpringQueryMap Params params);
}
  1. 多实体入参数

错误的方式:
以下这两种方式都是错误的,都是只能生效一个实体的传参数
feign调用方

@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@RequestParam Pagination pagination, @SpringQueryMap SysUserQueryRequest queryRequest);

@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@SpringQueryMap Pagination pagination, @SpringQueryMap SysUserQueryRequest queryRequest);

正确的方式: 根据上面的结论,可知只会生效一个实体的传参,其原因是因为get方式只能是表单提交的,不能通过body传输,如果这两个实体存在相同的属性,就会出现问题,所以就默认不会取第二个实体来传参数。

解决方法也很简单,将这两个实体都转为map,放到一个map中即可,再次提醒这两个实体中不能存在相同的属性名,否则出现参数覆盖情况

feign调用方

## 实体转map,使用阿里巴巴库的JSON类
Map<String, Object> param = JSON.parseObject(JSON.toJSONString(Pagination), Map.class);
Map<String, Object> param1 = JSON.parseObject(JSON.toJSONString(SysUserQueryRequest), Map.class);
param.addAll(param1);

## 以param作为参数传递
@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@SpringQueryMap Map<String, Object> param);

被调用方,无需用map接收,可以随意对象,只需要对应上属性名和类型即可

@GetMapping
@Operation(summary = "分页查询 用户")
public ApiPageResponse<SysUserQueryResponse> query(Pagination pagination, @Valid SysUserQueryRequest queryRequest) {
    return ApiPageResponse.ok(pagination, sysUserReadModelRepo.query(pagination, queryRequest));
}

十一、FeignClient的参数传递给服务提供方的方式

服务提供方和服务调用方保持一致即可

    /**
     * 服务提供方:post获取请求体
     */
    @PostMapping("/test7/{myId}")
    public String test7(@PathVariable("myId") String myId, @RequestBody Student student){
        System.out.println("LiveRoomController.test7");
        System.out.println(myId + "||" + student);
        return "success";
    }

    /**
     * FeignClient:post获取请求体
     */
    @PostMapping("/test7/{myId}")
    String test7(@PathVariable("myId") String myId, @RequestBody Student student);