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整体机制图
当服务的提供者提供了一种接口的实现之后,需要在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-log4j12/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
/META-INF/services/org.slf4j.spi.SLF4JServiceProvider 文件
org.slf4j.log4j12.Log4j12ServiceProvider
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