review
In the Soul Request Processing Overview overview article, we learned that Soul’s request-specific processing repository is excute in DefaultSoulPluginChain, which implements a plugin chain pattern to complete the processing of the request.
We summarized the plugins injected into the plugins in general, but even then, we still couldn’t see the whole picture. For this purpose, we specifically combed the classes involved in soul plug-in. The overall combing results are shown in the following figure.
In the carding article, we can see that the core classes are SoulPlugin, PluginEnum, PluginDataHandler and MetaDataSubscriber. In the articles related to carding request, we only need to focus on SoulPlugin and PluginEnum classes.
Now that we know about the SoulPlugin class, what is the main purpose of the PluginEnum enum class?
PluginEnum: An enumerated class of plug-ins
attribute | role |
---|---|
code | Plug-ins are executed first in a smaller order |
role | The role did not discover the actual reference address |
name | The plug-in name |
In fact, it is not difficult to find that the plugins of DefaultSoulPluginChain have a fixed execution order. Then, where is the execution order of this plug-in defined?
It can be traced back to the SoulConfiguration class
public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) {
/ / to omit
final List<SoulPlugin> soulPlugins = pluginList.stream()
.sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList());
return new SoulWebHandler(soulPlugins);
}
Copy the code
After sorting out the entire PluginEnum class references, the following table shows the sequential relationship between plug-ins
level | role |
---|---|
The first level | Only the GlobalPlugin global plug-in is available |
Grades two through eight | Think of it as a pre-processing plug-in before a request is initiated |
Grades nine through eleven | Can be understood as different call handling for the caller-specific approach |
Grade twelve | Only MonitorPlugin monitors plug-ins |
Grade 13 | Is a response-related plug-in for each caller to return result processing |
In this review we’ve seen the general flow of soul processing requests
- 1. The GloBalPlugin plugin performs global initialization
- 2. Some plug-ins process requests based on rules such as authentication, traffic limiting, and fusing
- 3. Select an appropriate call method to assemble parameters and initiate the call.
- 4. Monitor
- 5. Process the result of the call
Request process combing
The following demo code screenshots from the soul – examples of HTTP demo, call interface address to http://127.0.0.1:9195/http/test/findByUserId? userId=10
Check the excute method in DefaultSoulPluginChain to see what classes an HTTP request call passed through.
public Mono<Void> execute(final ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < plugins.size()) {
SoulPlugin plugin = plugins.get(this.index++);
Boolean skip = plugin.skip(exchange);
if (skip) {
System.out.println("Skipped plug-in is"+plugin.getClass().getName().replace("org.dromara.soul.plugin.".""));
return this.execute(exchange);
}
System.out.println("Unskipped plug-ins are"+plugin.getClass().getName().replace("org.dromara.soul.plugin.".""));
return plugin.execute(exchange, this);
}
return Mono.empty();
});
}
Copy the code
The final output of the unskipped plug-in is as follows:
Did not skip the plugin for global. GlobalPlugin did not skip the plug-in to sign SignPlugin did not skip the plugin for the waf. WafPlugin did not skip the plugin for ratelimiter. RateLimiterPlugin Did not skip the plugin for hystrix. HystrixPlugin did not skip the plugin for resilience4j. Resilience4JPlugin did not skip the plugin to divide DividePlugin Did not skip the plugin for httpclient. WebClientPlugin did not skip the plugin for alibaba. Dubbo. Param. BodyParamPlugin did not skip the plugin for the monitor. The MonitorPlugin Did not skip the plugin for httpclient. Response. WebClientResponsePlugin
. Here is a little confused, why the alibaba dubbo. Param. BodyParamPlugin plug-ins can be executed, ignored, late tracking.
We found that the general flow of the plug-in for a gateway call to an HTTP request was consistent with what we expected. At present, we only choose the key points, namely GlobalPlugin, DividePlugin, WebClientPlugin, WebClientResponsePlugin.
Issue a Debug call to trace the action of the four plug-ins in turn.
The GlobalPlugin SoulContext object encapsulates the plug-in
The Excute method for the GlobalPlugin plugin is shown below
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
final ServerHttpRequest request = exchange.getRequest();
final HttpHeaders headers = request.getHeaders();
final String upgrade = headers.getFirst("Upgrade");
SoulContext soulContext;
if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) {
soulContext = builder.build(exchange);
} else {
final MultiValueMap<String, String> queryParams = request.getQueryParams();
soulContext = transformMap(queryParams);
}
exchange.getAttributes().put(Constants.CONTEXT, soulContext);
return chain.execute(exchange);
}
Copy the code
As you can see, the main purpose of GlobalPlugin’s Excute method is to encapsulate a SoulContext object and place it in Exchange. (Exchange objects are shared objects along the chain of plug-ins, and one plug-in executes and passes them to the next. I understand it as a ThreadLocal object.
So what are the properties in the SoulContext object?
attribute | meaning |
---|---|
module | Each RPCType has a different value that refers to the gateway’s pre-address when the HTTP call is made |
method | Cut method name (when RpcType is HTTP) |
rpcType | RPC call types include Http, Dubbo, and SOFA |
httpMethod | Currently, only GET and POST are supported |
sign | The specific functions of authentication attributes are not known, but may be related to SignPlugin |
timestamp | The time stamp |
appKey | The specific functions of authentication attributes are not known, but may be related to SignPlugin |
path | Path refers to the full path of the call to the Soul gateway (when RpcType is HTTP) |
contextPath | Same value as module (when RPCType is HTTP) |
realUrl | Same value as method (when RpcType is HTTP) |
dubboParams | Dubbo parameters? |
startDateTime | Start time suspect is associated with monitoring plug-in and statistics indicator module |
After the GlobalPlugin plugin is executed, the final wrapped SoulContext object is shown below.
The parameters of other RPCType SoulContext encapsulation can view DefaultSoulContextBuilder build method of tracking, because this title this paper traces the HTTP calls, so here not discuss excess.
DividePlugin A routing plugin
After executing the GlobalPlugin plugin, a SoulContext object is finally encapsulated and placed in ServerWebExchange for use by the downstream call chain.
Now let’s see what role the DividePlugin plays in the chain call process.
AbstractSoulPlugin
Tracing the source code shows that the DividePlugin extends from the AbstractSoulPlugin class, which implements the SoulPlugin interface.
So what kind of extensions does AbstractSoulPlugin make? Let’s comb through the methods of this class.
The method name | role |
---|---|
excute | Implemented in the SoulPlugin interface and in AbstractSoulPluginThe role of template methods |
doexcute | Abstract methodsSubclasses implement it |
matchSelector | Match selector |
filterSelector | Filter selector |
matchRule | Match rule |
filterRule | Filtering rules |
handleSelectorIsNull | Handles the case where the selector is empty |
handleRuleIsNull | Handle rule null cases |
selectorLog | Selector log print |
ruleLog | Rule Log Printing |
Take a look at the Excute method in action
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
String pluginName = named();
// Get the corresponding plug-in
final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
// Check whether the plug-in is enabled
if(pluginData ! =null && pluginData.getEnabled()) {
// Get all the selectors under the plug-in
final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
if (CollectionUtils.isEmpty(selectors)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
// Match selector
final SelectorData selectorData = matchSelector(exchange, selectors);
if (Objects.isNull(selectorData)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
// Prints the selector log
selectorLog(selectorData, pluginName);
final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
if (CollectionUtils.isEmpty(rules)) {
return handleRuleIsNull(pluginName, exchange, chain);
}
RuleData rule;
if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
rule = rules.get(rules.size() - 1);
} else {
// Match rules
rule = matchRule(exchange, rules);
}
if (Objects.isNull(rule)) {
returnhandleRu! [](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f523f655f0014d288b7a4502cc6a08d1~tplv-k3u1fbpfcp-watermark.image)leIsNull(pl uginName, exchange, chain);
}
// Prints rule logs
ruleLog(rule, pluginName);
// Implement the subclass implementation
return doExecute(exchange, chain, selectorData, rule);
}
return chain.execute(exchange);
}
Copy the code
The final flow chart is as follows:
Ps: No specific method-level processing is detailed in the above flowchart.
But there are a few points that need to be highlighted:
- 1. Plug-in data, selector data, and rule data are all obtained from BaseDataCache, which is the class that is ultimately affected during data synchronization.
- In the global proxy mode, only one selector rule (which refers to all interfaces of the proxy) will be registered, so the corresponding processing here is rule-.size ()-1.
- 3. The actual processing of selector and rule selection is much more complicated. Considering that it is to introduce the general logic of a request process, it will not be elaborated here. The corresponding page is as follows:
AbstractSoulPlugin exeute method AbstractSoulPlugin exeute method AbstractSoulPlugin exeute method AbstractSoulPlugin exexcute method
Let’s take a look at what DividePlugin’s Doexcute method does.
DividePlugin
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assertsoulContext ! =null;
// Get rule processing data
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
// Gets the address of the injection under this selector
final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
if (CollectionUtils.isEmpty(upstreamList)) {
log.error("divide upstream configuration error: {}", rule.toString());
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
// Select an ADDRESS based on the load balancing policy corresponding to the rule
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
if (Objects.isNull(divideUpstream)) {
log.error("divide has no upstream");
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// set the http url
String domain = buildDomain(divideUpstream);
// Assemble the real call address
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
// Set the timeout period and retry times
exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
return chain.execute(exchange);
}
Copy the code
The general logic after combing through the above code is as follows:
- 1. Obtain the registration address of the selector
- 2. Obtain the load balancing policy based on the Handle field of the rule, select the actual call address (LoadBalanceUtils), retry times, and timeout period.
- 3. Send the actual call address, timeout period, and retry times to ServerWebExchange for use by the downstream call chain.
The debug demo:Ps: Where do we not see the parameters in the theme logic above? So where is this parameter encapsulated? The answer inBuildRealURL methodFrom theexchangeRetrieved from the context.
WebClientPlugin Http request invocation plug-in
Now let’s see how does Soul initiate a request call
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assertsoulContext ! =null;
// Get the real address
String urlPath = exchange.getAttribute(Constants.HTTP_URL);
if (StringUtils.isEmpty(urlPath)) {
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// Get the timeout period
long timeout = (long) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_TIME_OUT)).orElse(3000L);
// Get the number of retries
int retryTimes = (int) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_RETRY)).orElse(0);
log.info("The request urlPath is {}, retryTimes is {}", urlPath, retryTimes);
HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
return handleRequestBody(requestBodySpec, exchange, timeout, retryTimes, chain);
}
Copy the code
There are three main things you do in webClient’s Excute method
- 1. Pull out the properties put into Exchange from Divide plug-in, the real address of the call, timeout, number of retries.
- 2. Encapsulates a RequestBodySpec object (not aware of this reactive programming thing)
- 3. A handleRequestBody method is called
See the handleRequestBody method first
private Mono<Void> handleRequestBody(final WebClient.RequestBodySpec requestBodySpec,
final ServerWebExchange exchange,
final long timeout,
final int retryTimes,
final SoulPluginChain chain) {
return requestBodySpec.headers(httpHeaders -> {
httpHeaders.addAll(exchange.getRequest().getHeaders());
httpHeaders.remove(HttpHeaders.HOST);
})
.contentType(buildMediaType(exchange))
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
.exchange()
// Failed to print logs
.doOnError(e -> log.error(e.getMessage()))
// Set the timeout period
.timeout(Duration.ofMillis(timeout))
// Set the actual request retry
.retryWhen(Retry.onlyIf(x -> x.exception() instanceof ConnectTimeoutException)
.retryMax(retryTimes)
.backoff(Backoff.exponential(Duration.ofMillis(200), Duration.ofSeconds(20), 2.true)))
// Process the request after it ends
.flatMap(e -> doNext(e, exchange, chain));
}
Copy the code
In this method, it can be roughly interpreted as
- The request header in Exchange is placed in the request header for this call
- Set the contentType
- Setting timeout
- Setting failure response
- Set the retry scenario and retry times
- Processing of final results.
You also need to look at the doNext method in the process
The general logic is to determine whether the request was successful and put the result of the request into Exchange for the downstream plug-in to process.
private Mono<Void> doNext(final ClientResponse res, final ServerWebExchange exchange, final SoulPluginChain chain) {
if (res.statusCode().is2xxSuccessful()) {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
} else {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.ERROR.getName());
}
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_ATTR, res);
return chain.execute(exchange);
}
Copy the code
Ps: The fact that we don’t understand responsive programming doesn’t stop us from reading code.
WebClientResponsePlugin Http result handling plug-in
The excute method of this implementation has no core logic except to determine the request status code and return it to the front end in different data formats based on the status code.
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
return chain.execute(exchange).then(Mono.defer(() -> {
ServerHttpResponse response = exchange.getResponse();
ClientResponse clientResponse = exchange.getAttribute(Constants.CLIENT_RESPONSE_ATTR);
if (Objects.isNull(clientResponse)
|| response.getStatusCode() == HttpStatus.BAD_GATEWAY
|| response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) {
Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_TIMEOUT.getCode(), SoulResultEnum.SERVICE_TIMEOUT.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
response.setStatusCode(clientResponse.statusCode());
response.getCookies().putAll(clientResponse.cookies());
response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
return response.writeWith(clientResponse.body(BodyExtractors.toDataBuffers()));
}));
}
Copy the code
conclusion
At this point, the process of invoking an Http request based on the Soul Gateway is basically over.
Sort out the HTTP request invocation process
- The Global plug-in encapsulates the SoulContext object
- The front plug-in handles fuses and current limiting authentication.
- Divide plug-in selects the actual address for the call, number of retries, and timeout.
- The WebClient plug-in makes the actual Http call
- The WebClientResponse plug-in processes the result and returns to the foreground.
Based on the general flow of Http calls, we can roughly guess the process based on other RPC calls, which is to replace the plug-in that initiates the request and the plug-in that returns the result processing.
In the above article we also mentioned the selection of routing rules LoadBalanceUtils, selector and rule processing MatchStrategy.
After that, a new chapter will be opened to reveal the mystery of RPC generalization call, routing, selector and rule matching step by step.