Gateway中Feign调用问题
问题背景
我们希望在Gateway中做一些登录鉴权,记录日志等操作,这些操作的实现可能不在Gateway中,可能需要Gateway自己去通过HTTP来调用其他服务以实现这些功能,但是在Gateway是基于Reactor模式的响应式编程,而Feign调用是阻塞式调用,这两者相矛盾,并且在官方文档里面Reactive-Support指出,由于OpenFeign 项目目前不支持响应式客户端,例如Spring WebClient ,Spring Cloud OpenFeign 也不支持。一旦核心项目可用,我们将在此处添加对其的支持。在此之前,我们建议使用feign-reactive来支持 Spring WebClient。
问题复现
简单写一个FeignClient
@FeignClient(name = "testClient", url = "http://localhost:8080") // 请将URL替换为实际服务地址
public interface TestFeignClient {
@GetMapping("/test/get")
R get(@RequestParam("name") String name, @RequestParam("age") String age);
@PostMapping("/test/post")
R post(@RequestBody User user);
}
在过滤器中随便调用一个 ,模拟鉴权等操作
@Component
public class FirstFilter implements GlobalFilter {
@Autowired
private TestFeignClient testFeignClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
User user = new User();
user.setAge(1);
user.setName("测试Feign调用");
System.out.println("线程:" + Thread.currentThread().getName() + ",调用Feign");
R post = testFeignClient.post(user);
System.out.println("线程:" + Thread.currentThread().getName() + ",调用结果:" + post);
return chain.filter(exchange);
}
}
报错:No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
解决办法:
@Configuration
public class FeignConfig {
@Bean
public HttpMessageConverters messageConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter()); // 根据需要添加其他的HttpMessageConverter
return new HttpMessageConverters(converters);
}
}
此时可以实现调用:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
User user = new User();
user.setAge(1);
user.setName("测试Feign调用");
System.out.println("线程:" + Thread.currentThread().getName() + ",调用Feign");
R post = testFeignClient.post(user);
System.out.println("线程:" + Thread.currentThread().getName() + ",调用结果:" + post);
return chain.filter(exchange);
}
也就是这样其实不会失败,为什么呢?
因为现在相当于直接进行HTTP调用,并没有负载均衡等操作,现在加上,为了更加真实,应该是从Nacos中拉取服务,然后自动负载选择一个实例进行调用:
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- nacos服务发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
此时再去调用就会发现报错了:
block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2
问题原因
在微服务场景下,服务间的调用可以使用Feign的方式,但是网关是Reactor模式,即异步调用模式,而Feign调用为同步方式,这里使用Feign调用,所以这两者之间就会出现矛盾。
解决办法
直接解决报错(不推荐)
自定义一个BlockingLoadBalancerClient.java Bean覆盖原有Bean
public class CustomBlockingLoadBalancerClient extends BlockingLoadBalancerClient {
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;
public CustomBlockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory, LoadBalancerProperties properties) {
super(loadBalancerClientFactory, properties);
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
if (loadBalancer == null) {
return null;
}
CompletableFuture<Response<ServiceInstance>> f = CompletableFuture.supplyAsync(() -> {
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
return loadBalancerResponse;
});
Response<ServiceInstance> loadBalancerResponse = null;
try {
loadBalancerResponse = f.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
if (loadBalancerResponse == null) {
return null;
}
return loadBalancerResponse.getServer();
}
}
创建BlockingLoadBalancerClient类将自定义CustomBlockingLoadBalancerClient注入到容器中
@Configuration
public class BlockingLoadBalancerClientConfig {
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
LoadBalancerProperties properties;
@Bean
public LoadBalancerClient BlockingLoadBalancerClient() {
return new CustomBlockingLoadBalancerClient(loadBalancerClientFactory, properties);
}
}
测试结果:
这种方式可以解决报错的问题。但是,在gateway网关中强行使用feignClient,同步调用,其实是有风险的一个事情。假设feignClient的下游服务,由于某些原因导致性能变慢。而gateway是同步阻塞式的调用。那么gateway的主线程也会被阻塞。由于gateway底层实际上就是netty的线程池,有两个线程池(主从多线程模型)这种模型使用一个独立的线程池来处理连接请求(Acceptor),而I/O操作则由另一个线程池处理。这种方式可以减少连接请求处理对I/O操作的干扰,提高系统并发性能。
线程池转异步(不推荐)
我们使用线程池来将Feign同步调用转为异步调用:
private final ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建线程池
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
User user = new User();
user.setAge(1);
user.setName("测试Feign调用");
Future<R> future = executorService.submit(() -> {
System.out.println("线程:" + Thread.currentThread().getName() + ",调用Feign");
return testFeignClient.post(user);
});
try {
R post = future.get();
System.out.println("线程:" + Thread.currentThread().getName() + ",调用结果:" + post);
boolean flag = (Boolean) post.getData();
if (!flag) {
// 鉴权失败
return onError(exchange, "访问拒绝");
}
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest modifyRequest = request.mutate().header("FLAG", "true").build();
return chain.filter(exchange.mutate().request(modifyRequest).build());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
这种方式确实可行:
但是这种方案并不是真正意义上的异步调用,只不过通过线程池强行提交了feign调用,而且获取feign调用返回结果的future.get()