Who can’t understand Java SPI in depth?

1. SPI Introduction & Examples

1.1 Introduction to SPI

SPI is a mechanism provided by Java to support third-party implementation or extension interfaces. Its full name is Service Provider Loader. Traditional API callers have no right to choose the implementation of the interface and can only make calls according to the implementation of the interface provider. With the SPI provider, the implementation can be externalized to the caller, which greatly increases the scalability and pluggability of the module. .

1.2 Java SPI implementation elements:

SPI interface: define a standard interface, which is also the strength of Java, and various standards are defined

SPI implementation: implement the standard interface, different manufacturers can have different implementations

SPI configuration: In the META-INF/services directory, create a file named the fully qualified class name of the interface, and the content is the fully qualified class name of each implementation class

SPI loading: Use ServiceLoader to load SPI configuration services, which is the core of SPI, and its essence is class loading.

1.3 SPI interface definition

Let's start with a set of our own SPI to see, according to the above steps, first define a standard interface

package com.star.spi.hello;
public interface IHelloService {
    
    
      String sayHi(String name);
}

1.4 SPI interface implementation

Below we simulate two implementations, one for simulating the Java language and one for simulating Python

package com.star.spi.hello;
public class JavaHelloService implements IHelloService {
    
    
    @Override
    public String sayHi(String name) {
    
    
        return String.format("hello %s from java !", name);
    }
}
package com.star.spi.hello;
public class Py3HelloService implements IHelloService {
    
    
    @Override
    public String sayHi(String name) {
    
    
        return String.format("hello %s from python3 !", name);
    }
}

1.5 SPI configuration

If you want to use Java SPI, you must follow the corresponding rules. Create the META-INF/services directory in the project. The file name is the fully qualified class name of the interface: com.star.spi.hello.IHelloService. The content is the fully qualified class name of the implementation class.

com.star.spi.hello.Py3HelloService
com.star.spi.hello.JavaHelloService

1.6 SPI loading

It is relatively simple to use, ServiceLoader realizes everything silently


package com.star.spi.hello;

import java.util.ServiceLoader;

public class HelloSpi {
    
    
    public static void main(String[] args) {
    
    
        ServiceLoader<IHelloService> serviceLoader = ServiceLoader.load(IHelloService.class);
        serviceLoader.forEach(item -> System.out.println(item.sayHi("spi")));
    }
}

console output

hello spi from python3 !
hello spi from java !

2. Interpretation of SPI source code

We have run the SPI above, and we can see that the key to the use of SPI lies in ServiceLoader, which is essentially a class loader that loads the target implementation class according to the external configuration. Let's take a look at its internal working mechanism and how it is parsed and loaded.

2.1 ServiceLoader overview

package java.util;

public final class ServiceLoader<S>
    implements Iterable<S> 
    
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

2.2 ServiceLoader loads SPI

2.2.1 Static load method

Let’s first look at the entry load function, which uses the current thread class loader to load a limited interface or abstract class.

public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
                                           ClassLoader loader){
    
    
    return new ServiceLoader<>(service, loader);
}
2.2.2 Construction method

You can see that the last step of the above code creates a new ServiceLoader object. Next, enter the construction method. You can see the key step of the reload method. First, the cache is cleared, and then lazy loading is performed.

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    
    
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
public void reload() {
    
    
   providers.clear();
   lookupIterator = new LazyIterator(service, loader);
}
2.2.3 Iterator

ServiceLoader itself implements the Iterator interface. In the iterator function, if there is already a cache in the provider, the iterator of the object (LinkedHashMap) is directly returned. Otherwise, the lazy loading iterator created above is used for iteration.


public Iterator<S> iterator() {
    
    
    return new Iterator<S>() {
    
    
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
    
    
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        public S next() {
    
    
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
    
    
            throw new UnsupportedOperationException();
        }
    };
}
2.2.4 LazyIterator

LazyIterator itself is also an iterator, used to actually load the implementation class. Use the class loader to specify the directory to load the fully qualified class name, then create the instance through reflection, and put it into the cache after creation.

private boolean hasNextService() {
    
    
    if (nextName != null) {
    
    
        return true;
    }
    if (configs == null) {
    
    
        try {
    
    
            //  目标实现类
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
    
    
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
    
    
        if (!configs.hasMoreElements()) {
    
    
            return false;
        }
        // 解析实现类名
        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 {
    
    
        // 返回目标类
        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 {
    
    
        // 实例化实现类
        S p = service.cast(c.newInstance());
        // 放入缓存
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
    
    
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

2.3 Class loader description

Careful friends should have discovered that the class loader of the current thread is used in the above class loading process. Note here that due to the parent delegation mechanism, SPI is located in the core library and is loaded by the Bootstrap class loader. Bootstrap cannot load the SPI implementation class, and the implementation class can only be loaded by the App class loader.

3. Common applications of SPI

3.1 SPI database driver

Java only provides a standard data-driven interface, and the implementation is completed by different database vendors. The standard driver interface is: java.sql.Driver. You can take a look at the mysql driver package.

Insert image description here

It can be seen that the standard Java SPI is used, so we only need a simple line of code to get the database connection

DriverManager.getConnection("", "", "");
可以看到DriverManager一上来就执行了静态代码块,进行驱动初始化

static {
    
    
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

Then use SPI technology to load Driver.class, so that Mysql is scanned

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
所以在getConnection方法时,已经拿到了已注册的驱动

for(DriverInfo aDriver : registeredDrivers) {
    
    ...}

RegisteredDrivers registers itself when Mysql Driver is loaded by SPI, perfectly cooperates to register the manufacturer's implementation class into the Java standard

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

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!");
        }
    }
}

The above is the process of loading the database driver using SPI, which is concise and clear

3.2 SpringBoot of SPI

Let's continue to look at the application of SPI in SpringBoot. The reason why SpringBoot is simple is because it helps us do a lot of automatic assembly. Let’s start with the startup class

@SpringBootApplication
public class App {
    
    ...}
一个注解就能搞定一切?这只是表面,继续进入该注解看下

@SpringBootConfiguration
@EnableAutoConfiguration
EnableAutoConfiguration中又包含了新的注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    
    }

Among them, @AutoConfigurationPackage will use the package of the class it modifies as the root path, which is why the startup class is usually placed in the root path.

Then enter AutoConfigurationImportSelector, because this class is directly imported here

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    
    
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
        getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
        + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    
    
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
    
    
        return result;
    } else {
    
    
        HashMap result = new HashMap();

        try {
    
    
            Enumeration urls = classLoader.getResources("META-INF/spring.factories");
            ...
        }
      

You can see the key loading method, the loading path META-INF/spring.factories, save it in the form of key and value in the file, and automate the assembly by loading the fully qualified class name under the file, specifically: How do they work in series?

All SpringBoot dependent starters will depend on the spring-boot-starter package, which in turn will depend on the spring-boot-autoconfigure package
Insert image description here

During the loading instantiation process, according to various configuration conditions @ConditionalXXX, the automatic assembly task can be intelligently completed.

Unknowingly, I have written too much. Let’s start with the next article about Dubbo’s SPI introduction. It can be regarded as an extension of Java SPI, but the idea of ​​​​SPI has not changed.

Guess you like

Origin blog.csdn.net/weixin_43275277/article/details/127860132