前言
Spring Cloud Gateway 是 Spring Cloud 新推出的網(wǎng)關(guān)框架,之前是 Netflix Zuul。網(wǎng)關(guān)通常在項(xiàng)目中為了簡(jiǎn)化前端的調(diào)用邏輯,同時(shí)也簡(jiǎn)化內(nèi)部服務(wù)之間互相調(diào)用的復(fù)雜度;具體作用就是轉(zhuǎn)發(fā)服務(wù),接收并轉(zhuǎn)發(fā)所有內(nèi)外部的客戶端調(diào)用;其他常見(jiàn)的功能還有權(quán)限認(rèn)證,限流控制等等。
我們都知道,由于Spring Cloud Gateway是基于Spring5開(kāi)發(fā)的,在Web框架上,Spring Cloud Gateway采用了自家新推出的Web框架WebFlux。由于WebFlux底層是采用Reactive Netty的NIO框架,所以無(wú)論在網(wǎng)絡(luò)編程方面與傳統(tǒng)的WebMvc都有所不同,雖然WebFlux可以完全兼容舊的WebMvc寫(xiě)法,但這并不代表我們的代碼可以完全遷移沒(méi)有任何問(wèn)題。
最近在項(xiàng)目中,在使用Spring Cloud Gateway的過(guò)程中,發(fā)現(xiàn)有幾個(gè)坑需要我們?nèi)プ⒁馇疫M(jìn)行改造修復(fù),在此分享一下,我在項(xiàng)目中使用Gateway遇到的問(wèn)題及解決方案。
1. 千萬(wàn)別依賴(lài)Undertow
我們?cè)陂_(kāi)發(fā)SpringBoot應(yīng)用中都會(huì)把spring-web-starter默認(rèn)依賴(lài)的Web容器Tomcat排除掉,并添加上undertow的依賴(lài),使用undertow作為我們的Web運(yùn)行容器。由于undertow與tomcat相比性能會(huì)更優(yōu)一些,具體原因不再此贅述,感興趣的同學(xué)可以看下這篇文章:http://www.itdecent.cn/p/f7cb40a8ce22
上面提到Gateway是基于WebFlux開(kāi)發(fā),WebFlux是基于NIO的Web框架,所以要注意在添加了spring-cloud-starter-gateway依賴(lài)的項(xiàng)目中,不可再添加undertow依賴(lài)。之所以說(shuō)這是個(gè)坑,是因?yàn)樘砑恿藆ndertow依賴(lài)后,gateway仍可以正常啟動(dòng),不會(huì)有任何報(bào)錯(cuò),我們并不容易察覺(jué)。當(dāng)我們啟動(dòng)后,發(fā)送請(qǐng)求進(jìn)入Gateway你就會(huì)發(fā)現(xiàn)會(huì)出現(xiàn)一個(gè)DataBuffey類(lèi)型轉(zhuǎn)換的錯(cuò)誤,代碼出問(wèn)題的地方出現(xiàn)在NettyRoutingFilter 139行,如下代碼:
return nettyOutbound.options(NettyPipeline.SendOptions::flushOnEach)
.send(request.getBody()
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
.getNativeBuffer()));
這里我們可以看到dataBuffer是直接強(qiáng)轉(zhuǎn)成NettyDataBuffer類(lèi)型,而當(dāng)我們依賴(lài)中加入了undertow此處便為報(bào)類(lèi)型轉(zhuǎn)換異常,原因是因?yàn)镚ateway基于NIO,而Undertow是基于BIO,而這里由于是undertow處理的請(qǐng)求,所以dataBuffer并不能強(qiáng)軒成NIO的NettyDataBuffer類(lèi)型。所以注意,在Gateway項(xiàng)目中千萬(wàn)要記住不可再添加undertow依賴(lài),否則你會(huì)發(fā)現(xiàn),會(huì)有很多讓你覺(jué)得不可思議的錯(cuò)誤出現(xiàn)。
2. Form表單數(shù)據(jù)重復(fù)讀取
我們都知道InputStream只能允許我們讀取一次,在我使用Gateway的過(guò)程中,由于需要在網(wǎng)關(guān)中執(zhí)行一些用戶鑒權(quán)的邏緝,而在一個(gè)獲取賬戶明細(xì)的接口中,我們可以從請(qǐng)求頭、Url請(qǐng)求參數(shù)、Form Body這三個(gè)地方去獲取用戶的Token來(lái)進(jìn)行鑒權(quán)校驗(yàn)。在Gateway中,我們通過(guò)實(shí)現(xiàn)WebFlux的WebFilter接口來(lái)實(shí)現(xiàn)一個(gè)過(guò)濾,以校驗(yàn)用戶Token,以下是我寫(xiě)的Filter,校驗(yàn)邏緝做了刪減:
@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對(duì)象中獲取到請(qǐng)求頭和Url請(qǐng)求參數(shù),而當(dāng)我想從FormData中取參時(shí),卻發(fā)現(xiàn)并不能輕易的調(diào)用取到參數(shù)。于是,通過(guò)查閱WebFlux官方API文檔,找到了獲取表單Body的方法,如下圖:

于是,我便按照上述的方法取出表單數(shù)據(jù),并從Map中取到了token,代碼如下:
exchange.getFormData().flatMap(formData->{
String token = formData.getFirst(TOKEN_PARAM_KEY);
...
return chain.filter(exchange);
});
以上方式看上去雖然麻煩了點(diǎn),但還算是拿到了token,實(shí)現(xiàn)了鑒權(quán)的邏緝,但當(dāng)過(guò)濾器執(zhí)行完成后,進(jìn)入到獲取賬戶明細(xì)的Controller中時(shí),我發(fā)現(xiàn)Form表單傳的參數(shù)不見(jiàn)了,在Controller并不能接收到前端傳過(guò)來(lái)的參數(shù)。此時(shí),我便想到在Filter中取過(guò)一次FormData,應(yīng)該問(wèn)題出在此處。那么,該如何解決這個(gè)問(wèn)題呢?我們想在Filter取到表單參數(shù),又想在Controller中能夠正常接收參數(shù)。于是,我便想到是否能夠讓這個(gè)FormData支持多次讀取,而FormData是從exchange中取出來(lái)的,于是只要解決好Exchange這個(gè)對(duì)象就可以實(shí)現(xiàn),決定對(duì)exchange再做一次封裝。通過(guò)網(wǎng)上搜索,找到了網(wǎng)友對(duì)exchange二次封裝的編碼實(shí)現(xiàn),下面直接出代碼:
Request裝飾類(lèi)
創(chuàng)建包裝類(lèi)PartnerServerHttpRequestDecorator繼承ServerHttpRequestDecorator,在含參構(gòu)造放中打印請(qǐng)求url,query,headers和報(bào)文信息。
@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裝飾類(lèi)
創(chuàng)建響應(yīng)裝飾類(lèi)PartnerServerHttpResponseDecorator繼承ServerHttpResponseDecorator
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裝飾類(lèi)
創(chuàng)建PayloadServerWebExchangeDecorator類(lèi)繼承ServerWebExchangeDecorator
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;
}
}
實(shí)現(xiàn)思路,其實(shí)很簡(jiǎn)單,通過(guò)封裝ServerHttpRequestDecorator,將body作為成員變量緩存起來(lái),方便后面隨時(shí)獲取調(diào)用。最后使用方式很簡(jiǎn)單,我采用的方式是直接新創(chuàng)建一個(gè)Filter在所有自定義過(guò)濾器之前執(zhí)行,用來(lái)讀取FormData,以便后面的Filter使用,代碼如下:
@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);
}
}
通過(guò)在exchange中setAttribute的方式,在后面的Filter中直接getAttribute()的方式,方便的取到表單數(shù)據(jù)完成校驗(yàn)邏緝。
3. @RequestParam無(wú)法接收post的FormData數(shù)據(jù)
這個(gè)問(wèn)題也是很坑,通過(guò)查看WebFlux文檔
The Servlet API “request parameter” concept conflates query parameters, form data, and multiparts into one. However, in WebFlux, each is accessed individually through
ServerWebExchange. While@RequestParambinds to query parameters only, you can use data binding to apply query parameters, form data, and multiparts to a command object.
文檔中已經(jīng)明確說(shuō)明了webflux中,該注解僅支持url傳參方式
解決方案其實(shí)比較直接,既然WebFlux不幫我們賦值,我們便自己實(shí)現(xiàn),為此通過(guò)閱讀文檔,可以采用自定義實(shí)現(xiàn)一個(gè)RequestParamMethodArgumentResolver的方式,去定義我們的表單參數(shù)映射。實(shí)現(xiàn)代碼如下:
@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;
}
}
}
通過(guò)實(shí)現(xiàn)WebFluxConfigurer接口,將我們自定義的ArgumentResolver注冊(cè)到配置中去,在獲取FormData的方式上,也是延用了上述的方式,從exchange的attribute中去獲取,然后剩下的就是表單K/V對(duì)的賦值邏緝實(shí)現(xiàn)了,這個(gè)比較簡(jiǎn)單,就不在此贅述了。
以上是我在使用Spring Cloud Gateway中遇到的3個(gè)比較大的問(wèn)題,且都一一完成了填坑,希望給夠給予開(kāi)發(fā)者們一些指導(dǎo)與幫助。