【深入解析spring cloud gateway】10 用最简单的方式修改gateway请求报文

然而在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变多,就不太优雅了。

二、Gateway是如何实现修改请求报文的

2.1源码分析

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网关修改请求参数,可能只需要几行代码就搞定了。

现在来分析一下这个源码

  • 首先得解析yml中配置,生成config对象,config对象包括了入参需要解析成的class,改body后需要解析成的class,以及一个重写body的接口
  • 生成一个GatewayFilter,对需要拦截的请求,执行apply方法
  • apply方法里面就比较复杂了,但是要修改成我们想要的body体内容的关键是RewriteFunction。其它实际上都是一些基础的操作缓存,流之类的代码。重点代码如下:
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass)
						.flatMap(originalBody -> config.getRewriteFunction().apply(exchange, originalBody))
						.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction().apply(exchange, null)));
  • 最终就是调用RewriteFunction的apply方法来修改body。如果我们能实现自己的getRewriteFunction的话,那修改请求Body就变得简单了。

三、修改请求body的简单示例

我们可以利用好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中的一些实现细节,只是比较麻烦。

你可能感兴趣的:(深入解析SpringCloud,Gateway,gateway,java,windows)