Java SPI机制

 SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,主要作用是解耦,提高可扩展性。

(1) SPI是什么

 SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。

 比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL、PostgreSQL、Oracle都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

 Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。

SPI整体机制图

Java SPI流程

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
JDK中查找服务的实现的工具类是:java.util.ServiceLoader。  

SPI机制的简单示例

public interface Search {

    List<String> searchByKeyword(String keyword);

}
public class FileSearch implements Search {

    @Override
    public List<String> searchByKeyword(String keyword) {
        System.out.println("文件搜索 " + keyword);
        return null;
    }

}
public class DatabaseSearch implements Search {

    @Override
    public List<String> searchByKeyword(String keyword) {
        System.out.println("DatabaseSearch 搜索 " + keyword);
        return null;
    }

}

/META-INF/services/cn.wkq.java.spi.Search 文件,文件里填对应的实现类的类名

cn.wkq.java.spi.DatabaseSearch
import java.util.Iterator;
import java.util.ServiceLoader;

public class SpiTest {

    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.searchByKeyword("hello world"); // DatabaseSearch 搜索 hello world
        }
    }

}

 
 ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services目录下找接口的全限定名文件,再根据里面的内容加载相应的实现类。    

(2) SPI源码学习

SPI核心源码流程图

 TODO

源码学习

源码位置:java.util.ServiceLoader

入口 ServiceLoader.load()

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 根据 需要加载的类、类加载器 创建ServiceLoader
    return ServiceLoader.load(service, cl);
}

 

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    // 创建一个ServiceLoader
    return new ServiceLoader<>(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();
}

 

public void reload() {
    // 清空已经加载的类
    providers.clear();
    // 创建一个延迟加载迭代器,后面要用到
    lookupIterator = new LazyIterator(service, loader);
}

 这一步只是创建了一个 ServiceLoader 以及 延迟加载迭代器 lookupIterator

 这块有一个iterator方法,并且在iterator方法里实现了延迟加载

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

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

    };
}

 

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;

    // 一个缓存工厂 key是对应的类名 value是对应的类 
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 延迟迭代器 
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

}

 

// 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 {
                // 配置文件名称   META-INF/services/cn.wkq.java.spi.Search
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName); // 通过ClassLoader解析文件 
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // configs.nextElement() 是 文件的绝对路径
            // pending是从文件(META-INF/services/cn.wkq.java.spi.Search) 解析出来的数据集合  一行是对应一个要加载的类
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        // cn对应从文件里解析到的类名  例如:cn.wkq.java.spi.DatabaseSearch
        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());
            // 放到缓存里
            // key是对应的类名  value是对应的类
            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();
    }

}

 

SPI的优缺点

 
Java SPI机制的不足:

1、不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
2、获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
3、多个并发多线程使用 ServiceLoader 类的实例是不安全的。

(3) SPI机制的广泛应用

 SPI在 JDBC、SLF4J日志门面、Spring中都有应用  

(3.1) SPI机制-JDBC DriverManager中的SPI机制

(3.1.1) 加载驱动

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
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 the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            // 加载Driver
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 获取延迟加载迭代器
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            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);
        }
    }
}

(3.1.2) 使用驱动

@CallerSensitive
public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));
}
// 注册的JDBC驱动列表
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();


//  Worker method called by the public getConnection() methods.
private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    println("DriverManager.getConnection(\"" + url + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

    // 
    for(DriverInfo aDriver : registeredDrivers) {
        // 判断Driver定义的类是否存在
        // If the caller does not have permission to load the driver then
        // skip it.
        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());
        }

    }

    // if we got here nobody could connect.
    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
            // 加载driver类 看是否存在
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }

         result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

(3.2) SPI机制-Log

 slf4j可以做到在无需更改代码的情况下,自由切换日志框架。其实现是通过在slf4j-api和不同的日志框架之间,添加不同的适配器。

 slf4j 1.7.x及以前版本用的 StaticLoggerBinder
 sl4j 1.8.X及更高 版本用的 SLF4JServiceProvider

源码位置 org/slf4j/LoggerFactory.java
https://github.com/qos-ch/slf4j/blob/2.0/slf4j-api/src/main/java/org/slf4j/LoggerFactory.java

private static List<SLF4JServiceProvider> findServiceProviders() {
    ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
    List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
    for (SLF4JServiceProvider provider : serviceLoader) {
        providerList.add(provider);
    }
    return providerList;
}

 https://github.com/qos-ch/slf4j/blob/2.0/slf4j-api/src/main/java/org/slf4j/spi/SLF4JServiceProvider.java

 https://github.com/qos-ch/slf4j/blob/2.0/slf4j-log4j12/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
/META-INF/services/org.slf4j.spi.SLF4JServiceProvider 文件

org.slf4j.log4j12.Log4j12ServiceProvider

 https://github.com/qos-ch/slf4j/blob/2.0/slf4j-log4j12/src/main/java/org/slf4j/log4j12/Log4j12ServiceProvider.java

public class Log4j12ServiceProvider implements SLF4JServiceProvider {

    /**
     * Declare the version of the SLF4J API this implementation is compiled against. 
     * The value of this field is modified with each major release. 
     */
    // to avoid constant folding by the compiler, this field must *not* be final
    public static String REQUESTED_API_VERSION = "1.8.99"; // !final

    private ILoggerFactory loggerFactory; 
    private IMarkerFactory markerFactory; 
    private MDCAdapter mdcAdapter;
    
    public Log4j12ServiceProvider() {
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }

    @Override
    public void initialize() {
        loggerFactory = new Log4jLoggerFactory();
        markerFactory = new BasicMarkerFactory();
        mdcAdapter = new Log4jMDCAdapter();
    }
    
 
}

 

(3.3) Spring中SPI机制

在Spring boot中也有一种类似的加载机制,在启动过程中通过 SpringFactoriesLoader加载 META-INFO/spring.factories文件中配置的接口实现类,然后在程序中读取这些配置文件并实例化。
 这种自定义的SPI机制就是SpringBoot Starter实现的基础。

在SpringFactoriesLoader类中定义了两个对外的方法:
loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表
loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:

源码位置: org.springframework.core.io.support.SpringFactoriesLoader

/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

/**
* 使用给定的类加载器从 META-INF/spring.factories 加载给定类型的工厂实现的完全限定类名。
* 
* spring.factories文件的格式为:key=value1,value2,value3
* 从所有的jar包中找到META-INF/spring.factories文件
* 然后从文件中解析出key=factoryClass类名称的所有value值
*
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

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

    try {
        // 从 META-INF/spring.factories 文件获取所有的配置 
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        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 factoryTypeName = ((String) entry.getKey()).trim();
                for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryTypeName, factoryImplementationName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

遇到的问题

Exception in thread "main" java.util.ServiceConfigurationError: cn.wkq.java.spi.Search: Provider cn.wkq.java.spi.Search could not be instantiated
	at java.util.ServiceLoader.fail(ServiceLoader.java:232)
	at java.util.ServiceLoader.access$100(ServiceLoader.java:185)
	at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:384)
	at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
	at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
	at cn.wkq.java.spi.SpiTest.main(SpiTest.java:12)
Caused by: java.lang.InstantiationException: cn.wkq.java.spi.Search
	at java.lang.Class.newInstance(Class.java:427)
	at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)
	... 3 more
Caused by: java.lang.NoSuchMethodException: cn.wkq.java.spi.Search.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 4 more

参考资料

[1] Java常用机制 - SPI机制详解
[2] spring boot 中的spring factories 机制
[3] slf4j与java SPI