SPI(service provider interface)机制是JDK内置的一种服务发现机制,可以动态的发现服务
jdk 的 spi 机制是通过 ServiceLoader 类来加载扩展的服务提供类
为什么需要这种机制呢?
大家可以从类加载器文章中双亲委派的破坏了解原因
双亲委派模型破坏
简单的说,类加载器中的双亲委派模型的工作原理是对于类的加载,先交给父类加载器完成,如果父类加载器无法完成加载,子类加载器才进行加载。JDK中的一些基础类接口,例如 JDBC,如果按照双亲委派模型,JDBC只能用启动类加载器完成驱动的加载,但是驱动的实现是在用户目录的 jar 包实现,所以启动类加载器就无法加载用户的目录的驱动类了(下面可以从代码实例了解一下)
ServiceLoader 的源码中的介绍如下
A service is a well-known set of interfaces and (usually abstract) classes.
A service provider is a specific implementation of a service. The classes
in a provider typically implement the interfaces and subclass the classes
defined in the service itself. Service providers can be installed in an
implementation of the Java platform in the form of extensions, that is,
jar files placed into any of the usual extension directories. Providers
can also be made available by adding them to the application's class
path or by some other platform-specific means.
大体上的意思是
服务是一组众所周知的接口和类(通常是抽象的)。
服务提供者是一个服务的特定实现。提供者中的类通常是实现服务接口
和子类的具体实现。服务提供者可以以扩展的形式在 Java 平台中实现,
也就是放在任何常用jar文件扩展目录中。提供者也可以通过将它们添加
到应用程序的 classpath 或其他特定于平台的方法。
ServiceLoader 的作用是通过接口或抽象类,找到 class path 的 META-INF/services/ 路径下对应的文件,把文件中描述全路径的实现类(服务提供者)加载起来
SpiInterface接口
package spi;
public interface SpiInterface {
String getInfo();
}
文件路径,文件命名是接口的路径
spi.SpiInterface文件内容为两个接口的实现类
spi.SpiInfo
spi.SpiMessage
SpiInfo 和 SpiMessage实现类
package spi;
public class SpiInfo implements SpiInterface {
@Override
public String getInfo() {
return "SpiInfo";
}
}
package spi;
public class SpiMessage implements SpiInterface {
@Override
public String getInfo() {
return "SpiMessage";
}
}
再来看看实际使用的输出结果
package spi;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
ServiceLoader<SpiInterface> loader = ServiceLoader.load(SpiInterface.class);
for (SpiInterface spi : loader) {
System.out.println(spi.getInfo());
}
// 输出:
// SpiInfo
// SpiMessage
}
}
通过 JDK 提供的 SPI 机制,就可以通过 SpiInterface 接口和实现类文件的描述,把具体的实现类加载了
这种 SPI 机制又与类加载器的双亲委派模型破坏有什么关系的(大家可以想一下)
SPi 机制对于双亲委派模型的破坏在于增加了线程上下文类加载器,它的作用是可以逆向加载,例如需要用启动类加载器目录下的类,可以通过线程上下文类加载器来加载
假如没有线程上下文类加载器,加载的方式如下
package spi;
public class Main {
public static void main(String[] args) throws Exception {
// 假如 SpiInterface 在 rt.jar 中,那么类加载器是启动类加载器,
// 启动类加载器是无法加载用户路径("spi.SpiInfo")的类的
Class<SpiInterface> spiInfo = (Class<SpiInterface>)
SpiInterface.class.getClassLoader().loadClass("spi.SpiInfo");
System.out.println(spiInfo.newInstance().getInfo());
}
}
但自从有了线程上下文类加载器,加载方式可以如下
package spi;
public class Main {
public static void main(String[] args) throws Exception {
Class<SpiInterface> spiInfo = (Class<SpiInterface>)
Thread.currentThread().getContextClassLoader().loadClass("spi.SpiInfo");
System.out.println(spiInfo.newInstance().getInfo());
// 无论 SpiInterface 在哪个路径,线程上下文类加载器都可以加载类
// 完美输出:SpiInfo
}
}
这样大家是不是对于 JDK 的 SPi 机制很熟悉了呢,如果对于类加载过程与类加载器还不熟悉的,建议看看上面链接的文章(重点在于类加载,类加载器),肯定大有收获
介绍完了 JDK 的 SPi 机制,我们还可以在 Sping 中发现这种机制的应用,Spring 的 SPi 机制可以把 spring.factories 文件中的类实例化,注册到 Spring 容器中,该机制在 Sping 是这样应用的:
服务:接口或抽象类
服务提供者:实现服务接口的实现类
服务与服务提供者的文件描述:在 class path 的 META-INF/spring.factories 文件中,该文件可以在任意 jar 包存在,所以不同的 jar 包不会存在冲突,spring 启动的时候会扫描所有 jar 包的 spring.factories 文件
加载类:SpringFactoriesLoader(相当于 JDK 的 ServiceLoader)
从 SpringFactories 的源码可以看出
public abstract class SpringFactoriesLoader {
/**
* The location to look for factories.
* Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
/**
* @param factoryClass 工厂类接口或抽象类
* @param classLoader 类加载器
* @return 实现 factoryClass 的所有实例
*/
public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
}
List<T> result = new ArrayList<T>(factoryNames.size());
for (String factoryName : factoryNames) {
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
}
其中 spring.factories 是这样定义的,实现类通过逗号分隔
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration
org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration
有兴趣的还可以了解 dubbo 的 spi 机制,来自 Dubbo 官方文档
Dubbo SPI 实现了一套功能更强的 SPI 机制