一、目前的代码
- 服务提供者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);
}
}
- 服务消费者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);
}
- 降级处理类为
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);
}
}
- 服务消费者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客户端去请求该服务提供者的接口时,肯定会因为超时而走降级逻辑
- 编写个接口测试下
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);
}
}
果不其然,返回了降级处理类中的数据(多请求几次仍然是该结果)
二、问题的复现
- 修改nacos上服务消费者的配置
确认发布。这下超时时间设置为6秒,完全够用了吧
- 我们在消费者控制台也看到了日志
该日志说明运行在服务消费者server-consumer中的、负责监听nacos配置变化的组件已经收到了nacos上的配置变更,并且发布了RefreshEvent事件,也触发了RefreshEventListener。源码如下
说明消费者服务已经感知到了配置的变化,但至于feign客户端会不会使用最新的配置
- 我们再次访问接口试试
多次访问还是这个接口,说明最新配置对feign客户端并没有生效
- 那重启服务消费者server-consumer试试
再次访问接口,返回了正常业务结果,说明最新配置生效了
问题:目前的情况,即使修改了nacos配置,想要feign的配置生效,要重启服务才行!那这样岂不是太不灵活了,跟直接写在代码里有什么区别,这样岂不是白用配置中心了吗?!
三、完美地解决
- 在服务消费者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
- 引入依赖 apache httpclient 依赖,创建自定义feignClient时会使用
implementation "io.github.openfeign:feign-httpclient"
- 修改nacos上服务消费者的配置文件:server-consumer.yaml,添加如下配置
feign:
httpclient:
enabled: true
- 创建一个配置类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);
}
}
}
- 重启服务(修改完代码,第一次肯定要重启,之后只需修改nacos配置即可),再次调用:http://localhost:8082/tests/test1?name=dj
因为nacos配置数据响应超时时间是4秒,而服务提供者处理业务需要5秒,故会走降级逻辑,没毛病
- 再次修改nacos上服务消费者的配置,将readTimeout由原来的4000毫秒调整为6000毫秒,而服务提供者需要5秒(5000毫秒)后才返回,按道理应该不会超时,会返回正常的业务结果
- 不重启服务,再次调用:http://localhost:8082/tests/test1?name=dj
至此,在不重启服务的前提下,动态调整feign的超时时间,服务也能动态感知并应用最新配置的需求,完美实现!!!