Code Analysis For Param-Mapping Plugin
Before starting, you can refer to this article to start the gateway
#
BodyLet's take a look at the structure of this plugin first, as shown in the figure below.
Guess: handler is used for data synchronization; strategy may be adapted to various request bodies, which should be the focus of this plugin; ParamMappingPlugin
should be the implementation of ShenyuPlugin
.
First, take a look at the ParamMappingPlugin
, the focus is on the override of the doExecute
method.
public Mono<Void> doExecute(final ServerWebExchange exchange, final ShenyuPluginChain chain, final SelectorData selector, final RuleData rule) { ... // judge whether paramMappingHandle is null // Determine the request body type according to the contentType in the header line HttpHeaders headers = exchange.getRequest().getHeaders(); MediaType contentType = headers.getContentType(); // * return match(contentType).apply(exchange, chain, paramMappingHandle);}
The match method returns the corresponding
Operator
according to contentTypeprivate Operator match(final MediaType mediaType) { if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { return operatorMap.get(MediaType.APPLICATION_JSON.toString()); } else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) { return operatorMap.get(MediaType.APPLICATION_FORM_URLENCODED.toString()); } else { return operatorMap.get(Constants.DEFAULT); }}
As can be seen from the code of the match method, there are currently three types of
DefaultOperator
,FormDataOperator
, andJsonOperator
, which support the request body in two formats:x-www-form-urlencoded
andjson
.
So let's take a look at what the above three operators are like.
#
1. DefaultOperatorNothing happens, its apply method just continues to execute the plug-in chain, and has no real function. When the request body does not match the Operator, it will be skipped by DefaultOperator
.
#
2. FormDataOperatorThis class is used to process the request body in the format of x-www-form-urlencoded
.
Mainly depends on the apply
method, but it looks a bit strange.
public Mono<Void> apply(final ServerWebExchange exchange, final ShenyuPluginChain shenyuPluginChain, final ParamMappingHandle paramMappingHandle) { return exchange.getFormData() .switchIfEmpty(Mono.defer(() -> Mono.just(new LinkedMultiValueMap<>()))) .flatMap(multiValueMap -> { ... });}
The code in the ellipsis is the processing of the request body, as follows.
// judge whether it is emptyif (Objects.isNull(multiValueMap) || multiValueMap.isEmpty()) { return shenyuPluginChain.execute(exchange);}// convert form-data to jsonString original = GsonUtils.getInstance().toJson(multiValueMap);LOG.info("get from data success data:{}", original);// *modify request body*String modify = operation(original, paramMappingHandle);if (StringUtils.isEmpty(modify)) { return shenyuPluginChain.execute(exchange);}...// Convert the modified json into LinkedMultiValueMap. Pay attention to this line, it will be mentioned later!LinkedMultiValueMap<String, String> modifyMap = GsonUtils.getInstance().toLinkedMultiValueMap(modify);...final BodyInserter bodyInserter = BodyInserters.fromValue(modifyMap);...// modify the request body in the exchange, and then continue to execute the plugin chainreturn bodyInserter.insert(cachedBodyOutputMessage, new BodyInserterContext()) .then(Mono.defer(() -> shenyuPluginChain.execute(exchange.mutate() .request(new ModifyServerHttpRequestDecorator(httpHeaders, exchange.getRequest(), cachedBodyOutputMessage)) .build()) )).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> release(cachedBodyOutputMessage, throwable));
PS: The omitted part is to set the request first and other operations.
The more important thing above should be the modification request body of the star, that is, the call of the operation
method. Here, because of the parameter type, the default method of the Operator
interface will be called first (instead of being overridden by the FormDataOperator
).
default String operation(final String jsonValue, final ParamMappingHandle paramMappingHandle) { DocumentContext context = JsonPath.parse(jsonValue); // call the override operation method and add addParameterKey operation(context, paramMappingHandle); // replace the related replacedParameterKey if (!CollectionUtils.isEmpty(paramMappingHandle.getReplaceParameterKeys())) { paramMappingHandle.getReplaceParameterKeys().forEach(info -> { context.renameKey(info.getPath(), info.getKey(), info.getValue()); }); } // Delete the related removeParameterKey if (!CollectionUtils.isEmpty(paramMappingHandle.getRemoveParameterKeys())) { paramMappingHandle.getRemoveParameterKeys().forEach(info -> { context.delete(info); }); } return context.jsonString();}
After sorting it out, we can find that the json tool JsonPath imported here makes the processing of the request body much simpler and clearer.
In addition, we can notice that the FormDataOperator
overrides the operation(DocumentContext, ParamMappingHandle)
method.
Why override it? There is a default method for handling addParameterKey in the interface.
// Default method in Operator interfacedefault void operation(final DocumentContext context, final ParamMappingHandle paramMappingHandle) { if (!CollectionUtils.isEmpty(paramMappingHandle.getAddParameterKeys())) { paramMappingHandle.getAddParameterKeys().forEach(info -> { context.put(info.getPath(), info.getKey(), info.getValue()); //不同之处 }); }}
// method overridden by FormDataOperator@Overridepublic void operation(final DocumentContext context, final ParamMappingHandle paramMappingHandle) { if (!CollectionUtils.isEmpty(paramMappingHandle.getAddParameterKeys())) { paramMappingHandle.getAddParameterKeys().forEach(info -> { context.put(info.getPath(), info.getKey(), Arrays.asList(info.getValue())); }); }}
In fact, there is such a line in FormDataOperator#apply
(mentioned earlier):
LinkedMultiValueMap<String, String> modifyMap = GsonUtils.getInstance().toLinkedMultiValueMap(modify);
This line converts the modified json into LinkedMultiValueMap
, GsonUtils#toLinkedMultiValueMap
is as follows.
public LinkedMultiValueMap<String, String> toLinkedMultiValueMap(final String json) { return GSON.fromJson(json, new TypeToken<LinkedMultiValueMap<String, String>>() { }.getType());}
The attribute targetMap
in the LinkedMultiValueMap
class is defined as: private final Map<K, List<V>> targetMap
Therefore, the value in the json string must be in the form of a list, otherwise Gson will throw a conversion error exception, which is why the FormDataOperator
must override the operator method.
But why use LinkedMultiValueMap
?
Go back to the first line exchange.getFormData
of the FormDataOperator#apply
method. In SpringMVC, the return value type of DefaultServerWebExchange#getFormData
is Mono<MultiValueMap<String, String>>
, and LinkedMultiValueMap
is a subclass of MultiValueMap
. And, the getFormData
method is for the request body in the format of x-www-form-urlencoded
.
#
三、JsonOperatorObviously, this class is used to process the request body in Json format.
public Mono<Void> apply(final ServerWebExchange exchange, final ShenyuPluginChain shenyuPluginChain, final ParamMappingHandle paramMappingHandle) { ServerRequest serverRequest = ServerRequest.create(exchange, MESSAGE_READERS); Mono<String> mono = serverRequest.bodyToMono(String.class).switchIfEmpty(Mono.defer(() -> Mono.just(""))).flatMap(originalBody -> { LOG.info("get body data success data:{}", originalBody); // call the default operation method to modify the request body String modify = operation(originalBody, paramMappingHandle); return Mono.just(modify); }); BodyInserter bodyInserter = BodyInserters.fromPublisher(mono, String.class); ... //process the header line CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); // modify the request body in the exchange, and then continue to execute the plugin chain return bodyInserter.insert(outputMessage, new BodyInserterContext()) .then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = new ModifyServerHttpRequestDecorator(headers, exchange.getRequest(), outputMessage); return shenyuPluginChain.execute(exchange.mutate().request(decorator).build()); })).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> release(outputMessage, throwable));}
The processing flow of JsonOperator
is roughly similar to that of FormDataOperator
.
#
ConclusionFinally, use a picture to briefly summarize.