Spring Cloud Gateway如何优雅地进行feign调用
作者:mmseoamin日期:2023-12-18

之前写过一篇文章,介绍微服务场景下的权限处理,方案如下:

Spring Cloud Gateway如何优雅地进行feign调用,微服务鉴权,第1张

在实践中,上面的网关选型为Spring Cloud Gateway,所以这里就存在一个问题,即网关如何调用用户服务进行鉴权的问题。

在微服务场景下,服务间的调用可以通过feign的方式,但这里的问题是,网关是reactor模式,即异步调用模式,而feign调用为同步方式,这里直接通过feign调用会报错。

那Spring Cloud Gateway如何优雅的进行feign调用呢,今天的文章带大家来看下。

1 Spring Cloud Gateway直接进行feign调用

不做特殊处理,在Spring Cloud Gateway中直接进行feign调用的代码如下(这里贴出整个鉴权的GatewayFilterFactory代码以方便理解):

@SuppressWarnings("rawtypes")
@Component
@Slf4j
public class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory {
    private static final String USER_HEADER_NAME = "User-Info";
    @Autowired
    private UserClient userClient;
    public ApiAuthGatewayFilterFactory() {
        super(Config.class);
    }
    @Override
    public List shortcutFieldOrder() {
        return Collections.singletonList("checkAuth");
    }
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (config.checkAuth) {
                String cookie = exchange.getRequest().getHeaders().getFirst("Cookie");
                String url = exchange.getRequest().getPath().toString();
                String httpMethod = exchange.getRequest().getMethodValue();
                // 这里调用了feign接口,到用户模块进行鉴权
                ResultResponse resultResponse = userClient.checkPermission(url, httpMethod, cookie);
                if (resultResponse.isSuccess()) {
                    // 鉴权通过,则将用户信息放入header中,传到下游服务
                    ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(resultResponse.getData())).build();
                    return chain.filter(exchange.mutate().request(request).build());
                } else {
                    return Mono.defer(() -> {
                        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                        final ServerHttpResponse response = exchange.getResponse();
                        byte[] bytes = JSON.toJSONString(resultResponse).getBytes(StandardCharsets.UTF_8);
                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                        return response.writeWith(Flux.just(buffer));
                    });
                }
            } else {
                return chain.filter(exchange);
            }
        };
    }
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Config {
        private boolean checkAuth;
    }
}

不出意外的话,你将会出现如下错误:

java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint ⇢ org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ HTTP GET "/api/v1/users/getUserInfo" [ExceptionHandlingWebHandler]

上述错误则说明了,不能再Spring Cloud Gateway中使用同步调用,而普通的feign调用又是同步的,所以会有问题。

2 如何解决Spring Cloud Gateway同步调用feign问题

一、通过线程池来将feign同步调用转为异步调用

在搜索引擎上搜索关于Spring Cloud Gateway调用feign的问题,你可能大概率会得到下面的解决方案,及通过将feign同步调用封装成异步调用来解决。

关键代码如下:

        // 将feign调用封装成异步任务,通过线程池的方式提交
        Future future = executorService.submit(() -> {
            userClient.checkPermission(url, httpMethod, cookie);
        });
        try {
            // 通过future方式获取结果
            ResultResponse resultResponse = (ResultResponse) future.get();
            if (resultResponse.isSuccess()) {
                ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(resultResponse.getData())).build();
                return chain.filter(exchange.mutate().request(request).build());
            } else {
                return Mono.defer(() -> {
                    exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                    final ServerHttpResponse response = exchange.getResponse();
                    byte[] bytes = JSON.toJSONString(resultResponse).getBytes(StandardCharsets.UTF_8);
                    DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                    return response.writeWith(Flux.just(buffer));
                });
            }
        } catch (InterruptedException | ExecutionException e) {
            // ignore exception
        }
        // 异常返回
        return Mono.defer(() -> {
                    exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                    final ServerHttpResponse response = exchange.getResponse();
                    byte[] bytes = JSON.toJSONString("ERROR").getBytes(StandardCharsets.UTF_8);
                    DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                    return response.writeWith(Flux.just(buffer));
                });

遗憾的是,上述代码我在调试的时候虽然能够解决上面block的报错,但是并没有调通,还是会报错,初步定位是异步任务调用获取返回值的时候有问题,因为此处只是作为一个解决思路展示,而且最终也没有采用上述方案,就没有继续花时间去解决了。各位如果有解决该问题的欢迎指教。

二、真正的异步调用——ReactiveFeign

排除方案一的调试问题,假设方案一可以解决feign同步调用的问题,那么该方案有什么问题呢?

在我看来方案一的问题有二:一是并不是真正意义上的异步调用,只不过通过线程池强行提交了feign调用,而且获取feign调用返回结果的future.get()方法也是同步的;二是此种方式实在算不上优雅。

实际上feign无法进行异步调用的问题,早已被程序员们注意到,并且现在已经有了比较成熟的解决方案,即feign-reactive项目,项目地址:GitHub - PlaytikaOSS/feign-reactive。

该项目通过Spring WebClient实现了feign的功能,实现了真正意义上的异步feign调用。

下面就让我们通过使用ReactiveFeign来解决Spring Cloud Gateway调用feign接口的问题,直接看代码(这里贴出整个鉴权的GatewayFilterFactory代码以方便理解):

@Component
@Slf4j
public class ApiAuthGatewayFilterFactory extends AbstractGatewayFilterFactory {
    private static final String USER_HEADER_NAME = "User-Info";
    @Autowired
    private UserReactiveClient userReactiveClient;
    public ApiAuthGatewayFilterFactory() {
        super(Config.class);
    }
    @Override
    public List shortcutFieldOrder() {
        return Collections.singletonList("checkAuth");
    }
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (config.checkAuth) {
                String cookie = exchange.getRequest().getHeaders().getFirst("Cookie");
                String url = exchange.getRequest().getPath().toString();
                String httpMethod = exchange.getRequest().getMethodValue();
                // ReactiveFeign异步调用,获取鉴权结果
                return userReactiveClient.checkPermission(url, httpMethod, cookie).flatMap(commonResponse -> {
                    // 鉴权不通过则返回异常
                    if (!commonResponse.isSuccess()) {
                        return Mono.defer(() -> {
                            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                            final ServerHttpResponse response = exchange.getResponse();
                            byte[] bytes = JSON.toJSONString(commonResponse).getBytes(StandardCharsets.UTF_8);
                            DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                            return response.writeWith(Flux.just(buffer));
                        });
                    } else {
                        // 鉴权通过将用户信息带入后端
                        log.info("User-Info: [{}]", JSON.toJSONString(commonResponse.getData()));
                        ServerHttpRequest request = exchange.getRequest().mutate().header(USER_HEADER_NAME, JSON.toJSONString(commonResponse.getData())).build();
                        return chain.filter(exchange.mutate().request(request).build());
                    }
                });
            } else {
                return chain.filter(exchange);
            }
        };
    }
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Config {
        private boolean checkAuth;
    }
}

上述方案,完美解决了Spring Cloud Gateway同步feign调用的问题,而且看起来也要优雅的多,符合异步编程的风格(上述方案的完整代码,将会在文末给出)。

写在最后

Spring Cloud Gateway通过WebFlux响应式框架实现了全异步处理,看过Spring Cloud Gateway源码的同学应该都深有体会,响应式编程的代码有多么难理解。

正因为Spring Cloud Gateway的响应式编程,导致它直接调用feign会有问题,因为feign的调用是同步调用。

遇到feign同步调用的问题,直接通过线程池强制将feign调用转成异步调用,简单粗暴,在我看来也并不是一个好的方案。

继续深入探究,找到解决feign同步调用问题的根本解决方案,才是一个合格程序员应该做的事。

通过使用ReactiveFeign,可以优雅地解决Spring Cloud Gateway feign同步调用的问题。

完整示例代码,请关注公众号:WU双,对话框回复【网关】即可获取。

完整示例代码除了包含网关的ReactiveFeign异步调用,还包含了XSS过滤器,缓存请求体等网关常用功能。