动态配置feign的超时时间,服务在不重启也能使用最新的配置,很完美!!!

0 阅读5分钟

一、目前的代码

  1. 服务提供者server-provider中有个接口,使用睡眠5秒的方式来模拟真实业务操作耗时了5秒
package com.chaoup.provider.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/hellos")
public class Hello {

    @GetMapping("/say")
    String hello(@RequestParam("name") String name) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return String.format("hello %s,我是服务提供者", name);
    }
}

  1. 服务消费者server-consumer通过feign client调用服务提供者server-provider中的该接口
package com.chaoup.consumer.feign.clients;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "server-provider", fallback = HelloClientFallback.class)
public interface HelloClient {

    @GetMapping("/hellos/say")
    String hello(@RequestParam("name") String name);
}

  1. 降级处理类为
package com.chaoup.consumer.feign.clients;

import org.springframework.stereotype.Component;

@Component
public class HelloClientFallback implements HelloClient{
    @Override
    public String hello(String name) {
        return String.format("hello %s,我是服务提供者的备胎", name);
    }
}

  1. 服务消费者server-consumer在nacos上的配置为
server:
    port: 8082
spring:
  cloud:
    sentinel: # sentinel相关的配置
      transport:
        dashboard: localhost:8080   # Sentinel控制台地址
        port: 8719                  # 客户端监控端口
      eager: true                    # 提前加载,避免首次调用才初始化
      enabled: true                   # 启用Sentinel
# feign相关的配置
feign:
  client:
    config:
      default:                        # 默认配置
        connectTimeout: 5000          # 连接超时(毫秒)
        readTimeout: 6000             # 读取超时(毫秒)
        loggerLevel: full              # 日志级别:NONE, BASIC, HEADERS, FULL
      server-provider:                # 针对服务提供者的配置(会覆盖默认)
        connectTimeout: 3000
        readTimeout: 4000
  # 启用sentinel对feign的支持
  sentinel:
    enabled: true
# 暴露端点用于监控
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

其中server-provider下的readTimeout配置的4000毫秒,就表示feign客户端等待服务提供者返回数据或者响应的最长时间为4秒。

而上面我们专门让服务提供者的接口要睡眠5秒后才返回数据,所以这种情况下,服务消费者中的feign客户端去请求该服务提供者的接口时,肯定会因为超时而走降级逻辑

  1. 编写个接口测试下
package com.chaoup.consumer.controller;

import com.chaoup.consumer.feign.clients.HelloClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/tests")
public class Test {

    @Resource
    private HelloClient helloClient;

    @GetMapping("/test1")
    public String test1(@RequestParam("name") String name) {
       return helloClient.hello(name);
//        return String.format("%s,测试成功!", name);
    }
}

果不其然,返回了降级处理类中的数据(多请求几次仍然是该结果)

二、问题的复现

  1. 修改nacos上服务消费者的配置

确认发布。这下超时时间设置为6秒,完全够用了吧

  1. 我们在消费者控制台也看到了日志

该日志说明运行在服务消费者server-consumer中的、负责监听nacos配置变化的组件已经收到了nacos上的配置变更,并且发布了RefreshEvent事件,也触发了RefreshEventListener。源码如下

说明消费者服务已经感知到了配置的变化,但至于feign客户端会不会使用最新的配置

  1. 我们再次访问接口试试

多次访问还是这个接口,说明最新配置对feign客户端并没有生效

  1. 那重启服务消费者server-consumer试试

再次访问接口,返回了正常业务结果,说明最新配置生效了

问题:目前的情况,即使修改了nacos配置,想要feign的配置生效,要重启服务才行!那这样岂不是太不灵活了,跟直接写在代码里有什么区别,这样岂不是白用配置中心了吗?!

三、完美地解决

  1. 在服务消费者server-consumer中创建一个参数配置类DynamicProperties.java

该配置类负责接收feign的相关参数,并使用@RefreshScope标记该配置类可动态刷新,即只要修改此类关注的、在nacos上的相关配置,则该配置类就会被重新创建,并得到最新的配置信息

package com.chaoup.consumer.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

/**
 * @Configuration :标注该类为配置类,即是一个Bean
 * @RefreshScope注解 :使得整个类为刷新作用域,当nacos上的配置变更时,spirng cloud会刷新该bean,然后重新注入@value值,重新调用@Bean表述的方法
 * 由以上两个注解的配合,只要nacos上的配置由变化,那新的配置类DynamicFeignProperties就会重新创建,且该bean的属性是最新的
 */
@Data
@RefreshScope
@Configuration
public class DynamicFeignProperties {

    @Value("${feign.client.config.server-provider.connectTimeout:3000}")
    private int serverProviderConnectTimeout;

    @Value("${feign.client.config.server-provider.readTimeout:4000}")
    private int serverProviderReadTimeout;

}

自定义一个FeignClient,使用@Primary注解标注,这样可以覆盖默认的FeignClient

  1. 引入依赖 apache httpclient 依赖,创建自定义feignClient时会使用
implementation "io.github.openfeign:feign-httpclient"
  1. 修改nacos上服务消费者的配置文件:server-consumer.yaml,添加如下配置
feign:
  httpclient: 
    enabled: true

  1. 创建一个配置类FeignConfig,负责注入相关的Bean(自定义feignClient所修的依赖),然后再该配置类的内部自定义一个实现Feign的Client接口的类,并且重写该接口中的execute方法,该方法会在每次通过feign调用时就会执行,所以就在方法内部来动态地获取最新的配置(超时参数)给http客户端去执行,从而实现不重启服务也能实现feign超时时间的动态配置
package com.chaoup.consumer.config;

import feign.Client;
import feign.Request;
import feign.Response;
import lombok.Data;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.concurrent.TimeUnit;


@Data
@Configuration
public class FeignConfig {

    @Resource
    private DynamicFeignProperties feignProperties;

    @Resource
    private LoadBalancerClient loadBalancerClient;

    @Resource
    private LoadBalancerClientFactory loadBalancerClientFactory;

    @Bean
    @Primary // 覆盖默认的 feignClient
    public Client feignClient() {
        // 实际执行http请求的client
        Client httpClient = new feign.httpclient.ApacheHttpClient();
        // 将实际执行请求的client包装成可实现负载均衡的client
        Client ldClient = new FeignBlockingLoadBalancerClient(
                httpClient, // 实际执行http请求的client
                loadBalancerClient, // 用于选择服务实例
                loadBalancerClientFactory // 用于获取每个服务实例的负载均衡配置
        );
        return new DynamicFeignClient(ldClient, feignProperties);
    }

    /**
     * 动态的feign client实现
     */
    public static class DynamicFeignClient implements Client {

        private final Client client;

        private final DynamicFeignProperties feignProperties;

        public DynamicFeignClient(Client client, DynamicFeignProperties feignProperties) {
            this.client = client;
            this.feignProperties = feignProperties;
        }


        @Override
        public Response execute(Request request, Request.Options options) throws IOException {
            // 从可刷星的配置类中获取最新的超时配置
            int connectTimeout = feignProperties.getConnectTimeout();
            int readTimeout = feignProperties.getReadTimeout();

            // 创建新的request.Options对象,覆盖传进来的options
            Request.Options dynamicOptions = new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, options.isFollowRedirects());

            return client.execute(request, dynamicOptions);
        }
    }


}

  1. 重启服务(修改完代码,第一次肯定要重启,之后只需修改nacos配置即可),再次调用:http://localhost:8082/tests/test1?name=dj

因为nacos配置数据响应超时时间是4秒,而服务提供者处理业务需要5秒,故会走降级逻辑,没毛病

  1. 再次修改nacos上服务消费者的配置,将readTimeout由原来的4000毫秒调整为6000毫秒,而服务提供者需要5秒(5000毫秒)后才返回,按道理应该不会超时,会返回正常的业务结果

  1. 不重启服务,再次调用:http://localhost:8082/tests/test1?name=dj

至此,在不重启服务的前提下,动态调整feign的超时时间,服务也能动态感知并应用最新配置的需求,完美实现!!!