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

然而在Spring Cloud Gateway中修改报文体似乎并不是一件容易的事。本文尝试用简单的方式解决在Gateway中修改报文。

一、官方示例

修改请求报文

java 复制代码
@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

这个类实际上就是重写请求体的一个实现。我们可以参考这个实现,来实现重写请求报文的功能。

先来看一下这个类的源码

java 复制代码
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。其它实际上都是一些基础的操作缓存,流之类的代码。重点代码如下:
java 复制代码
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

java 复制代码
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)定义一个接口,验证一下我们的结果

java 复制代码
@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;
    }
}

请求试一下

bash 复制代码
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中的一些实现细节,只是比较麻烦。

相关推荐
Chase_Mos21 分钟前
Spring 必会之微服务篇(1)
java·spring·微服务
小林学习编程3 小时前
SpringBoot校园失物招领信息平台
java·spring boot·后端
撸码到无法自拔3 小时前
docker常见命令
java·spring cloud·docker·容器·eureka
heart000_13 小时前
IDEA 插件推荐:提升编程效率
java·ide·intellij-idea
IT专业服务商4 小时前
联想 SR550 服务器,配置 RAID 5教程!
运维·服务器·windows·microsoft·硬件架构
海尔辛4 小时前
学习黑客5 分钟小白弄懂Windows Desktop GUI
windows·学习
ŧ榕树先生4 小时前
查看jdk是否安装并且配置成功?(Android studio安装前的准备)
java·jdk
未来的JAVA高级开发工程师4 小时前
适配器模式
java
gushansanren4 小时前
基于WSL用MSVC编译ffmpeg7.1
windows·ffmpeg
LUCIAZZZ4 小时前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot