Guide :
Changes in requirements are the only constant thing in a programmer's life. This article will introduce the SPI mechanism in JDK/Spring/Dubbo to help us write a set of code frameworks that are highly scalable and easy to maintain.
Text|Yang Liang, Senior Java Development Engineer, NetEase Cloud Business
1. What is SPI?
SPI (Service Provider Interface) is an API designed to be implemented or extended by third parties. It can be used to enable, extend or even replace components in the framework. The purpose of the SPI is that developers can use new plugins or modules to enhance the framework functionality without modifying the original codebase. For example, JDBC, which we often use, does not specify what type of database developers need to use in the core class library of Java. Developers can choose different database types according to their own needs, which can be MySQL and Oracle.
Therefore , the core class library of Java 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 classes, and the JDBC framework can load the first The three-party service enables clients to access different types of databases.
In many mainstream development frameworks, we can see SPI. In addition to the SPI mechanism provided by JDK, there are also such as Spring, Spring cloud Alibaba Dubbo, etc. Next, the author will introduce how to use them and their implementation principles.
2. JDK SPI
(1) Case
- Define the interface specification
package com.demo.jdkspi.api;public interface SayHelloService { String sayHello(String name);}
- Define the interface implementation class
public class SayHelloImpl implements SayHelloService { public String sayHello(String name) { return "你好"+name+",欢迎关注网易云商!"; }}
configuration file
Add the plain text file META-INF/services/com.demo.jdkspi.api.SayHelloService to the resources directory, with the following contents:
com.demo.jdkspi.impl.SayHelloServiceImpl
write test class
The client introduces dependencies and uses ServiceLoader to load the interface:
public static void main(String[] args) { // 1. 根据SayHelloService.class创建ServiceLoader实例,此时SayHelloService实例并没有被创建(懒加载) ServiceLoader<SayHelloService> loader = ServiceLoader.load(SayHelloService.class); // 2. SayHelloService实例是在遍历的时候创建的 loader.forEach(sayHelloService ->{ System.out.println(sayHelloService.sayHello("Jack")); });}
The results are as follows:
(2) JDK SPI principle analysis
Through the case, we can know that the JDK SPI mechanism is mainly implemented through ServiceLoader. It should be noted that the loading of implementation classes is a lazy loading mechanism. Creating a ServiceLoader will not load the interface implementation, but will go to the traversal. load.
Create a ServiceLoader instance process:
Main process description
- Obtain the ClassLoader of the thread context: Since the ServiceLoader is under rt.jar, and the interface implementation class is under the classpath, which breaks the parent delegation model, it is necessary to obtain the AppClassLoader from the thread context to load the target interface and its implementation class.
- Clear providers cache: Clear the cache of historical loading.
- Create a LazyIterator , which will be used later when traversing all implementation classes.
Load the target service process:
Main process description
- Before the iterator starts to traverse , SayHelloService will load all the configuration information of the target interface under the ClassPath (determined by the AppClassLoader mentioned above).
- The instantiation of the interface implementation class is to first create a Class object through Class.forName, and then create an instance through reflection.
- After the implementation class is instantiated , the ServiceLoader caches the instance based on the fully qualified name of the implementation class.
(3) JDK SPI summary
advantage:
- Decoupling: JDK SPI separates the third-party service module loading control logic from the caller's business code, thereby achieving decoupling.
- Lazy loading: The third-party service module is not loaded when the ServiceLoader instance is created, but is loaded during traversal.
shortcoming
- All interface implementation classes can only be obtained by traversing, and on-demand loading is not implemented.
- If the interface implementation class depends on other extension implementations, the JDK SPI does not implement the function of dependency injection.
3. Spring SPI
Spring Boot Starter is a collection of dependencies, which allows us to obtain a one-stop service for Spring and related technologies with 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) Spring Boot Starter case
Write the implementation class of SayHello Service and the Spring configuration class
Create a separate project greeter-spring-boot-starter, and write SayHelloService implementation class and Spring configuration class
public class Greeter implements SayHelloService, InitializingBean { public String sayHello(String name) { return "你好"+name+",欢迎关注网易云商!"; } public void afterPropertiesSet() throws Exception { System.out.println("网易云商服务加载完毕,欢迎使用!"); }}
@Configurationpublic class TestAutoConfiguration { @Bean public SayHelloService sayHelloService(){ return new Greeter(); }}
configuration file
Create a spring.factories file in the resources/META-INF directory with the following contents:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.demo.springspi.TestAutoConfiguration
import dependencies
Reference greeter-spring-boot-starter dependency in client project
<dependency> <groupId>com.spi.demo</groupId> <artifactId>greeter-spring-boot-starter</artifactId> <version>1.0.0-SNAPSHOT</version></dependency>
Show results
When the client Spring project is started, it can be clearly seen that the Greeter we wrote will be loaded by the Spring IoC container.
(2) Analysis of the principle of Spring Boot Starter
In Spring SPI, there is also a class similar to ServiceLoader - SpringFactoriesLoader. When the Spring container starts, it will go to "META-INF/spring.factories" to obtain configuration class information through SpringFactoriesLoader, and then encapsulate these configuration class information into BeanDefinition , so that the Spring IoC container can manage these beans. The main process is as follows:
Main process description:
- SpringFactoriesLoader loading configuration class information occurs when building a SpringApplication instance, SpringFactoriesLoader will read the configuration information under "META-INF/spring.factories" and cache it.
- AutoConfigurationImportSelector is introduced in @EnableAutoConfiguration. The core function of AutoConfigurationImportSelector is: get the list of configuration classes of "org.springframework.boot.autoconfigure.EnableAutoConfiguration" , and filter it again (for example, if we configure the exclude property in @EnableAutoConfiguration), get The final list of configuration classes that need to be loaded.
- The ConfigurationClassPostProcessor will load the final configuration class list to be loaded as BeanDefinition. When parsing the BeanClass, it will also call Class.forName to obtain the Class object of the configuration class. The loading process of Spring Bean will not be repeated in this article.
(3) Summary of Spring SPI
- By handing the third-party service implementation class to the Spring container management, the problem that JDK SPI does not implement dependency injection is well solved.
- With Spring Boot conditional assembly, it is possible to load third-party services on demand under certain conditions, instead of loading all extension point implementations.
4. Dubbo SPI
The SPI mechanism is also used in Dubbo. Dubbo loads all components through the SPI mechanism , but Dubbo does not use Java's native SPI mechanism, but enhances it. In the Dubbo source code, you can often see the following codes, which are the specified name extension point, the activation extension point and the adaptive extension point:
ExtensionLoader.getExtensionLoader(XXX.class).getExtension(name);ExtensionLoader.getExtensionLoader(XXX.class).getActivateExtension();ExtensionLoader.getExtensionLoader(XXX.class).getAdaptiveExtension(url,key);
The relevant logic of Dubbo SPI is encapsulated in the ExtensionLoader class. Through the ExtensionLoader, we can load the specified implementation class. There are two rules for Dubbo's SPI extension:
- You need to create any directory structure in the resources directory: META-INF/dubbo, META-INF/dubbo/internal, META-INF/services Create a file named with the full path name of the interface in the corresponding directory.
- The content of the file is data in the form of Key and Value, where Key is a string, and Value is the implementation of a corresponding extension point.
(1) Specify the name extension point
case
declare extension point interface
In a project that relies on the Dubbo framework, create an extension point interface and an implementation. The extension point interface needs to be annotated with @SPI. The code is as follows:
@SPIpublic interface SayHelloService { String sayHello(String name);}
public class SayHelloServiceImpl implements SayHelloService { @Override public String sayHello(String name) { return "你好"+name+",欢迎关注网易云商!"; }}
configuration file
Add the plain text file META-INF/dubbo/com.spi.api.dubbo.SayHelloService in the resources directory, the content is as follows:
neteaseSayHelloService=com.spi.impl.dubbo.SayHelloServiceImpl
- write test class
public static void main(String[] args) { ExtensionLoader<SayHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(SayHelloService.class); SayHelloService sayHelloService = extensionLoader.getExtension("neteaseSayHelloService"); System.out.println(sayHelloService.sayHello("Jack"));}
(2) Activate the extension point
Sometimes an extension point may have multiple implementations, and we want to obtain some of the implementation classes to implement complex functions. Dubbo defines the @Activate annotation for us to mark the implementation classes, indicating that the extension point is an activation extension point. Among them, Dubbo Filter is our usual activation extension point.
case
Two functions are implemented on the service provider side. One is to print the call log when the service is called, and the second is to check the system status. If the system is not ready, an error will be returned directly.
- Define the filter that prints the log
/** * group = {Constants.PROVIDER}表示在服务提供者端生效 * order表示执行顺序,越小越先执行 */@Activate(group = {Constants.PROVIDER}, order = Integer.MIN_VALUE)public class LogFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { System.out.println("打印调用日志"); return invoker.invoke(invocation); }}
- Define a filter for system status checks
@Activate(group = {Constants.PROVIDER},order = 0)public class SystemStatusCheckFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 校验系统状态,如果系统未就绪则调用失败 if(!sysEnable()) { throw new RuntimeException("系统未就绪,请稍后再试"); } System.out.println("系统准备就绪,能正常使用"); Result result = invoker.invoke(invocation); return result; }}
configuration file
Add the plain text file META-INF/dubbo/com.alibaba.dubbo.rpc.Filter in the resources directory, the content is as follows:
logFilter=com.springboot.dubbo.springbootdubbosampleprovider.filter.LogFiltersystemStatusCheckFilter=com.springboot.dubbo.springbootdubbosampleprovider.filter.SystemStatusCheckFilter
Execution effect
On the service provider side, before executing the target method, the two filters we defined will be executed first. The effect is as shown in the figure:
(3) Adaptive extension point
The adaptive extension point is to dynamically match an extension class according to the context. Sometimes some extensions do not want to be loaded during the framework startup phase, but want to be loaded according to the runtime parameters when the extension method is called.
case
- Defining the Adaptive Extension Point Interface
@SPI("default")public interface SimpleAdaptiveExt { /** * serviceKey表示会根据URL参数中serviceKey的值来寻找对应的扩展点实现, * 如果没有找到就使用默认的扩展点。 */ @Adaptive("serviceKey") void sayHello(URL url, String name);}
- Define the extension point implementation class
public class DefaultExtImp implements SimpleAdaptiveExt { @Override public void sayHello(URL url, String name) { System.out.println("Hello " + name); }}
public class OtherExtImp implements SimpleAdaptiveExt { @Override public void sayHello(URL url, String name) { System.out.println("Hi " + name); }}
configuration file
Add the plain text file META-INF/dubbo/com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt to the resources directory, with the following contents:
default=com.spi.impl.dubbo.adaptive.DefaultExtImpother=com.spi.impl.dubbo.adaptive.OtherExtImp
- write test class
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); // 调用默认扩展点DefaultExtImp.sayHello方法 simpleExt.sayHello(url, "Jack"); url = url.addParameter("serviceKey", "other"); // 此时serviceKey=other,会调用扩展点OtherExtImp.sayHello方法 simpleExt.sayHello(url, "Tom");}
(4) Principle analysis of Dubbo extension point
Get the ExtensionLoader instance
ExtensionLoader.getExtensionLoader This method mainly returns an ExtensionLoader instance, the main logic is as follows:
- First get the instance corresponding to the extension class from the cache "EXTENSION_LOADERS";
- If the cache misses, create a new instance and save it in EXTENSION_LOADERS;
- In the ExtensionLoader construction method, an ExtensionFactory will be initialized;
Get extension point method getExtension
- First get the extension class from cachedClasses, if not, load it from META-INF/dubbo/internal/, META-INF/dubbo/, META-INF/services/ three directories.
- After obtaining the extension class, check whether there is an implementation of the extension class in the cache EXTENSION_INSTANCES, if not, instantiate it through reflection and put it in the cache.
- Implement dependency injection. If the current instance depends on other extension implementations, Dubbo will inject the dependency into the current instance.
- Wrap the extended class instance with the Wrapper decorator.
In the above steps, the first step is the key to loading the extension class, and the third and fourth steps are the specific implementation of Dubbo IoC and AOP. Dependency injection is achieved by calling injectExtension and only supports setter injection.
Get the adaptive extension point method getAdaptiveExtension
- Call the getAdaptiveExtensionClass method to obtain the adaptive extension Class object.
- Instantiate via reflection. Call the injectExtension method to inject dependencies into the extension class instance.
Although the above three processes are similar to the acquisition method of ordinary extension points, when processing Class objects, Dubbo will dynamically generate dynamic proxy classes of adaptive extension points, and then use javassist (default) to compile the source code to get the proxy class Class instance . The source code of 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.SimpleAdaptiveExt.class).getExtension(extName); extension.sayHello(arg0, arg1); }}
From the above code, we can see that in the method SayHello, the value corresponding to the serviceKey in the url will be obtained. If there is one, the extension point corresponding to the value will be used, otherwise, the default extension point will be used.
(5) Summary of Dubbo SPI
Dubbo's extension point loading is enhanced from the JDK SPI extension point discovery mechanism, and the following issues of the JDK SPI are improved:
- JDK SPI instantiates all implementations of extension points at one time, while Dubbo can use adaptive extension points and instantiate them when extension methods are called.
- Added support for IoC, an extension point can inject other extension points through a setter.
- Added AOP support, based on the Wrapper wrapper class to enhance the original extension class instance.
5. Prospects of custom technology combined with SPI in multi-tenant systems
The dynamic personalized configuration and customization technology in the multi-tenant system can meet the personalized requirements of different tenants, but a large number of customization tasks may make the system very complicated.
In order to facilitate the management and maintenance of the personalized configuration of different tenants, combined with the idea that SPI can use different extension implementations to enable or extend the components in the framework, we can design a tenant personalized customization management platform, which can manage the customization of each tenant. Configuration, developers abstract the individual differences of different tenants into individual customization points, the customization management platform can collect and manage the information of these customization points, and the business system can obtain the personalized configuration of the tenants from the customization platform at runtime and load the corresponding configuration. The extended implementation of , so as to meet the individual needs of different tenants. The overall structure is as follows:
The main functions and features of the Tenant Personalized Customization Management Platform are as follows:
- Abstract customization point: Developers abstract tenant characteristics into different customization point interfaces, and have different extension implementations for tenants with different characteristics.
- Customization point discovery: The customization point and implementation information of each service need to be reported to the customization management platform.
- Customized tenant personalized configuration: Operators can configure different customization points according to the characteristics of the tenant.
- Dynamic loading: When a tenant accesses the specific services 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 chain of responsibility/decorator pattern.
- Tenant isolation: After the operator sets the personalized configuration for the tenant, the customization management platform can store the configuration information in the dimension of the tenant, thereby realizing the isolation of the customized content of different tenants.
- Custom reuse: Reuse the configuration for tenants' common features or use the default configuration for those tenants that do not have the configuration.
The tenant personalized customization management platform can manage the tenant personalized characteristics in the form of metadata. In the future, as long as the personalized requirements of the new tenant can be described by the metadata of the existing customization point, only the configuration method needs to be modified to meet the new requirements. Even if the requirements cannot be met, it is only necessary to add or implement a custom interface and report it to the custom management platform, which makes the system easier to maintain and has higher code reusability.
References
"Dubbo 2.7 Development Guide"
"Spring Cloud Alibaba Microservice Principle and Practice"
about the author
Yang Liang, senior Java development engineer of NetEase Cloud Merchant, is responsible for the design and development of the public business modules and internal middleware of the cloud merchant platform.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。