preface

SPI, or Service Provider Interface, is a Service discovery mechanism. It automatically loads the classes defined in the files by looking for them in the META-INF/services folder in the ClassPath path.

This mechanism makes it possible to extend many frameworks, such as Dubbo, JDBC, and SpringBoot, using the SPI mechanism. Although their implementations differ, the principles are similar. Today we’ll take a look at who SPI really is and what role it plays in many open source frameworks.

SPI in JDK

Let’s start with the JDK and take a very simple example to see how it works.

1. Chestnut

First, we need to define an interface, SpiService

public interface SpiService {
    void println();
}
Copy the code

Then, you define an implementation class, no other meaning, just to print.

public class SpiServiceImpl implements SpiService {
    @Override
    public void println() {
        System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- --"); }}Copy the code

Finally, add a file to the resources configuration. The file name is the fully qualified class name of the interface, the content is the fully qualified class name of the implementation class, and multiple implementation classes are separated by a newline character.

com.youyouxunyin.service.impl.SpiServiceImpl
Copy the code

2, test,

We can then get an instance of the implementation class using the Serviceloader.load method and call its methods.

public static void main(String[] args){
    ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
    Iterator<SpiService> iterator = load.iterator();
    while(iterator.hasNext()){ SpiService service = iterator.next(); service.println(); }}Copy the code

3. Source code analysis

First, let’s take a look at ServiceLoader and its class structure.

Public final class ServiceLoader<S> implements Iterable<S>{// Configuration file path private static final String PREFIX ="META-INF/services/"; Private final Class<S> service; Private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); Private final ClassLoader loader; // Inner class, the real load service class private LazyIterator lookupIterator; }Copy the code

When we call the load method, we don’t actually load and find the service class. Instead, the ServiceLoader constructor is called, and the most important thing here is to instantiate the inner class LazyIterator, which is the next big thing.

Private ServiceLoader(Class<S> SVC, ClassLoader cl) {private ServiceLoader(Class<S> SVC, ClassLoader CL) {private ServiceLoader(Class<S> SVC, ClassLoader CL) {"Service interface cannot be null"); // class loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl; / / access controller acc = (System. GetSecurityManager ()! = null) ? AccessController.getContext() : null; Providers.clear (); // Providers.clear (); LazyIterator lookupIterator = new LazyIterator(service, loader); }Copy the code

The process of finding and creating implementation classes is done in LazyIterator. When we call iterator.hasNext and iterator.next, we are actually calling the corresponding methods of LazyIterator.

public Iterator<S> iterator() {

    return new Iterator<S>() {
	
    	public boolean hasNext() {
    		return lookupIterator.hasNext();
    	}
    	public S next() {
    		returnlookupIterator.next(); }... }; }Copy the code

So, we’ll focus on the lookupiterator.hasNext () method, which eventually calls hasNextService, where it returns the implementation class name.

private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;	
    private boolean hasNextService() {// On the second call, parsing is complete and returns directlyif(nextName ! = null) {return true;
    	}
    	if(configs == null) {// meta-INF /services/ File is the service class supervisor / / META-INF/services/com.viewscenes.net. The spi. SPIService String fullName = PREFIX + service. The getName ();  // Convert the file path to the URL object configs = loader.getResources(fullName); }while((pending == null) || ! Pending.hasnext ()) {return pending = parse(service, configs.nextelement ()); NextName = pending.next();return true; }}Copy the code

And then when we call next () method, call to lookupIterator. NextService. It creates and returns an instance of the implementation class by reflection.

private S nextService() {// fully qualified class name String cn = nextName; nextName = null; // Create the Class object Class<? > c = Class.forName(cn,false, loader); S p = service.cast(c.newinstance ()); Providers.put (cn, p);return p; 
}
Copy the code

So far, you’ve got an instance of the class.

Two, JDBC applications

We began by saying that the SPI mechanism makes it possible to extend many frameworks, and JDBC applies it.

Driven in the past, need to set up the database Connection, and then through the DriverManager. GetConnection get a Connection.

String url = "jdbc:mysql:///consult? serverTimezone=UTC";
String user = "root";
String password = "root";

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
Copy the code

Now that this step is no longer required to set up a database-driven connection, how does it tell which database it is? The answer lies in SPI.

1, load,

Back to the DriverManager class, which does one of the more important things in static code blocks. Obviously, it has already initialized the database-driven connection through the SPI mechanism.

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized"); }}Copy the code

LoadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers: loadInitialDrivers

META-INF/services/java.sql.Driver

private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    	public Void run() {// Obviously, it loads the service class of the Driver interface, Driver interface package: Java. SQL. Driver / / so it is looking for meta-inf/services/Java SQL. The Driver file ServiceLoader < Driver > loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); Try {// Create the objectwhile(driversIterator.hasNext()) {
    		    	driversIterator.next();
    		    }
    		} catch(Throwable t) {
    		    // Do nothing
    		}
    		returnnull; }}); }Copy the code

So, where is this file? MySQL jar: com.mysql.cj.jdbc.driver

2. Create an instance

The com.mysql.cj.jdbc.driver fully qualified class name was found in MySQL in the previous step, and an instance of this class is created when the next method is called. One thing it does is register its own instance with DriverManager.

Public class Driver extends NonRegisteringDriver implements java.sql.Driver {static {try {// Registers the registration method of DriverManager / / to add instance registeredDrivers set DriverManager. RegisterDriver (new Driver ()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); }}}Copy the code

Create a Connection

DriverManager. GetConnection () method is to create a connection, it through the loop has registered database driver, call the connect method, getting connection and return.

private static Connection getConnection(String url, Properties info, Class<? >callerThrows SQLException {//registeredDrivers contains the com.mysql.cj.jdbc.driver instancefor(DriverInfo aDriver : registeredDrivers) {
    	if(isDriverAllowed(aDriver.driver, callerConnection con = adriver.driver. connect(url, info);if(con ! = null) {return (con);
    	    	}
    	    }catch (SQLException ex) {
    	    	if(reason == null) { reason = ex; }}}else {
    	    println("skipping: "+ aDriver.getClass().getName()); }}}Copy the code

4, extension,

Now that we know that JDBC creates database connections this way, can we extend it further? If we create a java.sql.Driver file ourselves and customize the implementation class MySQLDriver, we can dynamically modify some information before and after getting the connection.

Or under the project resources to create file, the file contents for custom driver class com. Youyouxunyin. Driver. MySQLDriver

Our MySQLDriver implementation class inherits from MySQL NonRegisteringDriver and implements the java.sql.Driver interface. This class is called when the connect method is called, but the actual creation process is done by MySQL.

public class MySQLDriver extends NonRegisteringDriver implements Driver{ static { try { DriverManager.registerDriver(new  MySQLDriver()); } catch (SQLException e) { e.printStackTrace(); } } public MySQLDriver() throws SQLException {} @Override public Connection connect(String url, Properties info) throws SQLException { System.out.println("Ready to create database connection. Url :"+url);
        System.out.println("JDBC configuration Information:"+info);
        info.setProperty("user"."root");
        Connection connection =  super.connect(url, info);
        System.out.println("Database connection created!"+connection.toString());
        returnconnection; }}Copy the code

That way, when we get a database connection, we call it here.

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the output -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - to create a database connection. Url: JDBC: mysql: / / / consult? ServerTimezone =UTC JDBC configuration information: {user=root, password=root} Database connection created! com.mysql.cj.jdbc.ConnectionImpl@7cf10a6fCopy the code

Application in SpringBoot

Spring Boot provides a quick way to create Spring-based applications that can be used in production environments. It’s based on the Spring framework, is more convention oriented than configuration, and is designed to get you up and running as quickly as possible.

SpringBoot Web applications run normally even without any configuration files. SpringBoot relies on auto-configuration to accomplish this magic.

At this point, we must keep an eye on one thing: the SpringFactoriesLoader, on which automatic configuration is loaded.

1. Configuration files

The SpringFactoriesLoader is responsible for loading the configuration. We open the class and see that the path to the loaded file is: meta-INF /spring.factories

I searched for this file in the project and found four Jar packages containing it:

  • Spring - the boot - 2.1.9. RELEASE. The jar
  • Spring beans - 5.1.10. RELEASE. The jar
  • Spring - the boot - autoconfigure - 2.1.9. RELEASE. The jar
  • Mybatis - spring - the boot - autoconfigure - 2.1.0. Jar

So what are they? It’s a mapping of interfaces and classes. Here I will not stick, interested partners to see their own.

In SpringBoot start when, for example, to load all the ApplicationContextInitializer, you can do this:

SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, classLoader)

2. Load the file

LoadSpringFactories is responsible for reading all the content of the Spring. factories file.

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {

    MultiValueMap<String, String> result = cache.get(classLoader);
    if(result ! = null) {returnresult; } try {// Get the path to all spring.factories: Enumeration<URL> urls = lassLoader.getResources("META-INF/spring.factories");
    	result = new LinkedMultiValueMap<>();
    	while(urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource);for(Map.Entry<? ,? > entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim();for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
    	    	    result.add(factoryClassName, factoryName.trim());
    	    	}
    	    }
    	}
    	cache.put(classLoader, result);
    	return result;
    }
    catch (IOException ex) {
    	throw new IllegalArgumentException("Unable to load factories from location [" +
    		FACTORIES_RESOURCE_LOCATION + "]", ex); }}Copy the code

As you can see, it doesn’t use the SPI mechanism in the JDK to load these classes, but the principle is similar. It’s all done through a configuration file, loading and parsing the contents of the file, and then creating instances through reflection.

3. Get involved

If you want to participate in the SpringBoot initialization process, now we have another way.

We also create a spring.factories file to customize an initializer.

org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer

public class MyContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { System.out.println(configurableApplicationContext); }}Copy the code

4. Application of Dubbo

The familiar Dubbo is no exception, which also loads all components through the SPI mechanism. Similarly, Dubbo does not use Java’s native SPI mechanism, but rather enhances it to better meet its needs. In Dubbo, SPI is a very important module. Based on SPI, we can easily extend Dubbo.

If you are not familiar with the principle, you can refer to the author’s article:

SPI and adaptive extension mechanisms in Dubbo

It is also used by creating a file in meta-INF /services and writing the associated class name.

For usage scenarios, you can refer to SpringBoot+Dubbo integrated ELK combat