An Introduction to Java SPI Mechanism

An Introduction to Java SPI Mechanism

Table of Contents

1. Overview

1. What is SPI

2. What are the differences compared to API

3. What is the use of SPI

4. SPI Working Mechanism

2. ServiceLoader

1. Source Code Analysis

2. Summary

3. Practical Application Scenarios of SPI

1. JDBC

2. Spring

3. Dubbo

4. Conclusion

1

Overview

What is SPI

SPI stands for Service Provider Interface, which is an interface for service providers.

SPI separates the service interface from the specific service implementation, decoupling the service caller from the service implementer, which enhances the program’s extensibility and maintainability. Additionally, modifying or replacing the service implementation does not require changes to the caller.

Many places in Java utilize the SPI mechanism, such as database driver loading in JDBC, Spring, and the extension implementations in Dubbo.

What are the differences compared to API

An Introduction to Java SPI Mechanism

API: The implementation party of the interface is responsible for both the interface definition and implementation, with control over the interface resting with the service provider.

SPI: The service caller is responsible for the interface definition, allowing different implementation parties to have different implementations based on the interface definition, enabling dynamic loading of different implementation classes at runtime, with control over the interface resting with the service caller.

What is the use of SPI

Decoupling

In framework development, it is often necessary to rely on pluggable functionalities without wanting to implement specific adaptations (to maintain flexibility). The SPI mechanism allows the framework to decouple from service implementations by defining interfaces and dynamically loading implementations.

※ Scenario

  • A database connection pool library needs to support multiple database implementations (such as MySQL, PostgreSQL), which can be dynamically loaded through the SPI mechanism.

  • Logging frameworks (like SLF4J) can load different logging libraries (like Log4j, Logback) through the SPI mechanism.

Extensibility

The SPI mechanism provides the ability to dynamically discover and load services, allowing applications to easily implement extensions without modifying existing code.

※ Scenario

  • A file processing system needs to support different file formats (like JSON or XML). The SPI mechanism can dynamically discover different file parser plugins without hardcoding the supported formats in advance.

Dynamic Loading

SPI can be used to implement a plugin architecture, allowing for the addition or removal of modules by dynamically loading specific service implementations without needing to republish the entire system.

※ Scenario

  • A web server (like Tomcat) can dynamically load different HTTP handlers or filters through the SPI mechanism.

  • A data analysis system can dynamically load new analysis algorithms through the SPI mechanism.

SPI Working Mechanism

An Introduction to Java SPI Mechanism

2

ServiceLoader

ServiceLoader is a service loading class provided in the JDK, located in the java.util package, marked as final and cannot be inherited, and is the core of implementing the SPI mechanism.

Source Code Analysis

public final class ServiceLoader<S> implements Iterable<S> {    // Default loading path prefix    private static final String PREFIX = "META-INF/services/";       // The class or interface representing the service being loaded    // The loaded instance or interface    private final Class<S> service;        // The class loader used to locate, load, and instantiate providers    // Class loader    private final ClassLoader loader;       // The access control context taken when the ServiceLoader is created    private final AccessControlContext acc;        // Cached providers, in instantiation order    // Local cache, key: class name value; class    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();      // The current lazy-lookup iterator    // Iterator    private LazyIterator lookupIterator;}

※ load method

// Exposed loading method for external usepublic static <S> ServiceLoader<S> load(Class<S> service,                                        ClassLoader loader){    return new ServiceLoader<>(service, loader);}public static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}// Constructor private methodprivate ServiceLoader(Class<S> svc, ClassLoader cl) {    service = Objects.requireNonNull(svc, "Service interface cannot be null");    // If a classLoader is specified, use that classLoader; if not, use the default classLoader    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;    reload();}

ClassLoader cl = Thread.currentThread().getContextClassLoader(); by obtaining the thread context class loader (Thread Context ClassLoader).

※ reload method

public void reload() {    // Clear cache, set the head and tail of linkedHashMap to null    providers.clear();    lookupIterator = new LazyIterator(service, loader);}

ServiceLoader implements the Iterable interface, gaining the ability to iterate. When the iterator method is called, it first looks in the ServiceLoader provider cache; if not found, it looks in the LazyIterator .

public Iterator<S> iterator() {    return new Iterator<S>() {        // Local cache providers        Iterator<Map.Entry<String,S>> knownProviders            = providers.entrySet().iterator();               public boolean hasNext() {            // Prefer to check local cache            if (knownProviders.hasNext())                return true;            // If not, check in LazyIterator            return lookupIterator.hasNext();        }              public S next() {            if (knownProviders.hasNext())                return knownProviders.next().getValue();            return lookupIterator.next();        }             public void remove() {            throw new UnsupportedOperationException();        }        };}

LazyIterator gives ServiceLoader lazy loading capability, loading the corresponding implementation class only when the iterator method is called or traversed. The core code is as follows:

// Private inner class implementing fully-lazy provider lookup//private class LazyIterator implements Iterator<S> {        Class<S> service;    ClassLoader loader;    Enumeration<URL> configs = null;    Iterator<String> pending = null;    String nextName = null;        private LazyIterator(Class<S> service, ClassLoader loader) {        this.service = service;        this.loader = loader;    }        private boolean hasNextService() {        if (nextName != null) {            return true;        }        if (configs == null) {            try {            // Configuration file path, default starts with /META-INF/services/                String fullName = PREFIX + service.getName();                if (loader == null)                    configs = ClassLoader.getSystemResources(fullName);                else // Read implementation class name                    configs = loader.getResources(fullName);            } catch (IOException x) {                fail(service, "Error locating configuration files", x);            }        }        while ((pending == null) || !pending.hasNext()) {            if (!configs.hasMoreElements()) {                return false;            }            // Parse class names in the configuration file            pending = parse(service, configs.nextElement());        }        nextName = pending.next();        return true;    }        private S nextService() {        if (!hasNextService())            throw new NoSuchElementException();        String cn = nextName;        nextName = null;        Class<?> c = null;        try {        // Load the corresponding implementation class            c = Class.forName(cn, false, loader);        } catch (ClassNotFoundException x) {            fail(service,                 "Provider " + cn + " not found");        }        if (!service.isAssignableFrom(c)) {            fail(service,                 "Provider " + cn  + " not a subtype");        }        try {        // Create implementation class            S p = service.cast(c.newInstance());        // Put into cache            providers.put(cn, p);            return p;        } catch (Throwable x) {            fail(service,                 "Provider " + cn + " could not be instantiated",                 x);        }        throw new Error();          // This cannot happen    }        public boolean hasNext() {        if (acc == null) {            return hasNextService();        } else {            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {                public Boolean run() { return hasNextService(); }            };            return AccessController.doPrivileged(action, acc);        }    }        public S next() {        if (acc == null) {            return nextService();        } else {            PrivilegedAction<S> action = new PrivilegedAction<S>() {                public S run() { return nextService(); }            };            return AccessController.doPrivileged(action, acc);        }    }        public void remove() {        throw new UnsupportedOperationException();    }} 

Summary

ServiceLoader essentially reads the files with fully qualified names of the corresponding interfaces in the agreed directory (/META-INF/services/) and loads all the interface implementation classes defined in the file through reflection, thus decoupling the interface from the implementation.

※ Advantages

  • Decoupling: The interface and implementation are separated, and there is no need to hardcode implementation classes in the code.

  • Extensibility: Adding new implementations only requires adding configurations without modifying existing code.

※ Disadvantages

  • Thread Safety: ServiceLoader is not thread-safe and cannot guarantee singleton behavior.

  • Performance Overhead: Each iteration reloads the file (which can be solved through caching) and will instantiate all implementation classes specified in the configuration file.

  • Lack of Robustness: Configuration errors (like class not found) will throw exceptions instead of gracefully degrading.

3

Practical Application Scenarios of SPI

JDBC

The Driver interface is defined in the JDK for connecting to databases.

package java.sql;import java.util.logging.Logger;public interface Driver {       Connection connect(String url, java.util.Properties info)        throws SQLException;       boolean acceptsURL(String url) throws SQLException;    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)                         throws SQLException;    int getMajorVersion();    int getMinorVersion();    boolean jdbcCompliant();    public Logger getParentLogger() throws SQLFeatureNotSupportedException;}

Different database vendors implement this interface to connect to databases. For example, to obtain a database connection with MySQL, the sample code is as follows:

// Get database connectionDriverManager connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");// Create statementStatement statement = connection.createStatement();// Execute SQLResultSet resultSet = statement.executeQuery("select * from student");

During loading, DriverManager executes static code blocks to load drivers, with some core code as follows:

static {    loadInitialDrivers();    println("JDBC DriverManager initialized");}private static void loadInitialDrivers() {    String drivers;    try {        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {            public String run() {                return System.getProperty("jdbc.drivers");            }        });    } catch (Exception ex) {        drivers = null;    }        // If the driver is packaged as a service provider, load it.    // Get all drivers through the class loader    AccessController.doPrivileged(new PrivilegedAction<Void>() {        public Void run() {            // Here, ServiceLoader is used to load interface implementation classes            // For example, mysql-connector-java loads com.mysql.jdbc.Driver and com.mysql.fabric.jdbc.FabricMySQLDriver            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);            Iterator<Driver> driversIterator = loadedDrivers.iterator();            try{                while(driversIterator.hasNext()) {                    driversIterator.next();                }            } catch(Throwable t) {            // Do nothing            }            return null;        }    });        println("DriverManager.initialize: jdbc.drivers = " + drivers);        if (drivers == null || drivers.equals("")) {        return;    }    String[] driversList = drivers.split(":");    println("number of Drivers:" + driversList.length);    for (String aDriver : driversList) {        try {            println("DriverManager.Initialize: loading " + aDriver);            // Load database driver, for example, mysql-connector-java loads com.mysql.jdbc.Driver and com.mysql.fabric.jdbc.FabricMySQLDriver            // and registers it in registeredDrivers            Class.forName(aDriver, true,                    ClassLoader.getSystemClassLoader());        } catch (Exception ex) {            println("DriverManager.Initialize: load failed: " + ex);        }    }}

Then, in the getConnection method, it iterates through the already loaded drivers and calls the connect method to establish a connection.

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); // Iterate through registered drivers and call connect method for connectionfor(DriverInfo aDriver : registeredDrivers) {     if(isDriverAllowed(aDriver.driver, callerCL)) {        try {            println("    trying " + aDriver.driver.getClass().getName());            Connection con = aDriver.driver.connect(url, info);            if (con != null) {                // Success!                println("getConnection returning " + aDriver.driver.getClass().getName());                return (con);            }        } catch (SQLException ex) {            if (reason == null) {                reason = ex;            }        }     } else {        println("    skipping: " + aDriver.getClass().getName());    }}
public class Driver extends NonRegisteringDriver implements java.sql.Driver {    public Driver() throws SQLException {    }       static {        try {            DriverManager.registerDriver(new Driver());        } catch (SQLException var1) {            throw new RuntimeException("Can't register driver!");        }}

Spring

Spring does not directly use the Java SPI mechanism, but the spring.factories mechanism in Spring is similar to SPI and has a more powerful extension mechanism, achieving automatic assembly and context initialization by reading the META-INF/spring.factories file.

// Core logic of SpringFactoriesLoader source codepublic final class SpringFactoriesLoader {    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {        // Load configurations from spring.factories files in all jar packages        Enumeration<URL> urls = (classLoader != null ?                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));        List<String> result = new ArrayList<>();        while (urls.hasMoreElements()) {            Properties properties = PropertiesLoaderUtils.loadProperties(                new UrlResource(urls.nextElement()));            String factoryClassNames = properties.getProperty(factoryType.getName());            // Support multiple implementations separated by commas            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));        }        return result;    }    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {        // 1. Load all implementation class names        List<String> names = loadFactoryNames(factoryType, classLoader);        // 2. Instantiate all implementations        List<T> instances = new ArrayList<>(names.size());        for (String name : names) {            Class<?> instanceClass = ClassUtils.forName(name, classLoader);            instances.add((T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance());        }        // 3. Sort by @Order        AnnotationAwareOrderComparator.sort(instances);        return instances;    }    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);    if (result != null) {        return result;    } else {        try {            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");            MultiValueMap<String, String> result = new LinkedMultiValueMap();               while(urls.hasMoreElements()) {                URL url = (URL)urls.nextElement();                UrlResource resource = new UrlResource(url);                Properties properties = PropertiesLoaderUtils.loadProperties(resource);                Iterator var6 = properties.entrySet().iterator();                      while(var6.hasNext()) {                    Map.Entry<?, ?> entry = (Map.Entry)var6.next();                    String factoryClassName = ((String)entry.getKey()).trim();                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());                    int var10 = var9.length;                     for(int var11 = 0; var11 < var10; ++var11) {                        String factoryName = var9[var11];                        result.add(factoryClassName, factoryName.trim());                    }                }            }            // Read files in the META-INF/spring.factories folder, parse the file content, and cache it in a Map            cache.put(classLoader, result);            return result;        } catch (IOException var13) {            IOException ex = var13;            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);        }    }}

The format of the spring.factories file is xxx=xxxx, where the key before the = sign is the fully qualified name of the interface, and the value after the = sign is the fully qualified name of the interface implementation class, with multiple implementation classes separated by commas. The key specified in the figure below is EnableAutoConfiguration, so Spring will register the classes after the = sign as beans. The principle of Spring’s automatic assembly will not be elaborated here.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.apache.hadoop.hbase.client.MonitorAlicloudHBaseAutoConfiguration

Dubbo

Dubbo’s extensibility mechanism also uses SPI, but it is not the native SPI; it has been optimized.

As analyzed in the source code of ServiceLoader, SPI reads configuration files, iterates through all classes, and instantiates them. If some classes are not used, they are still loaded, causing resource waste; the Java SPI configuration file simply lists all extension implementations without naming them, making it impossible to reference them accurately in the program; it cannot achieve automatic injection and assembly, etc. Dubbo’s extension point mechanism optimizes these issues and provides dynamic extension capabilities.

Demo

Define an interface and use the @SPI annotation to mark it, indicating that this interface is an extension point that can be loaded by Dubbo’s ExtensionLoader.

@SPIpublic interface DemoSpi {    void say();}

Implementation of the interface:

public class DemoSpiImpl implements DemoSpi {    public void say() {    }}

Place the implementation class in a specific directory. When loading extension classes, Dubbo will read from the META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ directories. Here, create a file named after the DemoSpi interface in the META-INF/dubbo directory, with the following content:

demoSpiImpl = com.xxx.xxx.DemoSpiImpl (the fully qualified name of the DemoSpi implementation class)

Usage:

public class DubboSPITest {     @Test        public void sayHello() throws Exception {        ExtensionLoader<DemoSpi> extensionLoader =             ExtensionLoader.getExtensionLoader(DemoSpi.class);        DemoSpi demoSpi = extensionLoader.getExtension("demoSpiImpl");        demoSpi.sayHello();    }}

Source Code Analysis

From the above example, it can be seen that Dubbo mainly loads the implementation classes specified in the configuration file through ExtensionLoader, and the overall process is similar to that of ServiceLoader, with relevant optimizations.

※ Main steps of Dubbo loading extensions

  • Read and parse configuration files

  • Cache all extension implementations

  • Based on the user-executed extension name, instantiate the corresponding extension implementation

  • Perform IOC injection of extension instantiation properties and wrap the instantiated extensions to achieve AOP features

The entry point for loading fixed extension classes in SPI is the getExtension method of ExtensionLoader, which mainly calls the createExtension method to obtain the extension instance:

private T createExtension(String name, boolean wrap) {    // Load all extension classes from the configuration file, obtaining a mapping of "configuration item name" to "configuration class"        Class<?> clazz = getExtensionClasses().get(name);    // If there is no extension for this interface, or if the implementation class of this interface does not allow duplicates but actually duplicates, throw an exception        if (clazz == null || unacceptableExceptions.contains(name)) {        throw findException(name);    }    try {        T instance = (T) EXTENSION_INSTANCES.get(clazz);        // This code ensures that the extension class is only constructed once, i.e., it is a singleton.                if (instance == null) {            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());            instance = (T) EXTENSION_INSTANCES.get(clazz);        }        // Inject dependencies into the instance                // If wrapping is enabled, automatically wrap it.                // For example, if I defined an extension of DubboProtocol based on Protocol, but in Dubbo, it is not directly used as DubboProtocol, but its wrapper class ProtocolListenerWrapper                if (wrap) {            List<Class<?>> wrapperClassesList = new ArrayList<>();                   if (cachedWrapperClasses != null) {                wrapperClassesList.addAll(cachedWrapperClasses);                            wrapperClassesList.sort(WrapperComparator.COMPARATOR);                Collections.reverse(wrapperClassesList);            }            // Loop to create Wrapper instances                        if (CollectionUtils.isNotEmpty(wrapperClassesList)) {                for (Class<?> wrapperClass : wrapperClassesList) {                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);                    if (wrapper == null                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {                        // Pass the current instance as a parameter to the constructor of the Wrapper and create the Wrapper instance through reflection.                        // Then inject dependencies into the Wrapper instance, and finally assign the Wrapper instance back to the instance variable                                                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));                    }                }            }        }        // Initialize                initExtension(instance);        return instance;    } catch (Throwable t) {        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +                type + ") couldn't be instantiated: " + t.getMessage(), t);    }}

※ Full process of the above code

  • Get all extension classes through getExtensionClasses

  • Create extension objects through reflection

  • Inject dependencies into the extension object

  • Wrap the extension object in the corresponding Wrapper object

  • Initialize the extension object

Getting all extension classes through getExtensionClasses is similar to JDK SPI, first checking the cache, if not found, locking and checking the cache again, and if still not found, loading extension classes through loadExtensionClasses.

private Map<String, Class<?>> getExtensionClasses() {    Map<String, Class<?>> classes = (Map)this.cachedClasses.get();    if (classes == null) {        synchronized(this.cachedClasses) {            classes = (Map)this.cachedClasses.get();            if (classes == null) {                classes = this.loadExtensionClasses();                this.cachedClasses.set(classes);            }        }    }     return classes;}

loadExtensionClasses does two things: first, it parses the @SPI annotation, and second, it calls loadDirectory to load the specified folder configuration files.

private Map<String, Class<?>> loadExtensionClasses() {    // Cache the default SPI extension name        cacheDefaultExtensionName();     Map<String, Class<?>> extensionClasses = new HashMap<>();    // Load files in the specified folder based on the strategy        // Read configuration files in META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ directories        for (LoadingStrategy strategy : strategies) {        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());    }     return extensionClasses;}

In the loadDirectory method, the full path of the files in the specified folder is obtained, and the loadResource method is called to load the resources.

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, boolean overridden, String... excludedPackages) {    try {        BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8));        Throwable var7 = null;               try {            String line;            try {            // Read each line in the configuration file                while((line = reader.readLine()) != null) {                    // Only read the content before #, the content after # is a comment                    int ci = line.indexOf(35);                    if (ci >= 0) {                        line = line.substring(0, ci);                    }                                line = line.trim();                    if (line.length() > 0) {                        try {                            String name = null;                            int i = line.indexOf(61);                            if (i > 0) {                            // Read the name before = as name, and the fully qualified name of the implementation class after =                                name = line.substring(0, i).trim();                                line = line.substring(i + 1).trim();                            }                                              if (line.length() > 0 && !this.isExcluded(line, excludedPackages)) {                                this.loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);                            }                        } catch (Throwable var21) {                            Throwable t = var21;                            IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + this.type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);                            this.exceptions.put(line, e);                        }                    }                }            } catch (Throwable var22) {                var7 = var22;                throw var22;            }        } finally {            //close ...                }    } catch (Throwable var24) {        Throwable t = var24;        logger.error("Exception occurred when loading extension class (interface: " + this.type + ", class file: " + resourceURL + ") in " + resourceURL, t);    }

loadResource mainly reads each line in the configuration file, dividing it into key and value parts, where the key is the implementation class name before the = sign, and the value is the implementation class instance after the = sign. The process of loading instances is done by the loadClass method, as follows:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,                       boolean overridden) throws NoSuchMethodException {    if (!type.isAssignableFrom(clazz)) {        throw new IllegalStateException("Error occurred when loading extension class (interface: " +                type + ", class line: " + clazz.getName() + "), class "                + clazz.getName() + " is not subtype of interface.");    }    // Check if the target class has a default constructor        if (clazz.getConstructor() == null) {            throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);        }        if (StringUtils.isEmpty(name)) {            // If name is empty, try to get the name from the Extension annotation or use the lowercase class name as the name            name = findAnnotationName(clazz);            if (name.length() == 0) {                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);            }        }              String[] names = NAME_SEPARATOR.split(name);        if (ArrayUtils.isNotEmpty(names)) {            // If the class has an Activate annotation, use the first element of the names array as the key,                        // Store the mapping of name to the Activate annotation object                        cacheActivateClass(clazz, names[0]);            for (String n : names) {                // Store the mapping of Class to name                                cacheName(clazz, n);                // Store the mapping of name to Class.                                // If the same extension name corresponds to multiple implementation classes, based on whether the override parameter allows overwriting, if not allowed, throw an exception.                                saveInExtensionClass(extensionClasses, clazz, n, overridden);            }        }    }}

Dubbo SPI Application Scenarios

※ Protocol Extension

# META-INF/dubbo/org.apache.dubbo.rpc.Protocoldubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocolhttp=org.apache.dubbo.rpc.protocol.http.HttpProtocolgrpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol

※ Cluster Fault Tolerance

@SPI(FailoverCluster.NAME)public interface Cluster {    @Adaptive    <T> Invoker<T> join(Directory<T> directory) throws RpcException;}// Usage example<dubbo:reference cluster="failfast"/>

※ Service Governance Filter

@SPIpublic interface Filter {    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;        public interface Listener {        void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);              void onError(Throwable t, Invoker<?> invoker, Invocation invocation);    }}

4

Conclusion

It is worth mentioning that there are some issues in the actual use of SPI:

※ Resource Waste and Performance Issues

As analyzed in the source code, ServiceLoader scans the META-INF/services directory and parses files, instantiating all implementation classes through reflection. If there are many implementation classes or if initialization is time-consuming, it can cause a certain degree of performance overhead and resource waste. Therefore, Dubbo’s ExtensionLoader optimizes this by caching and specifying keys in the configuration file to load only part of the implementation classes.

※ Multiple Implementation Class Loading Order Issues

The order in which ServiceLoader loads service provider implementations is determined by the order of jar packages in the classpath. If your logic depends on the first implementation obtained, exceptions may occur due to inconsistent loading order in different environments, so it is best to avoid relying on loading order.

※ Class Duplicate Loading Issues

If there are multiple jar packages in the classpath declaring the same implementation class for the same interface, or if the same jar package is loaded multiple times by different class loaders, it can lead to the same implementation class being loaded and instantiated multiple times, potentially causing singleton failures or resource conflicts.

Overall, the application scenarios of the SPI mechanism are still broad, with its core being the dynamic loading of interface implementations by reading configuration files, decoupling interface definitions from implementations, and achieving framework extensibility, which can be seen in many major frameworks.

Previous Articles

1. What is the Java volatile keyword? | Technology of DeWu

2. Community Search Offline Backtracking System Design: Architecture, Challenges, and Performance Optimization | Technology of DeWu

3. From Rust Modularization Exploration to DLB 2.0 Practice | Technology of DeWu

4. eBPF Assists NAS Minute-Level Pod Instance Tracing | Technology of DeWu

5. Implementation and Performance Optimization of Genuine Product Library Photo PWA Application | Technology of DeWu

Written by / Liuli

Follow DeWu Technology for technical content updates every Monday and Wednesday

If you find this article helpful, feel free to comment, share, and like it~

Unauthorized reproduction without the permission of DeWu Technology is strictly prohibited, otherwise legal responsibility will be pursued according to law.

Scan to add the assistant’s WeChat

If you have any questions or want to know more technical information, please add the assistant’s WeChat:

An Introduction to Java SPI Mechanism

Leave a Comment