然而在Spring Cloud Gateway中修改报文体似乎并不是一件容易的事。本文尝试用简单的方式解决在Gateway中修改报文。
修改请求报文
@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;
}
}
这种方式只能写在生成Route的地方,一旦api变多,就不太优雅了。
org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
这个类实际上就是重写请求体的一个实现。我们可以参考这个实现,来实现重写请求报文的功能。
先来看一下这个类的源码
public class ModifyRequestBodyGatewayFilterFactory
extends AbstractGatewayFilterFactory<ModifyRequestBodyGatewayFilterFactory.Config> {
private final List<HttpMessageReader<?>> messageReaders;
public ModifyRequestBodyGatewayFilterFactory() {
super(Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public ModifyRequestBodyGatewayFilterFactory(List<HttpMessageReader<?>> messageReaders) {
super(Config.class);
this.messageReaders = messageReaders;
}
@Override
@SuppressWarnings("unchecked")
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
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));
}
@Override
public String toString() {
return filterToStringCreator(ModifyRequestBodyGatewayFilterFactory.this)
.append("Content type", config.getContentType()).append("In class", config.getInClass())
.append("Out class", config.getOutClass()).toString();
}
};
}
protected Mono<Void> release(ServerWebExchange exchange, CachedBodyOutputMessage outputMessage,
Throwable throwable) {
if (outputMessage.isCached()) {
return outputMessage.getBody().map(DataBufferUtils::release).then(Mono.error(throwable));
}
return 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();
}
};
}
/**
这个config表示的是配置,这个类的作用是解析yml配置文件
**/
public static class Config {
//表示输入参数用什么class来解析
private Class inClass;
//表示修改后的请求体用什么class来解析
private Class outClass;
//类型
private String contentType;
//重写请求body的接口
private RewriteFunction rewriteFunction;
public Class getInClass() {
return inClass;
}
public Config setInClass(Class inClass) {
this.inClass = inClass;
return this;
}
public Class getOutClass() {
return outClass;
}
public Config setOutClass(Class outClass) {
this.outClass = outClass;
return this;
}
public RewriteFunction getRewriteFunction() {
return rewriteFunction;
}
public Config setRewriteFunction(RewriteFunction rewriteFunction) {
this.rewriteFunction = rewriteFunction;
return this;
}
public <T, R> Config setRewriteFunction(Class<T> inClass, Class<R> outClass,
RewriteFunction<T, R> rewriteFunction) {
setInClass(inClass);
setOutClass(outClass);
setRewriteFunction(rewriteFunction);
return this;
}
public String getContentType() {
return contentType;
}
public Config setContentType(String contentType) {
this.contentType = contentType;
return this;
}
}
}
代码量很大啊,如果用zuul网关修改请求参数,可能只需要几行代码就搞定了。
现在来分析一下这个源码
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass)
.flatMap(originalBody -> config.getRewriteFunction().apply(exchange, originalBody))
.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction().apply(exchange, null)));
我们可以利用好ModifyRequestBodyGatewayFilterFactory,将修改请求体的主要工作委派给它。实现如下
定义一个GlobalFilter
public class ModifyRequestBodyFilter implements GlobalFilter {
private final Gson gson = new Gson();
private final ModifyRequestBodyGatewayFilterFactory factory = new ModifyRequestBodyGatewayFilterFactory();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ModifyRequestBodyGatewayFilterFactory.Config config = new ModifyRequestBodyGatewayFilterFactory.Config();
config.setInClass(String.class);
config.setOutClass(String.class);
config.setRewriteFunction(new RewriteFunction() {
@Override
public Object apply(Object o, Object o2) {
ServerWebExchange serverWebExchange = (ServerWebExchange) o;
String oldBody = (String) o2;
if (exchange.getRequest().getURI().getRawPath().contains("modifybody")) {
Map map = gson.fromJson(oldBody, Map.class);
map.put("hello", "new body insert!!");
return Mono.just(gson.toJson(map));
}
return Mono.just(oldBody);
}
});
return factory.apply(config).filter(exchange, chain);
}
}
上面的代码逻辑是将url中包含modifybody的请求,往其参数(Map)中新增加一个参数,“hello”, “new body insert!!”。
实现是不是很简单!
后端微服务(hello-service)定义一个接口,验证一下我们的结果
@Controller
@Slf4j
public class ModifyBodyController {
@RequestMapping(value = "/modifybody/hello", method = RequestMethod.POST)
@ResponseBody
public Map<String, String> modify(@RequestBody Map<String, String> map) {
return map;
}
}
请求试一下
curl --location --request POST 'http://localhost:8080/hello-service/gateway/modifybody/hello' \
--header 'Content-Type: application/json' \
--data-raw '{
"aaa": "bbb"
}'
#输出结果如下:
{"aaa":"bbb","hello":"new body insert!!"}
可以看到,我们修改请求报文的目的达到了。
上面我们分析了Gateway修改请求报文的源码,并且利用委派的方式,将修改请求报文的实现细节交给了Gateway的已实现的源码。避免了自己大量操作buffer或者stream的操作。如果不用这种方式的话,大家也可以尝试自己来实现。主要还是ModifyRequestBodyGatewayFilterFactory中的一些实现细节,只是比较麻烦。