[Java - Detailed explanation of SPI mechanism]

SPI (Service Provider Interface) is a service provider discovery mechanism built into the JDK. It can be used to enable framework extensions and replacement components. It is mainly used by framework developers.

What is SPI mechanism

SPI (Service Provider Interface) is a service provider discovery mechanism built into the JDK. It can be used to enable framework extensions and replacement components. It is mainly used by framework developers, such as the java.sql.Driver interface. Different manufacturers can make different implementations for the same interface. MySQL and PostgreSQL have different implementations provided to users, and Java's SPI mechanism can find service implementations for a certain interface. The main idea of ​​the SPI mechanism in Java is to move the control of assembly outside the program. This mechanism is especially important in modular design. Its core idea is decoupling.

When the service provider provides an interface implementation, a file named after the service interface needs to be created in the META-INF/services/ directory under the classpath. The content of this file is The specific implementation class of this interface. When other programs need this service, they can search for the configuration file in META-INF/services/ of this jar package (which usually relies on the jar package). The configuration file contains the specific implementation of the interface. Class name, you can load and instantiate it based on this class name, and you can use the service. The tool class for finding service implementations in the JDK is:java.util.ServiceLoader.

Simple example of SPI mechanism

We now need to use a content search interface. The search implementation may be file system-based search or database-based search.

  • Define the interface first
public interface Search {
    public List<String> searchDoc(String keyword);   
}
  • File search implementation
public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 "+keyword);
        return null;
    }
}
  • Database search implementation
public class DatabaseSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据搜索 "+keyword);
        return null;
    }
}
  • resources Next, you can create a new META-INF/services/ directory under resources, and then create a file with the fully qualified name of the interface:com.cainiao.ys.spi.learn.Search, and add the implementation class we need to use.
com.cainiao.ys.spi.learn.FileSearch
  • Test Methods
public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}

You can see the output results: file search hello world

If two implementation classes are written in the com.cainiao.ys.spi.learn.Search file, the final output will be two lines.

This is becauseServiceLoader.load(Search.class) will go to META-INF/services to find the fully qualified name file of the interface when loading an interface, and then load the corresponding file based on the content inside. implementation class.

This is the idea of ​​spi. The interface is implemented by the provider. The provider is only used in the submitted jar packageMETA-INF/services to create a new file according to the interface defined by the platform and add it to the corresponding The implementation content is just fine.

 Wide application of SPI mechanism

 SPI mechanism - JDBC DriverManager

Before JDBC4.0, when we developed a database connection, we usually used Class.forName("com.mysql.jdbc.Driver") to load the database-related driver first, and then obtain it. Connection operations etc. After JDBC4.0, you don’t need to use Class.forName("com.mysql.jdbc.Driver") to load the driver. You can just get the connection directly. This method is used now. Java's SPI extension mechanism is implemented.

 JDBC interface definition

Firstly, the interface is defined in javajava.sql.Driver, and there is no specific implementation. The specific implementations are provided by different manufacturers.

 mysql implementation

In the mysql jar packagemysql-connector-java-6.0.6.jar, you can find the META-INF/services directory. There will be a name in this directory named java.sql.Driver file, the content of the file is com.mysql.cj.jdbc.Driver, and the content here is the implementation of the interface defined in Java.

 postgresql implementation

You can also find the same configuration file in the postgresql jar packagepostgresql-42.0.0.jar. The content of the file isorg.postgresql.Driver. This is postgresql's for Java. Implementation of /span>java.sql.Driver.

 Instructions

As mentioned above, SPI extension is now used to load specific drivers. When we write code to connect to the database in Java, we no longer need to use Class.forName("com.mysql.jdbc.Driver") to load the driver. Just use the following code directly:

String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
.....

The use of spi is not involved here, and then look at the analysis below.

 Source code implementation

The above usage method is our ordinary code for connecting to the database, and does not involve SPI, but one thing we can be sure of is that we have not written hard coding for specific drivers< a i=1>! Class.forName("com.mysql.jdbc.Driver")

The above code can directly obtain the database connection for operation, but what does it have to do with SPI? The above code does not have the code to load the driver. How do we determine which database connection driver to use? This involves using Java's SPI extension mechanism to find related driver information. The search for drivers is actually in DriverManager, and DriverManager is the implementation in Java. , used to obtain the database connection, there is a static code block in DriverManager as follows:

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

You can see that the instantiated driver is loaded, and then look at the loadInitialDrivers method:

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

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
			//使用SPI的ServiceLoader来加载接口的实现
            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);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

The main steps of the above code are:

  • Get the driver definition from system variables.
  • Use SPI to obtain the driver implementation.
  • Traverse the specific implementation obtained using SPI and instantiate each implementation class.
  • Instantiate the specific implementation class based on the driver list obtained in the first step.

We mainly focus on steps 2 and 3. These two steps are the usage of SPI. Let’s first look at the second step, using SPI to obtain the driver implementation. The corresponding code is:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

Here we did not go to the META-INF/services directory to find the configuration file, nor did we load the specific implementation class. What we did was to encapsulate our interface type and class loader, and initialize an iteration device.

Next, look at the third step, traverse the specific implementation obtained using SPI, and instantiate each implementation class. The corresponding code is as follows:

//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
    driversIterator.next();
}

When traversing, first call thedriversIterator.hasNext() method, which will search all META-INF/services directories in the classpath and jar package java.sql.Driver file, and find the name of the implementation class in the file. At this time, no specific implementation class has been instantiated (the specific source code of ServiceLoader is implemented below).

Then call thedriversIterator.next(); method. At this time, each implementation class will be instantiated according to the driver name. The driver is now found and instantiated.

You can see the screenshot below. I added two jar packages to the test project, mysql-connector-java-6.0.6.jar and postgresql-42.0.0.0.jar, after tracking them in DriverManager: and a>

You can see that there are two drivers in the iterator at this time, both mysql and postgresql are loaded.

 SPI mechanism - Common-Logging

common-logging (also called Jakarta Commons Logging, abbreviated as JCL) is a commonly used log library facade. Let's see how it is decoupled.

First, the log instance is created through the getLog(String) method of LogFactory:

public static getLog(Class clazz) throws LogConfigurationException {
    return getFactory().getInstance(clazz);
}

LogFatory is an abstract class, which is responsible for loading the specific log implementation and analyzing its Factory getFactory() method:

public static org.apache.commons.logging.LogFactory getFactory() throws LogConfigurationException {
    // Identify the class loader we will be using
    ClassLoader contextClassLoader = getContextClassLoaderInternal();

    if (contextClassLoader == null) {
        // This is an odd enough situation to report about. This
        // output will be a nuisance on JDK1.1, as the system
        // classloader is null in that environment.
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Context classloader is null.");
        }
    }

    // Return any previously registered factory for this class loader
    org.apache.commons.logging.LogFactory factory = getCachedFactory(contextClassLoader);
    if (factory != null) {
        return factory;
    }

    if (isDiagnosticsEnabled()) {
        logDiagnostic(
                "[LOOKUP] LogFactory implementation requested for the first time for context classloader " +
                        objectId(contextClassLoader));
        logHierarchy("[LOOKUP] ", contextClassLoader);
    }

    // Load properties file.
    //
    // If the properties file exists, then its contents are used as
    // "attributes" on the LogFactory implementation class. One particular
    // property may also control which LogFactory concrete subclass is
    // used, but only if other discovery mechanisms fail..
    //
    // As the properties file (if it exists) will be used one way or
    // another in the end we may as well look for it first.
    // classpath根目录下寻找commons-logging.properties
    Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);

    // Determine whether we will be using the thread context class loader to
    // load logging classes or not by checking the loaded properties file (if any).
    // classpath根目录下commons-logging.properties是否配置use_tccl
    ClassLoader baseClassLoader = contextClassLoader;
    if (props != null) {
        String useTCCLStr = props.getProperty(TCCL_KEY);
        if (useTCCLStr != null) {
            // The Boolean.valueOf(useTCCLStr).booleanValue() formulation
            // is required for Java 1.2 compatibility.
            if (Boolean.valueOf(useTCCLStr).booleanValue() == false) {
                // Don't use current context classloader when locating any
                // LogFactory or Log classes, just use the class that loaded
                // this abstract class. When this class is deployed in a shared
                // classpath of a container, it means webapps cannot deploy their
                // own logging implementations. It also means that it is up to the
                // implementation whether to load library-specific config files
                // from the TCCL or not.
                baseClassLoader = thisClassLoader;
            }
        }
    }

    // 这里真正开始决定使用哪个factory
    // 首先,尝试查找vm系统属性org.apache.commons.logging.LogFactory,其是否指定factory
    // Determine which concrete LogFactory subclass to use.
    // First, try a global system property
    if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
                "] to define the LogFactory subclass to use...");
    }

    try {
        String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
        if (factoryClass != null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
                        "' as specified by system property " + FACTORY_PROPERTY);
            }
            factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
        } else {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
            }
        }
    } catch (SecurityException e) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] A security exception occurred while trying to create an" +
                    " instance of the custom factory class" + ": [" + trim(e.getMessage()) +
                    "]. Trying alternative implementations...");
        }
        // ignore
    } catch (RuntimeException e) {
        // This is not consistent with the behaviour when a bad LogFactory class is
        // specified in a services file.
        //
        // One possible exception that can occur here is a ClassCastException when
        // the specified class wasn't castable to this LogFactory type.
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] An exception occurred while trying to create an" +
                    " instance of the custom factory class" + ": [" +
                    trim(e.getMessage()) +
                    "] as specified by a system property.");
        }
        throw e;
    }

    // 第二,尝试使用java spi服务发现机制,载META-INF/services下寻找org.apache.commons.logging.LogFactory实现
    // Second, try to find a service by using the JDK1.3 class
    // discovery mechanism, which involves putting a file with the name
    // of an interface class in the META-INF/services directory, where the
    // contents of the file is a single line specifying a concrete class
    // that implements the desired interface.

    if (factory == null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
                    "] to define the LogFactory subclass to use...");
        }
        try {
            // META-INF/services/org.apache.commons.logging.LogFactory, SERVICE_ID
            final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);

            if (is != null) {
                // This code is needed by EBCDIC and other strange systems.
                // It's a fix for bugs reported in xerces
                BufferedReader rd;
                try {
                    rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                } catch (java.io.UnsupportedEncodingException e) {
                    rd = new BufferedReader(new InputStreamReader(is));
                }

                String factoryClassName = rd.readLine();
                rd.close();

                if (factoryClassName != null && !"".equals(factoryClassName)) {
                    if (isDiagnosticsEnabled()) {
                        logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " +
                                factoryClassName +
                                " as specified by file '" + SERVICE_ID +
                                "' which was present in the path of the context classloader.");
                    }
                    factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
                }
            } else {
                // is == null
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
                }
            }
        } catch (Exception ex) {
            // note: if the specified LogFactory class wasn't compatible with LogFactory
            // for some reason, a ClassCastException will be caught here, and attempts will
            // continue to find a compatible class.
            if (isDiagnosticsEnabled()) {
                logDiagnostic(
                        "[LOOKUP] A security exception occurred while trying to create an" +
                                " instance of the custom factory class" +
                                ": [" + trim(ex.getMessage()) +
                                "]. Trying alternative implementations...");
            }
            // ignore
        }
    }

    // 第三,尝试从classpath根目录下的commons-logging.properties中查找org.apache.commons.logging.LogFactory属性指定的factory
    // Third try looking into the properties file read earlier (if found)

    if (factory == null) {
        if (props != null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic(
                        "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
                                "' to define the LogFactory subclass to use...");
            }
            String factoryClass = props.getProperty(FACTORY_PROPERTY);
            if (factoryClass != null) {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic(
                            "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
                }
                factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

                // TODO: think about whether we need to handle exceptions from newFactory
            } else {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
                }
            }
        } else {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
            }
        }
    }

    // 最后,使用后备factory实现,org.apache.commons.logging.impl.LogFactoryImpl
    // Fourth, try the fallback implementation class

    if (factory == null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic(
                    "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
                            "' via the same classloader that loaded this LogFactory" +
                            " class (ie not looking in the context classloader).");
        }

        // Note: unlike the above code which can try to load custom LogFactory
        // implementations via the TCCL, we don't try to load the default LogFactory
        // implementation via the context classloader because:
        // * that can cause problems (see comments in newFactory method)
        // * no-one should be customising the code of the default class
        // Yes, we do give up the ability for the child to ship a newer
        // version of the LogFactoryImpl class and have it used dynamically
        // by an old LogFactory class in the parent, but that isn't
        // necessarily a good idea anyway.
        factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
    }

    if (factory != null) {
        /**
            * Always cache using context class loader.
            */
        cacheFactory(contextClassLoader, factory);

        if (props != null) {
            Enumeration names = props.propertyNames();
            while (names.hasMoreElements()) {
                String name = (String) names.nextElement();
                String value = props.getProperty(name);
                factory.setAttribute(name, value);
            }
        }
    }

    return factory;
}

It can be seen that the steps for loading the specific implementation of the abstract class LogFactory are as follows:

  • From vm system property org.apache.commons.logging.LogFactory
  • Use the SPI service discovery mechanism to discover the implementation of org.apache.commons.logging.LogFactory
  • Check whether the org.apache.commons.logging.LogFactory property of commons-logging.properties in the classpath root directory specifies factory implementation.
  • Use the default factory implementation, org.apache.commons.logging.impl.LogFactoryImpl

The return type of LogFactory's getLog() method is the org.apache.commons.logging.Log interface, which provides methods from trace to fatal. It can be determined that if the log implementation provider only needs to implement this interface and create Log using a subclass inherited from org.apache.commons.logging.LogFactory, a loosely coupled logging system can be built.

 SPI mechanism - plug-in system

In fact, the most SPI thinking should belong to plug-in development. This kind of thinking is also used in our project. I will talk about it later. Here I will talk about the plug-in thinking of Eclipse in detail.

Eclipse uses OSGi as the basis of the plug-in system to dynamically add new plug-ins and stop existing plug-ins to manage component life cycles in a dynamic manner.

Generally speaking, the file structure of the plug-in must contain the following three files in the specified directory:

  • META-INF/MANIFEST.MF: Basic project configuration information, version, name, launcher, etc.
  • build.properties: Compilation configuration information of the project, including source code path and output path
  • plugin.xml: The operation configuration information of the plug-in, including the pop-up menu and the corresponding operation execution class after clicking the menu, etc.

When eclipse starts, it will traverse the directories in the plugins folder, scan the manifest file of each plug-inMANIFEST.MF, and build an internal model to record each plug-in it finds information, it is possible to dynamically add new plug-ins.

This also means that eclipse has formulated a series of rules, such as file structure, types, parameters, etc. Plug-in developers follow these rules to develop their own plug-ins. Eclipse does not need to know how the plug-in is developed. It only needs to parse and load the configuration file into the system at startup. This is a manifestation of SPI thinking. .

 SPI mechanism - SPI mechanism in Spring

In the automatic assembly process of springboot, the META-INF/spring.factories file will eventually be loaded, and the loading process is loaded by SpringFactoriesLoader. Search allMETA-INF/spring.factories configuration files from each Jar package under CLASSPATH, then parse the properties file and return after finding the configuration with the specified name. It should be noted that in fact, it will not only search under the ClassPath path, but also scan the Jar packages under all paths, but this file will only be in the jar package under the Classpath.

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得资源文件的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 遍历所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 组装数据,并返回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}

 In-depth understanding of SPI mechanism

Next, let’s have an in-depth understanding of SPI related content.

 How is the SPI mechanism usually used?

After reading the analysis of several examples above, you should all know the general process:

  • Relevant organizations or companies define standards.
  • Implemented by specific manufacturers or framework developers.
  • Used by programmers.
 Define standards

Defining standards means defining interfaces. Such as interfacejava.sql.Driver

 Implemented by specific manufacturers or framework developers

Vendors or framework developers develop specific implementations:

Define a file with the fully qualified name of the interface in the META-INF/services directory, such as the java.sql.Driver file. The content of the file is the specific implementation name, such as < /span>me.cxis.sql.MyDriver.

Write specific implementationsme.cxis.sql.MyDriver, which are all implementations of the interface Driver.

 Used by programmers

We will quote the specific manufacturer's jar package to implement our functions:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历
while(driversIterator.hasNext()) {
    driversIterator.next();
    //可以做具体的业务逻辑
}

What is the difference between SPI and API

There are actually two questions here. The first is the difference between SPI and API? Secondly, when to use API and when to use SPI?

SPI - the "interface" is in the same "package" as the "caller"

  • Conceptually more dependent on the caller.
  • Organizationally located in the caller's package.
  • The implementation is in a separate package.
  • Common examples are: plug-in mode plug-ins.

API - the "interface" is in the same "package" as the "implementer"

  • Conceptually closer to the implementation side.
  • Organizationally located in the same package as the implementer.
  • Implementation and interface in one package.

 SPI mechanism implementation principle

//ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S>
    implements Iterable<S>
{

    //查找配置文件的目录
    private static final String PREFIX = "META-INF/services/";

    //表示要被加载的服务的类或接口
    private final Class<S> service;

    //这个ClassLoader用来定位,加载,实例化服务提供者
    private final ClassLoader loader;

    // 访问控制上下文
    private final AccessControlContext acc;

    // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 迭代器
    private LazyIterator lookupIterator;


    //重新加载,就相当于重新创建ServiceLoader了,用于新的服务提供者安装到正在运行的Java虚拟机中的情况。
    public void reload() {
        //清空缓存中所有已实例化的服务提供者
        providers.clear();
        //新建一个迭代器,该迭代器会从头查找和实例化服务提供者
        lookupIterator = new LazyIterator(service, loader);
    }

    //私有构造器
    //使用指定的类加载器和服务创建服务加载器
    //如果没有指定类加载器,使用系统类加载器,就是应用类加载器。
    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();
    }

    //解析失败处理的方法
    private static void fail(Class<?> service, String msg, Throwable cause)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                            cause);
    }

    private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }

    //解析服务提供者配置文件中的一行
    //首先去掉注释校验,然后保存
    //返回下一行行号
    //重复的配置项和已经被实例化的配置项不会被保存
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        //读取一行
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        //#号代表注释行
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    //解析配置文件,解析指定的url配置文件
    //使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        }
        return names.iterator();
    }

    //服务提供者查找的迭代器
    private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;//服务提供者接口
        ClassLoader loader;//类加载器
        Enumeration<URL> configs = null;//保存实现类的url
        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 {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                }
            }
            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);
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            }
        }

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

    }

    //获取迭代器
    //返回遍历服务提供者的迭代器
    //以懒加载的方式加载可用的服务提供者
    //懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
    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();
            }

        };
    }

    //为指定的服务使用指定的类加载器来创建一个ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    //使用线程上下文的类加载器来创建ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    //使用扩展类加载器为指定的服务创建ServiceLoader
    //只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }

    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }

}

First of all, ServiceLoader implements the Iterable interface, so it has the properties of iterators. Here they mainly implement iterators is lazy loading iterator. , methods of and methods. Here we mainly call the corresponding hasNext and nextlookupIteratorhasNextnextlookupIterator

Secondly, in the method in LazyIterator, the static variable PREFIX is directory, which is why you need to create a file named after the service interface in the directory under . hasNext”META-INF/services/”classpathMETA-INF/services/

Finally, load the class object through the reflection methodClass.forName(), instantiate the class using the newInstance method, and put The instantiated class is cached in the providers object, (LinkedHashMap<String,S> type) and then the instance object is returned.

So we can see thatServiceLoader does not read the specific implementation in the configuration file and instantiate it after instantiation. Instead, the corresponding configuration file will be loaded for parsing when an iterator is used to traverse it. When the hasNext method is called, the configuration file will be loaded for parsing. Calling next Instantiate and cache when using the method.

All configuration files will only be loaded once, and service providers will only be instantiated once. To reload configuration files, use the reload method.

 Defects of SPI mechanism

Through the above analysis, we can find the shortcomings of our use of the SPI mechanism:

  • It cannot be loaded on demand. All implementations need to be traversed and instantiated, and then the implementation we need can be found in the loop. If you don't want to use some implementation classes, or some classes are time-consuming to instantiate, they are also loaded and instantiated, which causes waste.

  • The method of obtaining an implementation class is not flexible enough. It can only be obtained in the form of Iterator, and the corresponding implementation class cannot be obtained based on a certain parameter.

  • It is unsafe for multiple concurrent threads to use an instance of the ServiceLoader class.


Guess you like

Origin blog.csdn.net/abclyq/article/details/134701915