Soul Gateway learns plug-in chain implementation

Author: Shen Xiangjun

One, the introduction

Plug-ins are the Soul of Soul.

Soul uses a plug-in design concept that is hot pluggable and easily extensible. Built-in rich plug-in support, authentication, limiting, fuses, firewalls and more.

How does Soul implement plug-in design?

Before we dive into plug-in design, we need to take a look at microkernel architecture (also known as plug-in architecture).

2. Microkernel architecture

1. Structure interpretation

Microkernel architecture, also known as plug-in architecture, is a feature-oriented extensible architecture that is often used to implement product-based applications.

Application logic is split into separate plug-in modules and core systems, providing extensibility, flexibility, functional isolation, and custom processing logic features.

The essence of microkernel architecture is to encapsulate changes in plug-ins so as to achieve rapid and flexible expansion without affecting the overall system stability.

2. Key points of design

Key technologies of core system design:

  • ** Plug-in management: ** What plug-ins are currently available? How do I load these plug-ins? When do plug-ins load?

    A common implementation is the plug-in registry mechanism.

  • Plugins connect: How do plugins connect to the core system?

    The core system usually makes the connection specification, then the plug-in is implemented according to the specification, and the core system is loaded according to the specification.

    Common connection mechanisms include OSGi (Eclipse), message pattern, and dependency injection (Spring).

  • Plug-in communication: How do plug-ins communicate with plug-ins and with core systems?

    The communication must go through the core system, so it is usually the core system that provides the plug-in communication mechanism.

Third, plug-in design of Soul

In terms of the microkernel architecture, the soul-Web module of Soul is the core system, and the submodules of soul-plugin are the plug-in modules.

Plug-in management:

The SOUl-Bootstrap module’s POM file acts as a list of plug-ins that are hardcoded into each plug-in.

During the container startup phase, plug-in beans are automatically scanned and registered into the Spring container with springBoot’s starter mechanism.

Plug-in connection:

With springBoot’s multi-instance automatic injection capability (ObjectProvider Plugins), the plug-in Bean list is injected into the plug-in chain of the gateway, and the connection between the plug-in and the gateway is realized.

Plug-in communication:

The plugins are sorted in the initial stage of the plug-in chain, and then in the process of the plug-in, the ServerWebExchange that runs through the whole plug-in chain completes the directional parameter transmission to the downstream plug-in, which is the plug-in communication mechanism in a sense.

Fourth, the plug-in realization of Soul

The Soul Gateway defines a chain of plug-ins on which all plug-ins are processed in turn.

Before exploring the plug-in chain, let’s look at the plug-in implementation.

1. Plug-in implementation

All plug-ins in Soul eventually inherit from SoulPlugin, and the complete inheritance relationship is as follows:

As you can see, the Soul plugin ecosystem is extremely rich, and it is this rich plugin ecosystem that underlies the Soul Gateway’s powerful scalability capabilities.

Let’s take the commonly used DividePlugin as an example to analyze what’s going on inside the plug-in.

DividePlugin inheritance structure:

DividePlugin is derived from AbstractSoulPlugin and finally implements the SoulPlugin interface.

1) Focus on SoulPlugin, which has the following interface structure:

  • Execute method: Processing method that requires passing in the Exchange exchange and the SoulPluginChain plug-in chain
  • GetOrder method: Gets an ordinal number for plug-in sorting
  • Named method: Get the plug-in name
  • Skip method: Determine whether to skip the processing

During each processing, skip judgment is performed first, and if not, excute processing is performed.

AbstractSoulPlugin: AbstractSoulPlugin: AbstractSoulPlugin: AbstractSoulPlugin

Focus on the execute method, whose core code is as follows:

if (pluginData.getEnable()){
	// Get plug-in data
	final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
	// Get selector data
	final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
	final SelectorData selectorData = matchSelector(exchange, selectors);
	// Get the rule
	final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
	RuleData rule;
  if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
  	//get last
    rule = rules.get(rules.size() - 1);
  } else {
    rule = matchRule(exchange, rules);
  }
  // Perform specific processing
  return doExecute(exchange, chain, selectorData, rule);
}
// Continue with subsequent plug-in processing
return chain.execute(exchange);
Copy the code

Get the selector data and rules, and then pass in the doExecute method for specific processing, doExecute method is an abstract method, by subclass concrete implementation.

3) Check the subclass DividePlugin, its structure is as follows:

Focusing on the doExecute method, here’s the core code:

// Get the gateway context and rules handler
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
// Get the upstream list
final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
// Select the target upstream to be distributed
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
// Set the HTTP URL
String domain = buildDomain(divideUpstream);
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
// Set HTTP timeout
exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
return chain.execute(exchange);
Copy the code

It is clear that The Divide plug-in simply completes the distribution of the target upstream service by finding the corresponding service based on the selector and rules, and then allocating the upstream service instance through the load balancing policy.

The job of calling the upstream service is done by other corresponding Client-class plug-ins.

2. Plug-in chain implementation

Through plug-in chains, Soul brings together many plug-ins for unified scheduling.

Plug-in chain inheritance structure:

As you can see, the plug-in chain SoulPluginChain in Soul has only one default implementation class, DefaultSoulPluginChain.

1) DefaultSoulPluginChain class structure is as follows:

It holds a chain of plugins passed in through the constructor. Look at the execute method:

public Mono<Void> execute(final ServerWebExchange exchange) {
    // Reactive programming syntax: mono.defer
  	return Mono.defer(() -> {
        if (this.index < plugins.size()) {
            SoulPlugin plugin = plugins.get(this.index++);
            // Check whether it needs to be adjusted
          	Boolean skip = plugin.skip(exchange);
            if (skip) {
                return this.execute(exchange);
            }
          	// Execute the plug-in processing logic in turn
            return plugin.execute(exchange, this);
        }
        return Mono.empty();
    });
}
Copy the code

Process the plug-ins on the plug-in chain in turn, performing the plug-in processing logic.

DefaultSoulPluginChain is an inner class of SoulWebHandler. Look at the implementation of SoulWebHandler.

2) The SoulWebHandler structure is as follows:

SoulWebHandler is the starting point for Web request processing, where processing of the plug-in chain is created and started.

Like DefaultSoulPluginChain, SoulWebHandler holds a chain of plug-ins passed in through the constructor.

Look at the Handle method:

public Mono<Void> handle(@NonNull final ServerWebExchange exchange) {
    MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
    Optional<HistogramMetricsTrackerDelegate> startTimer = MetricsTrackerFacade.getInstance().histogramStartTimer(MetricsLabelEnum.REQUEST_LATENCY.getName());
    return new DefaultSoulPluginChain(plugins).execute(exchange).subscribeOn(scheduler)
            .doOnSuccess(t -> startTimer.ifPresent(time -> MetricsTrackerFacade.getInstance().histogramObserveDuration(time)));
}
Copy the code

The Handle method is responsible for the collection of plug-in chain execution metrics by adding a subscription to DefaultSoulPluginChain execution, where DefaultSoulPluginChain is initialized.

Find the SoulWebHandler constructor globally and navigate to the SoulWebHandler method of SoulConfiguration.

3) The SoulConfiguration structure is as follows:

SoulConfiguration is the core configuration class for Soul, which is responsible for the core bean objects needed to auto-assemble the gateway.

For example, with SoulWebHandler:

@Bean("webHandler")
public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) {
    // Get the available plug-ins
  	List<SoulPlugin> pluginList = plugins.getIfAvailable(Collections::emptyList);
    // Plugins are rearranged
  	final List<SoulPlugin> soulPlugins = pluginList.stream()
            .sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList());
    soulPlugins.forEach(soulPlugin -> log.info("load plugin:[{}] [{}]", soulPlugin.named(), soulPlugin.getClass().getName()));
    return new SoulWebHandler(soulPlugins);
}
Copy the code

Note that the list of plug-ins here has been rearranged, in the order shown in PluginEnum.

4) Initialize SoulWebHandler

How do all plug-ins form ObjectProvider<List> plugins and initialize SoulWebHandler during soul-bootstrap startup?

The SoulWebHandler configuration class tells Spring to scan the org.dromara.soul package by configuring @ComponentScan(“org.dromara.soul”).

The configuration classes specified in Spring. Factories are automatically loaded into the container using springboot’s starter mechanism.

Finally, with ObjectProvider, which spring4.3 has started to support, the integrated injection of plug-in beans within the container is implemented to form the plug-in chain we see.

conclusion

This article starts from the micro-kernel architecture, and analyzes the plug-in design of Soul as a framework, and then combines the source code implementation, basically clear the implementation of plug-in design in Soul.

Note:

1) SoulConfiguration automatically assembs The SoulWebHandler, which holds the list of plug-ins but does not initialize the plug-in chain.

2) When the handle method is called to process the request, the plug-in chain is initialized to enter the plug-in processing.