Spring Cloud Gateway如何优雅地进行feign调用

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

微服务鉴权

在实践中,上面的网关选型为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<ApiAuthGatewayFilterFactory.Config> {

    private static final String USER_HEADER_NAME = "User-Info";

    @Autowired
    private UserClient userClient;

    public ApiAuthGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> 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<ApiAuthGatewayFilterFactory.Config> {

    private static final String USER_HEADER_NAME = "User-Info";

    @Autowired
    private UserReactiveClient userReactiveClient;

    public ApiAuthGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> 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过滤器,缓存请求体等网关常用功能。

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
### 回答1: Spring Cloud Gateway 本身并不能直接使用 FeignFeign 是一个用于声明式、模板化 HTTP 客户端的库,它可以帮助我们更方便地编写基于 HTTP 的服务调用代码。但是 Spring Cloud Gateway 更多地是作为一个 API 网关来使用,用于在微服务架构中进行请求路由、限流、熔断等操作。 虽然 Spring Cloud GatewayFeign 都是基于 Spring 框架的,但它们的使用场景和功能不同,不能直接进行集成。不过,在实际的项目中,我们通常会同时使用 Spring Cloud GatewayFeign 来完成不同的任务。比如,我们可以使用 Spring Cloud Gateway 进行路由、过滤等操作,而使用 Feign 进行服务间的调用。 ### 回答2: Spring Cloud Gateway 确实无法直接使用 Feign 进行服务间的通信。这是因为 Spring Cloud GatewayFeign 是两个不同的组件,具有不同的设计理念和用途。 Spring Cloud Gateway 是一个基于 Spring WebFlux 和 Reactor 的网关服务,用于处理请求路由和转发。它支持使用各种方式定义路由规则,并且具有强大的过滤器功能,可以对请求进行修改和验证。Spring Cloud Gateway 更加注重对请求的处理和转发,而不是直接进行服务间的通信。 相比之下,Feign 是一个基于注解的轻量级 HTTP 客户端,用于服务间的通信。通过定义接口和注解,Feign 可以帮助我们简化 HTTP 请求的编写和发送,从而实现服务之间的互通。Feign 的设计初衷是为了简化服务间通信的代码编写和维护,而不是用于路由和转发请求。 然而,我们可以在 Spring Cloud Gateway 中使用 WebClient 进行服务间的通信。WebClient 是 Spring WebFlux 提供的用于发送 HTTP 请求的非阻塞客户端。我们可以在 Gateway 的过滤器中使用 WebClient 发送请求到目标服务,然后将响应返回给客户端。 综上所述,尽管 Spring Cloud Gateway 无法直接使用 Feign 进行服务间的通信,但我们仍然可以利用 WebClient 在 Gateway 中实现服务间的通信。这种方式可以更好地符合 Spring Cloud Gateway 的设计理念和用途。 ### 回答3: Spring Cloud GatewayFeign是两个完全不同的组件,分别属于Spring Cloud的不同子项目。Spring Cloud Gateway是一个基于Spring Framework5,Spring Boot2和Project Reactor的新一代微服务网关,主要用于路由请求和进行过滤处理。而Feign是一个声明式的HTTP客户端,用于在微服务之间进行远程服务调用。 不能说Spring Cloud Gateway无法集成Feign,因为它们并不是相互替代的关系。在使用Spring Cloud Gateway时,一般是将Feign用作微服务之间的远程服务调用工具,而不是直接集成到Spring Cloud Gateway中。 当需要在Spring Cloud Gateway进行远程服务调用时,可以使用Spring WebFlux提供的WebClient来实现。WebClient是一个响应式的HTTP客户端,可以用于发送请求并接收响应。通过使用WebClient,可以轻松地在Spring Cloud Gateway进行远程服务调用。 总结来说,虽然Spring Cloud Gateway本身不直接支持Feign,但可以使用Spring WebFlux提供的WebClient在Spring Cloud Gateway进行远程服务调用。在使用Spring Cloud Gateway时,可以结合Feign微服务之间进行远程服务调用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值