SPI机制
Java SPI(Service Provider Interface)是JDK提供的一种服务发现机制,用于在运行时动态加载和扩展应用程序中的服务提供者。
SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
【举例】 在Java中定义好接口java.sql.Driver,然后直接使用,不用管实现类。具体的实现类,是通过SPI机制来加载的。驱动有JDBC、ODBC等等,我们导入了哪一个驱动Jar包,这个Jar包的META-INF/services目录中都会有java.sql.Driver文件,里面存的是当前这个Jar包里面实现接口java.sql.Driver的实现类的全限定名称。
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
SPI机制的应用
JDBC中加载驱动
1、JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
2、mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
3、postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
4、源码实现
关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
可以看到是加载实例化驱动的,接着看loadInitialDrivers
方法:
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); } } }
上面的代码主要步骤是:
1、从系统变量中获取有关驱动的定义。
2、使用SPI来获取驱动的实现。
3、遍历使用SPI获取到的具体实现,实例化各个实现类。
4、根据第一步获取到的驱动列表来实例化具体实现类。
主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这里没有去META-INF/services
目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:
//获取迭代器 Iterator<Driver> driversIterator = loadedDrivers.iterator(); //遍历所有的驱动实现 while(driversIterator.hasNext()) { driversIterator.next(); }
在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。
然后是调用driversIterator.next();方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。
Spring SPI
在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
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; }
下面是一段Spring Boot
中 spring.factories
的配置
# Logging Systems org.springframework.boot.logging.LoggingSystemFactory=\ org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\ org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\ org.springframework.boot.logging.java.JavaLoggingSystem.Factory # PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader # ConfigData Location Resolvers org.springframework.boot.context.config.ConfigDataLocationResolver=\ org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\ org.springframework.boot.context.config.StandardConfigDataLocationResolver
Spring SPI中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩展配置,是用一个文件好,还是每个单独一个文件好这个,这个问题就见仁见智了。
Spring的SPI虽然属于spring-framework(core),但是目前主要用在spring boot中……
Spring SPI也是支持ClassPath中存在多个spring.factories文件的,加载时会按照classpath的顺序依次加载这些spring.factories文件,添加到一个ArrayList中。由于没有别名,所以也没有去重的概念,有多少就添加多少。
但由于Spring的SPI主要用在 Spring Boot 中,而Spring Boot中的ClassLoader会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个。
如果我们要扩展某个接口的话,只需要在你的项目里新建一个META-INF/spring.factories文件,只添加你要的那个配置。
Dubbo SPI
Dubbo就是通过SPI机制加载所有的组件。不过,Dubbo并未使用 Java 原生的SPI机制,而是对其进行了增强,使其能够更好的满足需求。在Dubbo中,SPI是一个非常重要的模块。基于SPI,我们可以很容易的对Dubbo进行拓展。
Dubbo中实现了一套新的SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了ExtensionLoader 类中,通过ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI所需的配置文件需放置在META-INF/dubbo路径下,配置内容如下(以下demo来自dubbo官方文档):
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee
与Java SPI
实现类配置不同,Dubbo SPI
是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI
注解。下面来演示Dubbo SPI
的用法:
@SPI public interface Robot { void sayHello(); } public class OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } } public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } }
Dubbo SPI和JDK SPI最大的区别就在于支持“别名”,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取Robot多个SPI实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!
通过@SPI注解的value属性,还可以默认一个“别名”的实现。比如在Dubbo中,默认的是Dubbo私有协议:dubbo protocol - dubbo://
来看看Dubbo中协议的接口:
@SPI("dubbo") public interface Protocol { ...... }
在Protocol
接口上,增加了一个@SPI
注解,而注解的value
值为Dubbo
,通过SPI
获取实现时就会获取 Protocol SPI
配置中别名为dubbo
的那个实现,com.alibaba.dubbo.rpc.Protocol
文件如下:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper mock=com.alibaba.dubbo.rpc.support.MockProtocol dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol com.alibaba.dubbo.rpc.protocol.http.HttpProtocol com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol registry=com.alibaba.dubbo.registry.integration.RegistryProtocol qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
然后只需要通过getDefaultExtension,就可以获取到@SPI注解上value对应的那个扩展实现了。
SPI深入理解
API与SPI的区别
API(Application Programming Interface)和SPI(Service Provider Interface)是两种不同的概念,API是软件组件之间的接口规范,用于定义交互方式和通信协议,以便于开发者使用和集成组件。而SPI是一种服务发现机制,用于动态加载和扩展应用程序中的服务提供者,允许通过插件式的方式添加和替换功能实现。API是软件开发中常见的概念,而SPI则是特定于服务发现和扩展的机制。
API(应用程序编程接口):
API是一组定义了软件组件之间交互方式和通信协议的接口。
API提供了一系列的函数、方法、类、协议等,用于让开发者能够与某个软件库、框架或平台进行交互。
API定义了外部组件与提供者之间的约定和规范,以便于开发者可以使用和集成这些组件来实现特定的功能。
API通常由供应商或平台提供,并且在软件开发中广泛使用,以简化开发者的工作,提供特定功能和服务的访问途径。
SPI(服务提供者接口):
SPI是一种服务发现机制,用于在运行时动态加载和扩展应用程序中的服务提供者。
SPI允许开发者定义服务接口,然后通过服务提供者实现该接口,并在运行时通过SPI机制动态发现和加载实现。
SPI通过在类路径下的META-INF/services目录中的配置文件中指定实现类的方式,使得应用程序可以通过插件式的方式添加、替换和扩展功能。
SPI提供了一种松耦合的方式,允许应用程序在不修改源代码的情况下,通过添加新的服务提供者实现来扩展功能。
ServiceLoader
//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() + "]"; } }
首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。
其次,LazyIterator中的hasNext方法,静态变量PREFIX就是META-INF/services/目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。
最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象。
所以我们可以看到ServiceLoader不是实例化以后,就去读取配置文件中的具体实现,并进行实例化。而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析,调用next方法的时候进行实例化并缓存。所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用reload方法。
JDK SPI、Spring SPI、Dubbo SPI综合对比