修改請求報(bào)文螟蝙、響應(yīng)報(bào)文是API網(wǎng)關(guān)框架的基礎(chǔ)功能,然而在Spring Cloud Gateway中修改報(bào)文體似乎并不是一件容易的事在塔,本文以3.0.3
版本為例胳搞,講講在Spring Cloud Gateway如何優(yōu)雅的修改請求報(bào)文、響應(yīng)報(bào)文姥闪。
一始苇、官方方法
在Spring Cloud Gateway官方文檔中,有如下方法筐喳,可供參考:
1.1 修改請求報(bào)文
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
.build();
}
static class Hello {
String message;
public Hello() { }
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
1.2 修改響應(yīng)報(bào)文
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
.build();
}
當(dāng)然催式,這種方式有其局限性:
- 只能寫死在生成Route的地方,一旦API變多避归,或者是動態(tài)路由荣月,不太優(yōu)雅
- 無法在自定義的Global Filter、Gateway Filter中直接調(diào)用
二梳毙、優(yōu)雅實(shí)現(xiàn)
一開始哺窄,當(dāng)我接觸Spring Cloud Gateway時(shí),想自己通過實(shí)現(xiàn)Global Filter實(shí)現(xiàn)修改請求報(bào)文、響應(yīng)報(bào)文萌业,摸不著頭腦坷襟。一個(gè)看似很簡單的問題,在zuul1中只需要修改兩下變量咽白,就可以輕松改掉啤握。換了異步非阻塞的Spring Cloud Gateway,仿若掉入了天坑晶框,想修改一次排抬,沒有100行代碼,辦不了這個(gè)事情授段。
看互聯(lián)網(wǎng)上有很多文章蹲蒲,代碼不僅冗余、復(fù)雜侵贵、不夠優(yōu)雅届搁、易讀性差,還不能夠支持HTTP 1.1窍育、Gzip卡睦,總給人一種hacky實(shí)現(xiàn)的感覺。這就讓我頓時(shí)疑惑了起來漱抓,一個(gè)堂堂的Gateway網(wǎng)關(guān)表锻,修改請求報(bào)文、響應(yīng)報(bào)文居然要這么麻煩乞娄。
后來瞬逊,隨著閱讀官方文檔、官方源碼的不斷深入仪或,我理解了其實(shí)Spring Cloud Gateway的初衷确镊,似乎并不是想做一個(gè)網(wǎng)關(guān)“框架”,而更像是做一個(gè)開箱即用的網(wǎng)關(guān)應(yīng)用程序范删,任何網(wǎng)關(guān)相關(guān)的參數(shù)蕾域,均可通過參數(shù)配置實(shí)現(xiàn),無需自行編碼瓶逃,或者使用輕量級的函數(shù)式編程語句束铭。確實(shí),這很好厢绝,對于微服務(wù)網(wǎng)關(guān)契沫,足夠了。但是昔汉,如果要深度定制網(wǎng)關(guān)的功能懈万,就會感到十分為難拴清,一個(gè)封裝十足徹底的工具,要想不動引用包源碼的情況下会通,從外層修改它口予,猶如把一個(gè)豪華法拉利改裝成特斯拉,使用網(wǎng)上的hacky辦法涕侈,總給人一種沪停,里外里套了兩層的感覺。
2.1 實(shí)現(xiàn)原理
為了解決不夠優(yōu)雅的問題裳涛,通過借鑒Spring Cloud Gateway 如下類的 原生的rewrite方法木张,重新實(shí)現(xiàn)Config的響應(yīng)式參數(shù)傳遞,從而實(shí)現(xiàn)在Filter中修改請求報(bào)文端三、響應(yīng)報(bào)文的函數(shù)式編程舷礼,一勞永逸。
org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory
通過該方式實(shí)現(xiàn)修改body體郊闯,相較于網(wǎng)絡(luò)上的通用方法妻献,好處如下:
- 代碼統(tǒng)一封裝,不用牽一發(fā)動全身团赁;
- 函數(shù)式編程育拨,實(shí)現(xiàn)優(yōu)雅;
- 支持gzip欢摄、chunked等HTTP特性至朗;
- 請求、響應(yīng)的修改剧浸,都還在Filter中修改;
值得注意的是矗钟,需要對Mono或Flux的異常進(jìn)行捕獲唆香,捕獲方式不一定是try catch的方式,而是.just(xxx).doOnError()
2.2 基礎(chǔ)封裝
在工程中吨艇,創(chuàng)建3個(gè)類躬它,放到基礎(chǔ)目錄下,用于調(diào)用东涡,如果Spring Cloud Gateway更新了請求冯吓、響應(yīng)相關(guān)的代碼,只需更新如下代碼即可疮跑。
RewriteConfig.java
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import java.util.Map;
public class RewriteConfig {
private Class inClass;
private Class outClass;
private Map<String, Object> inHints;
private Map<String, Object> outHints;
private String newContentType;
private String contentType;
private RewriteFunction rewriteFunction;
public Class getInClass() {
return inClass;
}
public RewriteConfig setInClass(Class inClass) {
this.inClass = inClass;
return this;
}
public Class getOutClass() {
return outClass;
}
public RewriteConfig setOutClass(Class outClass) {
this.outClass = outClass;
return this;
}
public Map<String, Object> getInHints() {
return inHints;
}
public RewriteConfig setInHints(Map<String, Object> inHints) {
this.inHints = inHints;
return this;
}
public Map<String, Object> getOutHints() {
return outHints;
}
public RewriteConfig setOutHints(Map<String, Object> outHints) {
this.outHints = outHints;
return this;
}
public String getNewContentType() {
return newContentType;
}
public RewriteConfig setNewContentType(String newContentType) {
this.newContentType = newContentType;
return this;
}
public RewriteFunction getRewriteFunction() {
return rewriteFunction;
}
public RewriteConfig setRewriteFunction(RewriteFunction rewriteFunction) {
this.rewriteFunction = rewriteFunction;
return this;
}
public <T, R> RewriteConfig setRewriteFunction(Class<T> inClass, Class<R> outClass,
RewriteFunction<T, R> rewriteFunction) {
setInClass(inClass);
setOutClass(outClass);
setRewriteFunction(rewriteFunction);
return this;
}
public String getContentType() {
return "application/json;charset=utf-8";
}
public RewriteConfig setContentType(String contentType) {
this.contentType = contentType;
return this;
}
}
ModifiedRequestDecorator.java
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
public class ModifiedRequestDecorator {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
private final RewriteConfig config;
public ModifiedRequestDecorator(ServerWebExchange exchange, RewriteConfig config) {
this.config = config;
}
@SuppressWarnings("unchecked")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Class inClass = config.getInClass();
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
// TODO: flux or mono
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass)
.flatMap(originalBody -> config.getRewriteFunction().apply(exchange, originalBody))
.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction().apply(exchange, null)));
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, config.getOutClass());
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
// if the body is changing content types, set it here, to the bodyInserter
// will know about it
if (config.getContentType() != null) {
headers.set(HttpHeaders.CONTENT_TYPE, config.getContentType());
}
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
// .log("modify_request", Level.INFO)
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
return chain.filter(exchange.mutate().request(decorator).build());
})).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> release(exchange,
outputMessage, throwable));
}
protected Mono<Void> release(ServerWebExchange exchange, CachedBodyOutputMessage outputMessage,
Throwable throwable) {
return outputMessage.getBody().map(DataBufferUtils::release).then(Mono.error(throwable));
}
ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers,
CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
}
else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
}
ModifiedResponseDecorator.java
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver;
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyDecoder;
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.function.Function.identity;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR;
public class ModifiedResponseDecorator extends ServerHttpResponseDecorator {
private final ServerWebExchange exchange;
private final RewriteConfig config;
private final Map<String, MessageBodyDecoder> messageBodyDecoders ;
private final Map<String, MessageBodyEncoder> messageBodyEncoders;
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
public ModifiedResponseDecorator(ServerWebExchange exchange, RewriteConfig config) {
super(exchange.getResponse());
this.exchange = exchange;
this.config = config;
Set<MessageBodyDecoder> messageBodyDecodersSet = new HashSet<>();
Set<MessageBodyEncoder> messageBodyEncodersSet = new HashSet<>();
MessageBodyDecoder messageBodyDecoder = new GzipMessageBodyResolver();
MessageBodyEncoder messageBodyEncoder = new GzipMessageBodyResolver();
messageBodyDecodersSet.add(messageBodyDecoder);
messageBodyEncodersSet.add(messageBodyEncoder);
this.messageBodyDecoders = messageBodyDecodersSet.stream()
.collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity()));
this.messageBodyEncoders = messageBodyEncodersSet.stream()
.collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity()));
}
@SuppressWarnings("unchecked")
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Class inClass = config.getInClass();
Class outClass = config.getOutClass();
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
HttpHeaders httpHeaders = new HttpHeaders();
// explicitly add it in this way instead of
// 'httpHeaders.setContentType(originalResponseContentType)'
// this will prevent exception in case of using non-standard media
// types like "Content-Type: image"
httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
ClientResponse clientResponse = prepareClientResponse(body, httpHeaders);
// TODO: flux or mono
Mono modifiedBody = extractBody(exchange, clientResponse, inClass)
.flatMap(originalBody -> config.getRewriteFunction().apply(exchange, originalBody))
.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction().apply(exchange, null)));
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
exchange.getResponse().getHeaders());
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
Mono<DataBuffer> messageBody = writeBody(getDelegate(), outputMessage, outClass);
HttpHeaders headers = getDelegate().getHeaders();
if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)
|| headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
}
// TODO: fail if isStreamingMediaType?
return getDelegate().writeWith(messageBody);
}));
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
private ClientResponse prepareClientResponse(Publisher<? extends DataBuffer> body, HttpHeaders httpHeaders) {
ClientResponse.Builder builder;
builder = ClientResponse.create(exchange.getResponse().getStatusCode(), messageReaders);
return builder.headers(headers -> headers.putAll(httpHeaders)).body(Flux.from(body)).build();
}
private <T> Mono<T> extractBody(ServerWebExchange exchange, ClientResponse clientResponse, Class<T> inClass) {
// if inClass is byte[] then just return body, otherwise check if
// decoding required
if (byte[].class.isAssignableFrom(inClass)) {
return clientResponse.bodyToMono(inClass);
}
List<String> encodingHeaders = exchange.getResponse().getHeaders().getOrEmpty(HttpHeaders.CONTENT_ENCODING);
for (String encoding : encodingHeaders) {
MessageBodyDecoder decoder = messageBodyDecoders.get(encoding);
if (decoder != null) {
return clientResponse.bodyToMono(byte[].class).publishOn(Schedulers.parallel()).map(decoder::decode)
.map(bytes -> exchange.getResponse().bufferFactory().wrap(bytes))
.map(buffer -> prepareClientResponse(Mono.just(buffer),
exchange.getResponse().getHeaders()))
.flatMap(response -> response.bodyToMono(inClass));
}
}
return clientResponse.bodyToMono(inClass);
}
private Mono<DataBuffer> writeBody(ServerHttpResponse httpResponse, CachedBodyOutputMessage message,
Class<?> outClass) {
Mono<DataBuffer> response = DataBufferUtils.join(message.getBody());
if (byte[].class.isAssignableFrom(outClass)) {
return response;
}
List<String> encodingHeaders = httpResponse.getHeaders().getOrEmpty(HttpHeaders.CONTENT_ENCODING);
for (String encoding : encodingHeaders) {
MessageBodyEncoder encoder = messageBodyEncoders.get(encoding);
if (encoder != null) {
DataBufferFactory dataBufferFactory = httpResponse.bufferFactory();
response = response.publishOn(Schedulers.parallel()).map(buffer -> {
byte[] encodedResponse = encoder.encode(buffer);
DataBufferUtils.release(buffer);
return encodedResponse;
}).map(dataBufferFactory::wrap);
break;
}
}
return response;
}
}
修改請求
filter()
方法返回參考代碼
// 修改請求內(nèi)容
return new ModifiedRequestDecorator(exchange, new RewriteConfig()
.setRewriteFunction(String.class, String.class, (ex, requestData)
-> Mono.just(要修改請求內(nèi)容的方法(requestData))
)).filter(exchange, chain);
修改響應(yīng)
filter()
方法返回參考代碼
// 修改響應(yīng)內(nèi)容
return chain.filter(exchange.mutate().response(
new ModifiedResponseDecorator(exchange, new RewriteConfig().
setRewriteFunction(String.class, String.class, (ex, responseData)
-> Mono.just(要修改響應(yīng)內(nèi)容的方法(responseData))
))).build());
修改請求组贺、響應(yīng)
filter()
方法返回參考代碼
// 修改請求內(nèi)容
return new ModifiedRequestDecorator(exchange, new RewriteConfig()
.setRewriteFunction(String.class, String.class, (ex, requestData)
-> Mono.just(要修改請求內(nèi)容的方法(requestData))
)).filter(exchange.mutate().response(
// 修改響應(yīng)內(nèi)容
new ModifiedResponseDecorator(exchange, new RewriteConfig().
setRewriteFunction(String.class, String.class, (ex, responseData)
-> Mono.just(要修改響應(yīng)內(nèi)容的方法(responseData))
))).build(),chain);