takeaway:

Requirements change is the only constant in a programmer’s life. This article will introduce the SPI mechanism in JDK/Spring/Dubbo to help us write a code framework that is extensible and easy to maintain.

Yang Liang senior Java Development engineer of NetEase Cloud Business

A,What is SPI?

Service Provider Interface (SPI) is an API designed to be implemented or extended by a third party. It can be used to enable, extend, and even replace components in the framework. The purpose of SPI is to enable developers to use new plug-ins or modules to enhance the framework without modifying the original code base. As we often use JDBC, in the Java core class library, there is no provision for developers to use what type of database, developers can choose different database types according to their own needs, can be MySQL, Oracle.

Therefore, the Java core class library only provides the database-driven interface java.sql.Driver, different database service providers can implement this interface, and developers only need to configure the corresponding database-driven implementation class, JDBC framework can load third-party services to achieve the client access to different types of database functions.

SPI can be found in many mainstream development frameworks. In addition to the SPI mechanism provided by JDK, there are also Spring, Spring Cloud Alibaba Dubbo, etc. Next, I will introduce how to use them and their implementation principles.

Second, the JDK SPI

(1) Cases

  • Defining interface specifications
package com.demo.jdkspi.api; public interface SayHelloService { String sayHello(String name); }Copy the code
  • Define the interface implementation class
public class SayHelloImpl implements SayHelloService { public String sayHello(String name) { return "Hello "+name+", welcome to NetEase Cloud business!" ; }}Copy the code
  • The configuration file

    Add plain text files in the resources directory meta-inf/services/com. The demo. Jdkspi. API. SayHelloService, content is as follows:

com.demo.jdkspi.impl.SayHelloServiceImpl
Copy the code

  • Writing test classes

    The client introduces the dependency and loads the interface using the ServiceLoader:

public static void main(String[] args) { // 1. Sayhelloservice.class to create a ServiceLoader instance, ServiceLoader<SayHelloService> loader = Serviceloader.load (sayHelloService.class); ForEach (SayHelloService ->{SayHelloService ->{SayHelloService ->{ System.out.println(sayHelloService.sayHello("Jack")); }); }Copy the code

The running results are as follows:

(2) JDK SPI principle analysis

The Java Java Development Kit (JDK) SPI mechanism is implemented mainly through the ServiceLoader. It is important to note that the implementation class loading mechanism is lazy. The ServiceLoader does not load the interface implementation, but loads it during traversal.

Process for creating a ServiceLoader instance:

Main Process Description

  1. Get the ClassLoader for the thread context: Because the ServiceLoader is under rt.jar and the interface implementation class is under classpath, breaking the parental delegation model, you need to get the AppClassLoader from the thread context to load the target interface and its implementation class.
  2. Clearing the providers cache: Clears the history load cache.
  3. Create a LazyIterator that will be used later when iterating through all implementation classes.

Loading target service process:

Main Process Description

  1. SayHelloService loads the configuration of all target interfaces under the ClassPath (as determined by the AppClassLoader mentioned earlier) before the iterator starts traversing.
  2. Interface implementation classes are instantiated primarily by first creating a Class object through class.forname and then creating the instance through reflection.
  3. After the implementation class is instantiated, the ServiceLoader caches the instance identified by the fully qualified name of the implementation class.

(3) SUMMARY of JDK SPI

Advantages:

  • Decoupling: THE JDK SPI decouples the logic of third-party service module load control from the caller’s business code.
  • Lazy loading: Third-party service modules are not loaded when the ServiceLoader instance is created, but are loaded during traversal.

disadvantages

  • All interface implementation classes can only be obtained through traversal, not implemented on demand loading.
  • If the interface implementation class relies on other extension implementations, the JDK SPI does not implement dependency injection.

Three, Spring SPI

The Spring Boot Starter is a collection of dependencies that allows you to get a one-stop shop for Spring and related technologies with a simple configuration. The implementation of Spring Boot Starter is also inseparable from the SPI idea. Let’s experience its charm by implementing a simple Starter component.

(1) Case of Spring Boot Starter

  • Write SayHello Service implementation class and Spring configuration class

    Create a separate project greeter-spring-boot-starter and write the SayHelloService implementation class and spring configuration class

public class Greeter implements SayHelloService, InitializingBean {public String sayHello(String name) {return "Hello "+name+" ; } public void afterPropertiesSet() throws Exception {system.out.println (" NetEase cloud service load complete, welcome to use! ); }}Copy the code
@Configurationpublic class TestAutoConfiguration { @Bean public SayHelloService sayHelloService(){ return new Greeter();  }}Copy the code
  • The configuration file

    Create a spring.factories file in the resources/ meta-INF directory.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.demo.springspi.TestAutoConfiguration
Copy the code
  • Introduction of depend on

    Reference the Greeter-spring-boot-starter dependency in a client project

<dependency> <groupId>com.spi.demo</groupId> <artifactId>greeter-spring-boot-starter</artifactId> < version > 1.0.0 - the SNAPSHOT < / version > < / dependency >Copy the code
  • Results show

    When the client Spring project starts, you can clearly see that the Greeter we wrote will be loaded by the Spring IoC container.

(2) Spring Boot Starter principle analysis

There is also a ServiceLoader class in Spring SPI, SpringFactoriesLoader. When the Spring container is started, The SpringFactoriesLoader goes to “meta-INF/Spring. factories” to get the configuration classes and encapsulates them into BeanDefinition. The Spring IoC container can then manage these beans as follows:

Main process description:

  1. The SpringFactoriesLoader loads the configuration class information when the SpringApplication instance is built, The SpringFactoriesLoader reads the configuration information under “meta-INF/Spring. factories” and caches it.
  2. AutoConfigurationImportSelector is introduced in @ EnableAutoConfiguration AutoConfigurationImportSelector core function is: Access to “org. Springframework. Boot. Autoconfigure. EnableAutoConfiguration” configuration of the class list, It will filter through (e.g. exclude in @enableAutoConfiguration) to get the final list of configuration classes that need to be loaded.
  3. ConfigurationClassPostProcessor will eventually need to load the configuration of the class list and loads it to BeanDefinition, follow-up at the time of analytical BeanClass, Class.forname is also called to get the Class object of the configuration Class. The Spring Bean loading process is not covered in this article.

(3) Summary of Spring SPI

  1. The problem that the JDK SPI does not implement dependency injection is solved by handing over the third-party service implementation classes to the Spring container.
  2. With Spring Boot conditional assembly, it is possible to load third-party services on demand under certain conditions, rather than loading all extension point implementations.

Four, Dubbo SPI

The SPI mechanism is also used in Dubbo, where Dubbo loads all components through the SPI mechanism, but instead of using Java’s native SPI mechanism, Dubbo has enhanced it. In the Dubbo source code, it is common to see the following code: named extension point, active extension point, and adaptive extension point:

ExtensionLoader.getExtensionLoader(XXX.class).getExtension(name); ExtensionLoader.getExtensionLoader(XXX.class).getActivateExtension(); ExtensionLoader.getExtensionLoader(XXX.class).getAdaptiveExtension(url,key);Copy the code

The Dubbo SPI logic is encapsulated in ExtensionLoader, through which we can load the specified implementation class. The Dubbo SPI extension has two rules:

  1. You need to create any directory structure in the resources directory: meta-INF /dubbo, meta-INF /dubbo/internal, and meta-INF /services. In the corresponding directories, create files named after the full path of the interface.
  2. The file contents are data in the form of Key, which is a string, and Value, which is an implementation of the corresponding extension point.

(1) Specify the name extension point

case

  • Declare an extension point interface

    In a project that relies on the Dubbo framework, create an extension point interface and an implementation that uses the @spi annotation as follows:

@SPIpublic interface SayHelloService { String sayHello(String name); }Copy the code
public class SayHelloServiceImpl implements SayHelloService { @Override public String sayHello(String name) { return "Hello "+name+", welcome to NetEase Cloud business!" ; }}Copy the code
  • The configuration file

    Add plain text files in the resources directory meta-inf/dubbo/com. The spi. API. Dubbo. SayHelloService, content is as follows:

neteaseSayHelloService=com.spi.impl.dubbo.SayHelloServiceImpl
Copy the code

  • Writing test classes
public static void main(String[] args) { ExtensionLoader<SayHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(SayHelloService.class); SayHelloService sayHelloService = extensionLoader.getExtension("neteaseSayHelloService"); System.out.println(sayHelloService.sayHello("Jack")); }Copy the code

(2) Activate extension points

Sometimes an extension point may have multiple implementations, and we want to retrieve some of the implementation classes to implement complex functions. Dubbo defines the @activate annotation for the implementation class, indicating that the extension point is an active extension point. Dubbo Filter is our usual activation extension point.

case

In the service provider side to achieve two functions, one is to print the call log when the service is called, the second is to check the system status, if the system is not ready, then directly return an error.

  • Define a filter for printing logs
/** * group = {Constants.PROVIDER} indicates that the service PROVIDER is in effect * ORDER indicates the execution order, */ @activate (group = {constants.provider}, order = Integer.MIN_VALUE)public class LogFilter implements Filter { @Override public Result invoke(Invoker<? > Invoker, Invocation) throws RpcException {system.out.println (" Print call log "); return invoker.invoke(invocation); }}Copy the code
  • Define the filter for the system status check​​​​​​​
@Activate(group = {Constants.PROVIDER},order = 0)public class SystemStatusCheckFilter implements Filter { @Override public Result invoke(Invoker<? Throws RpcException {// The system status is not ready if(! SysEnable ()) {throw new RuntimeException(" System not ready, please try again later "); } system.out.println (" system.out.println "); Result result = invoker.invoke(invocation); return result; }}Copy the code
  • The configuration file

    In the resources directory to add a plain text file meta-inf/dubbo/com. Alibaba. Dubbo. RPC. The Filter, the content is as follows:

logFilter=com.springboot.dubbo.springbootdubbosampleprovider.filter.LogFiltersystemStatusCheckFilter=com.springboot.dubb o.springbootdubbosampleprovider.filter.SystemStatusCheckFilterCopy the code
  • perform

    On the service provider side, the target method is executed before the two filters we defined are executed, as shown in the following figure:

(3) Adaptive extension points

Adaptive extension points are the ability to dynamically match an extension class based on context. Sometimes extensions do not want to be loaded at framework startup, but rather want to be loaded based on runtime parameters when extension methods are called.

case

  • Define the adaptive extension point interface
@spi ("default")public interface SimpleAdaptiveExt {/** * serviceKey * Use the default extension point if none is found. */ @Adaptive("serviceKey") void sayHello(URL url, String name); }Copy the code
  • Define extension point implementation classes
public class DefaultExtImp implements SimpleAdaptiveExt {    @Override    public void sayHello(URL url, String name) {        System.out.println("Hello " + name);    }}
Copy the code
public class OtherExtImp implements SimpleAdaptiveExt {    @Override    public void sayHello(URL url, String name) {        System.out.println("Hi " + name);    }}
Copy the code
  • The configuration file

    Add plain text files in the resources directory meta-inf/dubbo/com. The spi. Impl. Dubbo. The adaptive. SimpleAdaptiveExt, content is as follows:

default=com.spi.impl.dubbo.adaptive.DefaultExtImpother=com.spi.impl.dubbo.adaptive.OtherExtImp
Copy the code
  • Writing test classes
public static void main(String[] args) { SimpleAdaptiveExt simpleExt = ExtensionLoader.getExtensionLoader(SimpleAdaptiveExt.class).getAdaptiveExtension(); Map<String, String> map = new HashMap<String, String>(); URL URL = new URL(" HTTP ", "127.0.0.1", 1010, "path", map); SayHello (url, "Jack"); simpleExt. SayHello (url, "Jack"); url = url.addParameter("serviceKey", "other"); SayHello (url, "Tom"); // serviceKey=other. }Copy the code

(4) Principle analysis of Dubbo extension point

Get the ExtensionLoader instance

ExtensionLoader. The main () method returns a ExtensionLoader getExtensionLoader instance, main logic is as follows:

  1. Get the corresponding instance of the extension class from cache “EXTENSION_LOADERS”;
  2. If the cache misses, a new instance is created and stored in EXTENSION_LOADERS;
  3. In the ExtensionLoader constructor, an ExtensionFactory is initialized;

Gets the extension point method getExtension

  1. Get the extension class from cache cachedClasses, or load it from meta-INF /dubbo/internal/, meta-INF /dubbo/, meta-INF /services/.
  2. Once the extension class is obtained, check whether the cache EXTENSION_INSTANCES has an implementation of that extension class, and if not, instantiate it into the cache by reflection.
  3. Implement dependency injection, which Dubbo will inject into the current instance if the current instance relies on other extended implementations.
  4. The extended class instance is wrapped through the Wrapper decorator.

Of the above steps, the first is the key to loading the extended classes, while the third and fourth steps are concrete implementations of Dubbo IoC and AOP. Dependency injection is implemented by calling injectExtension and only setter-style injection is supported.

Get the adaptive extension point method getAdaptiveExtension

  1. Call getAdaptiveExtensionClass method for adaptive extended Class object.
  2. Instantiation via reflection. Call the injectExtension method to inject a dependency into the extended class instance.

While the above three processes are similar to ordinary extension point acquisition methods, when working with Class objects, Dubbo dynamically generates a dynamic proxy Class that ADAPTS to the extension point, and then compiles the source using JavAssist (the default) to get the proxy Class instance. The source code for the dynamically generated adaptive extension class is as follows (take SimpleAdaptiveExt in the above code as an example) :

package com.spi.impl.dubbo.adaptive; import org.apache.dubbo.common.extension.ExtensionLoader; public class SimpleAdaptiveExt$Adaptive implements com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt { public void sayHello(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("serviceKey", "default"); if(extName == null) throw new IllegalStateException("Failed to get extension (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt) name from url (" + url.toString() + ") use keys([serviceKey])"); com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt extension = (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt)ExtensionLoader.getExtensionLoader(com.spi.impl.dubbo.adaptive.SimpleAdap tiveExt.class).getExtension(extName); extension.sayHello(arg0, arg1); }}Copy the code

As you can see from the code above, in the SayHello method, we get the value of serviceKey in the URL, and use the extension point implementation if there is one, otherwise use the default extension point implementation.

(v) Summary of Dubbo SPI

Dubbo’s extension point loading is enhanced from the JDK SPI extension point discovery mechanism and improves on the following issues in JDK SPI:

  1. Whereas JDK SPI instantiates all implementations of extension points at once, Dubbo can use adaptive extension points that are instantiated at the time of extension method calls.
  2. Support for IoC has been added so that one extension point can inject other extension points via setters.
  3. AOP support has been added to augment existing extension class instances based on Wrapper classes.

5. Prospect of customization technology combined with SPI in multi-tenant system

Dynamic personalized configuration and customization technology in multi-tenant system can meet the personalized requirements of different tenants, but a large number of customization tasks may make the system very complicated.

For the convenience of management and maintenance of different tenants personalized configuration, combined with the SPI can use different extension implementation to enable or extension framework components in thoughts, we can design a tenant personalization management platform, the platform can manage each tenant customized configuration, developers will be individual differences of different tenants abstract as individual customization points, The customized management platform can collect and manage these customized points of information, and the business system can obtain the customized configuration of tenants from the customized platform and load the corresponding extended implementation, so as to meet the personalized needs of different tenants. The overall structure is as follows:

The tenant customization management platform has the following functions and features:

  1. Abstract customization Points: Developers abstract tenant characteristics into different customization point interfaces, with different extended implementations for tenants with different characteristics.
  2. Customization point discovery: The customization point and implementation information of each service must be reported to the customization management platform.
  3. Customized tenant customization: Operators can configure different customization points based on the characteristics of tenants.
  4. Dynamic loading: when a tenant accesses a specific service of the business system, the business system can obtain the configuration information of the corresponding tenant from the management platform, and can assemble one or more customization points through the responsibility chain/decorator pattern.
  5. Tenant isolation: After customized configurations are configured for tenants, the customized management platform can store configuration information in tenant dimensions to isolate customized content of different tenants.
  6. Custom reuse: Reuse configurations for common tenant characteristics or use default configurations for those tenants that are not configured.

Tenant personalization management platform tenants can be personalized features, in the form of metadata management, follow-up as long as the new tenants can through the existing demand for personalized custom point of metadata, then only need to modify the configuration way to meet new demand, even can’t satisfy the, only need to add or point to implement custom interface and report to the custom management platform, This makes the system easier to maintain and the code more reusable.

The resources

Dubbo 2.7 Development Guide

Spring Cloud Alibaba Micro Service Principle and Practice

The authors introduce

Yang Liang, senior Java development engineer of NetEase Cloud Business, is responsible for the design and development of common business modules and internal middleware of cloud business platform.