抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

前言

Spring Cloud Gateway 是 Spring Cloud 新推出的网关框架,之前是 Netflix
Zuul。网关通常在项目中为了简化前端的调用逻辑,同时也简化内部服务之间互相调用的复杂度;具体作用就是转发服务,接收并转发所有内外部的客户端调用;其他常见的功能还有权限认证,限流控制等等。
我们都知道,由于Spring Cloud Gateway是基于Spring5开发的,在Web框架上,Spring Cloud
Gateway采用了自家新推出的Web框架WebFlux。由于WebFlux底层是采用Reactive
Netty的NIO框架,所以无论在网络编程方面与传统的WebMvc都有所不同,虽然WebFlux可以完全兼容旧的WebMvc写法,但这并不代表我们的代码可以完全迁移没有任何问题。
最近在项目中,在使用Spring Cloud Gateway的过程中,发现有几个坑需要我们去注意且进行改造修复,在此分享一下,我在项目中使用Gateway遇到的问题及解决方案。

1. 千万别依赖Undertow

我们在开发SpringBoot应用中都会把spring-web-starter默认依赖的Web容器Tomcat排除掉,并添加上undertow的依赖,使用undertow作为我们的Web运行容器。由于undertow与tomcat相比性能会更优一些,具体原因不再此赘述,感兴趣的同学可以看下这篇文章:https://www.jianshu.com/p/f7cb40a8ce22
上面提到Gateway是基于WebFlux开发,WebFlux是基于NIO的Web框架,所以要注意在添加了spring-cloud-starter-gateway依赖的项目中,不可再添加undertow依赖。之所以说这是个坑,是因为添加了undertow依赖后,gateway仍可以正常启动,不会有任何报错,我们并不容易察觉。当我们启动后,发送请求进入Gateway你就会发现会出现一个DataBuffey类型转换的错误,代码出问题的地方出现在NettyRoutingFilter
139行,如下代码:

1
2
3
4
return nettyOutbound.options(NettyPipeline.SendOptions::flushOnEach)
.send(request.getBody()
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
.getNativeBuffer()));

这里我们可以看到dataBuffer是直接强转成NettyDataBuffer类型,而当我们依赖中加入了undertow此处便为报类型转换异常,原因是因为Gateway基于NIO,而Undertow是基于BIO,而这里由于是undertow处理的请求,所以dataBuffer并不能强轩成NIO的NettyDataBuffer类型。所以注意,在Gateway项目中千万要记住不可再添加undertow依赖,否则你会发现,会有很多让你觉得不可思议的错误出现。

2. Form表单数据重复读取

我们都知道InputStream只能允许我们读取一次,在我使用Gateway的过程中,由于需要在网关中执行一些用户鉴权的逻缉,而在一个获取账户明细的接口中,我们可以从请求头、Url请求参数、Form
Body这三个地方去获取用户的Token来进行鉴权校验。在Gateway中,我们通过实现WebFlux的WebFilter接口来实现一个过滤,以校验用户Token,以下是我写的Filter,校验逻缉做了删减:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE+2)
public class AccountContextFilter implements WebFilter {

private final static String TOKEN_PARAM_KEY = "access_token";

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

ServerHttpRequest request = exchange.getRequest();
HttpHeaders header = request.getHeaders();
String token = header.getFirst(AuthConstants.HEADER_AUTH);
if (StringUtils.isBlank(token)) {
token = request.getQueryParams().getFirst(TOKEN_PARAM_KEY);
//若为空,尝试从FormData取参
}
}
}

从上面的代码,我们可以很轻易的从exchange获取到的request对象中获取到请求头和Url请求参数,而当我想从FormData中取参时,却发现并不能轻易的调用取到参数。于是,通过查阅WebFlux官方API文档,找到了获取表单Body的方法,如下图:
jianshu3.webp
于是,我便按照上述的方法取出表单数据,并从Map中取到了token,代码如下:

1
2
3
4
5
exchange.getFormData().flatMap(formData->{
String token = formData.getFirst(TOKEN_PARAM_KEY);
...
return chain.filter(exchange);
});

以上方式看上去虽然麻烦了点,但还算是拿到了token,实现了鉴权的逻缉,但当过滤器执行完成后,进入到获取账户明细的Controller中时,我发现Form表单传的参数不见了,在Controller并不能接收到前端传过来的参数。此时,我便想到在Filter中取过一次FormData,应该问题出在此处。那么,该如何解决这个问题呢?我们想在Filter取到表单参数,又想在Controller中能够正常接收参数。于是,我便想到是否能够让这个FormData支持多次读取,而FormData是从exchange中取出来的,于是只要解决好Exchange这个对象就可以实现,决定对exchange再做一次封装。通过网上搜索,找到了网友对exchange二次封装的编码实现,下面直接出代码:
Request装饰类
创建包装类PartnerServerHttpRequestDecorator继承ServerHttpRequestDecorator,在含参构造放中打印请求url,query,headers和报文信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
public class PartnerServerHttpRequestDecorator extends ServerHttpRequestDecorator {

private Flux<DataBuffer> body;

PartnerServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
final String path = delegate.getURI().getPath();
final String query = delegate.getURI().getQuery();
final String method = Optional.ofNullable(delegate.getMethod()).orElse(HttpMethod.GET).name();
final String headers = delegate.getHeaders().entrySet()
.stream()
.map(entry -> " " + entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
.collect(Collectors.joining("\n"));
final MediaType contentType = delegate.getHeaders().getContentType();
if (log.isDebugEnabled()) {
log.debug("\n" +
"HttpMethod : {}\n" +
"Uri : {}\n" +
"Headers : \n" +
"{}", method, path + (StrUtil.isEmpty(query) ? "" : "?" + query), headers);
}
Flux<DataBuffer> flux = super.getBody();
body = flux;
}

@Override
public Flux<DataBuffer> getBody() {
return body;
}

}

Response装饰类
创建响应装饰类PartnerServerHttpResponseDecorator继承ServerHttpResponseDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PartnerServerHttpResponseDecorator extends ServerHttpResponseDecorator {

PartnerServerHttpResponseDecorator(ServerHttpResponse delegate) {
super(delegate);
}

@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return super.writeAndFlushWith(body);
}

@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return super.writeWith(body);
}
}

WebExchange装饰类
创建PayloadServerWebExchangeDecorator类继承ServerWebExchangeDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PayloadServerWebExchangeDecorator extends ServerWebExchangeDecorator {

private PartnerServerHttpRequestDecorator requestDecorator;

private PartnerServerHttpResponseDecorator responseDecorator;

public PayloadServerWebExchangeDecorator(ServerWebExchange delegate) {
super(delegate);
requestDecorator = new PartnerServerHttpRequestDecorator(delegate.getRequest());
responseDecorator = new PartnerServerHttpResponseDecorator(delegate.getResponse());
}

@Override
public ServerHttpRequest getRequest() {
return requestDecorator;
}

@Override
public ServerHttpResponse getResponse() {
return responseDecorator;
}
}

实现思路,其实很简单,通过封装ServerHttpRequestDecorator,将body作为成员变量缓存起来,方便后面随时获取调用。最后使用方式很简单,我采用的方式是直接新创建一个Filter在所有自定义过滤器之前执行,用来读取FormData,以便后面的Filter使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyReadMoreFilter implements WebFilter {

public final static String FORM_DATA_ATTR = "fromData";

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
PayloadServerWebExchangeDecorator payloadServerWebExchangeDecorator = new PayloadServerWebExchangeDecorator(exchange);
// mediaType
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();

if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {
return payloadServerWebExchangeDecorator.getFormData().flatMap(formData->{
payloadServerWebExchangeDecorator.getAttributes().put(FORM_DATA_ATTR,formData);
return chain.filter(payloadServerWebExchangeDecorator);
});
}
return chain.filter(payloadServerWebExchangeDecorator);
}
}

通过在exchange中setAttribute的方式,在后面的Filter中直接getAttribute()的方式,方便的取到表单数据完成校验逻缉。

3. @RequestParam无法接收post的FormData数据

这个问题也是很坑,通过查看WebFlux文档

https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-ann-requestparam

The Servlet API “request parameter” concept conflates query parameters, form data, and multiparts into one. However,
in WebFlux, each is accessed individually throughServerWebExchange. While@RequestParambinds to query parameters
only, you can use data binding to apply query parameters, form data, and multiparts to
acommand object.

文档中已经明确说明了webflux中,该注解仅支持url传参方式
解决方案其实比较直接,既然WebFlux不帮我们赋值,我们便自己实现,为此通过阅读文档,可以采用自定义实现一个RequestParamMethodArgumentResolver的方式,去定义我们的表单参数映射。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Configuration
public class WebArgumentResolversConfig implements WebFluxConfigurer {

@Autowired
ConfigurableApplicationContext applicationContext;

@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(new FormDataMethodArgumentResolver(applicationContext.getBeanFactory(), ReactiveAdapterRegistry.getSharedInstance(), true));
}

class FormDataMethodArgumentResolver extends RequestParamMethodArgumentResolver {

public FormDataMethodArgumentResolver(ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry, boolean useDefaultResolution) {
super(factory, registry, useDefaultResolution);
}

@Override
protected Object resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
MultiValueMap<String, String> requestParams = exchange.getRequest().getQueryParams();
Object value = resolveParameterByParam(name, parameter, requestParams);
MultiValueMap<String,Object> formMap = (MultiValueMap<String,Object>)exchange.getAttributes().get(RequestBodyReadMoreFilter.FORM_DATA_ATTR);
if(value == null && formMap != null) {
value = resolveParameterByForm(name, parameter, formMap);
}
return value;
}

private Object resolveParameterByParam(String name,MethodParameter parameter,MultiValueMap<String,String> data){
List<String> values = data.get(name);
if (values != null && values.size() > 0 && parameter.getParameterType() == List.class) {
return values;
} else if (values != null && values.size() > 0) {
return data.getFirst(name);
}
return null;
}

private Object resolveParameterByForm(String name,MethodParameter parameter,MultiValueMap<String,Object> data){
List<Object> values = data.get(name);
if (values != null && values.size() > 0 && parameter.getParameterType() == List.class) {
return values;
} else if (values != null && values.size() > 0) {
return data.getFirst(name);
}
return null;
}

}

}

通过实现WebFluxConfigurer接口,将我们自定义的ArgumentResolver注册到配置中去,在获取FormData的方式上,也是延用了上述的方式,从exchange的attribute中去获取,然后剩下的就是表单K/V对的赋值逻缉实现了,这个比较简单,就不在此赘述了。
以上是我在使用Spring Cloud Gateway中遇到的3个比较大的问题,且都一一完成了填坑,希望给够给予开发者们一些指导与帮助。

评论