In-depth understanding of SPI mechanism

1. What is SPI

SPI, full name Service Provider Interface, is a service discovery mechanism. It looks for files in the META-INF/services folder under the ClassPath path, and automatically loads the classes defined in the files.

This mechanism provides the possibility for many framework extensions. For example, the SPI mechanism is used in Dubbo and JDBC. Let's first look at how it is used through a very simple example.

1. Small chestnuts

First, we need to define an interface, SPIService

 

package com.viewscenes.netsupervisor.spi;
public interface SPIService {
    void execute();
}

Then, define two implementation classes, nothing else, just enter a sentence.

 

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl1.execute()");
    }
}
----------------------我是乖巧的分割线----------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
    public void execute() {
        System.out.println("SpiImpl2.execute()");
    }
}

Finally, configure and add a file under the ClassPath path. The file name is the fully qualified class name of the interface, and the content is the fully qualified class name of the implementation class. Multiple implementation classes are separated by newlines.
The file path is as follows:

 

SPI configuration file location

 

The content is the fully qualified class name of the implementing class:

 

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2

2. Test

Then we can ServiceLoader.load或者Service.providersget the instance of the implementation class through the method. where Service.providerspackage is located sun.misc.Serviceand ServiceLoader.loadpackage is located java.util.ServiceLoader.

 

public class Test {
    public static void main(String[] args) {    
        Iterator<SPIService> providers = Service.providers(SPIService.class);
        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

        while(providers.hasNext()) {
            SPIService ser = providers.next();
            ser.execute();
        }
        System.out.println("--------------------------------");
        Iterator<SPIService> iterator = load.iterator();
        while(iterator.hasNext()) {
            SPIService ser = iterator.next();
            ser.execute();
        }
    }
}

The output results of the two methods are consistent:

 

SpiImpl1.execute()
SpiImpl2.execute()
--------------------------------
SpiImpl1.execute()
SpiImpl2.execute()

2. Source code analysis

We see that one is located sun.misc包, one is located java.util包, and the source code under the sun package cannot be seen. Let's take ServiceLoader.load as an example, and see how it does it through the source code.

1、ServiceLoader

First of all, let's take a look at ServiceLoader and look at its class structure.

 

public final class ServiceLoader<S> implements Iterable<S>
    //配置文件的路径
    private static final String PREFIX = "META-INF/services/";
    //加载的服务类或接口
    private final Class<S> service;
    //已加载的服务类集合
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //类加载器
    private final ClassLoader loader;
    //内部类,真正加载服务类
    private LazyIterator lookupIterator;
}

2、Load

The load method creates some properties, the important thing is instantiating the inner class, LazyIterator. Finally return the instance of ServiceLoader.

 

public final class ServiceLoader<S> implements Iterable<S>
    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;
        //先清空
        providers.clear();
        //实例化内部类 
        LazyIterator lookupIterator = new LazyIterator(service, loader);
    }
}

3. Find the implementation class

The process of finding the implementation class and creating the implementation class is completed in LazyIterator. When we call the iterator.hasNext and iterator.next methods, we actually call the corresponding methods of LazyIterator.

 

public Iterator<S> iterator() {
    return new Iterator<S>() {
        public boolean hasNext() {
            return lookupIterator.hasNext();
        }
        public S next() {
            return lookupIterator.next();
        }
        .......
    };
}

So, we focus on the lookupIterator.hasNext() method, which will eventually call hasNextService.

 

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() {
        //第二次调用的时候,已经解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
            //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
            String fullName = PREFIX + service.getName();
            //将文件路径转成URL对象
            configs = loader.getResources(fullName);
        }
        while ((pending == null) || !pending.hasNext()) {
            //解析URL文件对象,读取内容,最后返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一个实现类的类名
        nextName = pending.next();
        return true;
    }
}

4. Create an instance

Of course, when the next method is called, what is actually called is lookupIterator.nextService. It creates an instance of the implementation class and returns it through reflection.

 

private class LazyIterator implements Iterator<S>{
    private S nextService() {
        //全限定类名
        String cn = nextName;
        nextName = null;
        //创建类的Class对象
        Class<?> c = Class.forName(cn, false, loader);
        //通过newInstance实例化
        S p = service.cast(c.newInstance());
        //放入集合,返回实例
        providers.put(cn, p);
        return p; 
    }
}

Seeing this, I think it is very clear. After obtaining the instance of the class, we can naturally do whatever we want with it!

3. Application in JDBC

We said at the beginning that the SPI mechanism provides the possibility for the expansion of many frameworks. In fact, JDBC is applied to this mechanism. Recall the process of JDBC obtaining a database connection. In earlier versions, you need to set up the connection of the database driver first, and then get a Connection through DriverManager.getConnection.

 

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);

In the newer version (the specific version, the author has not verified), this step is no longer necessary to set the database driver connection, so how does it distinguish which database it is? The answer lies in SPI.

1. Load

Let's turn our attention back to DriverManagerthe class, which does a more important thing in the static code block. Obviously, it has initialized the database driver connection through the SPI mechanism.

 

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

The specific process depends on loadInitialDrivers, which looks for the service class of the Driver interface, so its file path is: META-INF/services/java.sql.Driver.

 

public class DriverManager {
    private static void loadInitialDrivers() {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //很明显,它要加载Driver接口的服务类,Driver接口的包为:java.sql.Driver
                //所以它要找的就是META-INF/services/java.sql.Driver文件
                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;
            }
        });
    }
}

So, where is this file? Let's look at the jar package of MySQL, which is this file, and the content of the file is: com.mysql.cj.jdbc.Driver.

MySQL SPI file

 

2. Create an instance

The previous step has found the fully qualified class name of com.mysql.cj.jdbc.Driver in MySQL. When the next method is called, an instance of this class will be created. It completes one thing, registers its own instance with DriverManager.

 

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            //注册
            //调用DriverManager类的注册方法
            //往registeredDrivers集合中加入实例
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

3. Create Connection

The DriverManager.getConnection() method is where the connection is created. It loops through the registered database driver, calls its connect method, gets the connection and returns.

 

private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {   
    //registeredDrivers中就包含com.mysql.cj.jdbc.Driver实例
    for(DriverInfo aDriver : registeredDrivers) {
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                //调用connect方法创建连接
                Connection 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());
        }
    }
}

4. Further expansion

Now that we know that JDBC creates database connections in this way, can we expand it? If we also create a java.sql.Driver file and customize the implementation class MyDriver, then we can dynamically modify some information before and after obtaining the connection.

Or create a file under the project ClassPath first, and the content of the file is a custom driver classcom.viewscenes.netsupervisor.spi.MyDriver

custom database driver

Our MyDriver implementation class inherits from NonRegisteringDriver in MySQL and also implements the java.sql.Driver interface. In this way, when the connect method is called, this class will be called, but the actual creation process is still completed by MySQL.

 

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{
    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public MyDriver()throws SQLException {}
    
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("准备创建数据库连接.url:"+url);
        System.out.println("JDBC配置信息:"+info);
        info.setProperty("user", "root");
        Connection connection =  super.connect(url, info);
        System.out.println("数据库连接创建完成!"+connection.toString());
        return connection;
    }
}
--------------------输出结果---------------------
准备创建数据库连接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

 

Guess you like

Origin blog.csdn.net/qq_35240226/article/details/108627158