一、SPI的概念
SPI的全程为Service Provider Interface,翻译为中文就是服务提供者接口。是Java提供的一种可以被第三方来实现或者扩展的接口,通过Java制定的SPI实现规范,可以很灵活地来实现框架中某些功能的扩展或者某些接口实现的替换。说简单点,就是给定一个接口,在用到接口具体实现的地方,可以不修改源代码的情况下,灵活替换接口的实现。SPI接口一般由框架的设计者提供规范,然后由框架的扩展者来扩展使用。二、SPI的使用场景
SPI在一般的系统中几乎不会用到,通常在一些具备扩展性的框架中使用比较多,例如:Dubbo,Spring这种框架中,在用户需要实现自己的特定功能时,不能修改框架源码,同时要把功能集成进去,这个时候就可以通过SPI扩展点来进行扩展。三、具体示例
3.1、Java规定的SPI实现方式
- 定义SPI接口,也就是一个普通的interface接口
- 定义SPI接口的具体实现类
- 在classpath下创建META-INF/services文件夹
- 在文件夹中创建一个不带后缀,而且以接口的全限定名为文件名的文本文件
- 在文件中指定接口的实现类的全限定名,如果有多个实现,每行一个
- 通过JDK中提供的ServiceLoader去加载具体实现,即可实现SPI功能
3.2、定义接口ServiceInitializer
/**
* Description: SPI接口
* @author wangbin33
* @date 2020/12/2 12:08
*/
public interface ServiceInitializer {
void init();
}
3.3、定义接口实现类
DownLoadInitializer实现类:
/**
* @author wangbin33
* @date 2020/12/2 12:09
*/
public class DownLoadInitializer implements ServiceInitializer {
@Override
public void init() {
System.out.println("下载功能初始化!");
}
}
UploadInitializer实现类:
/**
* @author wangbin33
* @date 2020/12/2 12:09
*/
public class UploadInitializer implements ServiceInitializer {
@Override
public void init() {
System.out.println("上传功能初始化!");
}
}
3.4、创建META-INF/services文件夹
在resources目录下创建META-INF/services文件夹,同时创建com.wb.spring.spi.ServiceInitializer文件
文件内容为:
com.wb.spring.spi.impl.DownLoadInitializer
com.wb.spring.spi.impl.UploadInitializer
3.5、测试类TestSpiMain
/**
* Description: 测试SPI接口
* @author wangbin33
* @date 2020/12/2 12:10
*/
public class TestSpiMain {
public static void main(String[] args) {
ServiceLoader initializers = ServiceLoader.load(ServiceInitializer.class);for (ServiceInitializer initializer : initializers) {
initializer.init();
}
}
}
输出结果:
下载功能初始化!
上传功能初始化!
3.6、Java的SPI实现原理
打开jdk的SPI实现部分源码,如下:
public final class ServiceLoader<S> implements Iterable<S> {
// 扫描目录前缀,这就是为什么一定需要创建这个文件夹,这里默认是写死的
private static final String PREFIX = "META-INF/services/";
// 将要被加载的类或者接口
private final Class service;// 用来定位,加载或者实例化接口实现实现者的类加载器private final ClassLoader loader;// 访问控制上下文对象private final AccessControlContext acc;// 按照实例化的顺序缓存已经实例化的类private LinkedHashMap providers = new LinkedHashMap<>();// 懒加载迭代器private LazyIterator lookupIterator;/**
* 清除老的接口实现类,重新加载新的服务接口实现类
*/public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}/**
* 构造函数私有化,外部只能通过静态方法进行实例化
*/private ServiceLoader(Class 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方法加载接口实现
reload();
}private class LazyIterator implements Iterator<S> {
Class service;
ClassLoader loader;
Enumeration configs = null;
Iterator pending = null;
String nextName = null;private LazyIterator(Class service, ClassLoader loader) {this.service = service;this.loader = loader;
}private boolean hasNextService() {if (nextName != null) {return true;
}if (configs == null) {try {// 定位具体的文件名,供下面类加载器去加载文件的内容// fullName: META-INF/services/com.wb.spring.spi.ServiceInitializer
String fullName = PREFIX + service.getName();if (loader == null)
configs = ClassLoader.getSystemResources(fullName);else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;
}// 解析出的SPI接口实现类的全限定名
pending = parse(service, configs.nextElement());
}
nextName = pending.next();return true;
}// 加载具体的SPI接口实现private S nextService() {if (!hasNextService())throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class> c = null;try {// 加载具体的实现类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}try {// 实例化
S p = service.cast(c.newInstance());// 保存到缓存中
providers.put(cn, p);return p;
} catch (Throwable x) {// 省略
}// This cannot happenthrow new Error();
}public boolean hasNext() {// 删除了AccessController相关代码return hasNextService();
}public S next() {// 删除了AccessController相关代码return nextService();
}public void remove() {throw new UnsupportedOperationException();
}
}/**
* 通过Class类型创建ServiceLoader对象,可以自己指定类加载器
*/public static ServiceLoader load(Class service,
ClassLoader loader) {return new ServiceLoader<>(service, loader);
}/**
* 通过Class类型创建ServiceLoader对象
*/public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
}
四、图解ServiceLoader的执行过程
五、SPI的具体使用场景
SpringMVC框架应该都比较熟悉,都知道它是对Servlet的封装,所以他可以运行在Servlet容器中,例如平时常用的Tomcat容器,那么Tomcat容器启动的时候是如何加载及初始化SpringMVC的相关组件呢?这里面就会用到SPI内容,可以在spring源码的spring-web模块中看到如下的一个SPI接口,javax.servlet.ServiceContainerInitializer:
这个接口是在哪如何加载的呢?其实是在Tomcat容器启动的时候,会通过SPI机制去加载这个sevlet容器初始化器,不过Tomcat中并不是直接使用Java的原生SPI机制,而是自己封装的一套类似于Java的SPI机制,在org.apache.catalina.startup.ContextConfig类中,可以看到如下的一个名称为processServletContainerInitializers的方法:
/**
* Scan JARs for ServletContainerInitializer implementations.
*
* 扫描jar包中ServletContainerInitializer的实现类
*/
protected void processServletContainerInitializers() {
List detectedScis;try {// 创建WebappServiceLoader对象
WebappServiceLoader loader =new WebappServiceLoader(
context);// 通过SPI加载ServletContainerInitializer的实现
detectedScis = loader.load(ServletContainerInitializer.class);
} catch (IOException e) {
log.error(sm.getString("contextConfig.servletContainerInitializerFail",
context.getName()),
e);
ok = false;return;
}// 加载到所有ServletContainerInitializer的实现类之后,会再去检查是否标注有@HandlerTypes注解,没有的话也会直接忽略掉,不会加载for (ServletContainerInitializer sci : detectedScis) {
initializerClassMap.put(sci, new HashSet>());
HandlesTypes ht;try {
ht = sci.getClass().getAnnotation(HandlesTypes.class);
} catch (Exception e) {// 删除日志打印代码continue;
}if (ht == null) {continue;
}
Class>[] types = ht.value();if (types == null) {continue;
}for (Class> type : types) {if (type.isAnnotation()) {
handlesTypesAnnotations = true;
} else {
handlesTypesNonAnnotations = true;
}
Set scis =
typeInitializerMap.get(type);// 将解析到的标注有@HandlerTypes注解而且是ServletContainerInitializer的实现放入到一个set集合中,在初始化容器的时候会使用到该集合if (scis == null) {
scis = new HashSet();
typeInitializerMap.put(type, scis);
}
scis.add(sci);
}
}
}
此处自定义的WebappServiceLoader其实和Java原生的ServiceLoader特别类似,如下:
public class WebappServiceLoader<T> {
// jar文件所在目录的前缀
private static final String LIB = "/WEB-INF/lib/";
// SPI文件所在目录前缀
private static final String SERVICES = "META-INF/services/";
private static final Charset UTF8 = Charset.forName("UTF-8");
// 其他非核心代码省略
/**
* Load the providers for a service type.
*/
public List load(Class serviceType) throws IOException {
// 通过SPI加载META-INF/services下的ServletContainerInitializer的实现.
String configFile = SERVICES + serviceType.getName();
LinkedHashSet applicationServicesFound = new LinkedHashSet();
LinkedHashSet containerServicesFound = new LinkedHashSet();
ClassLoader loader = servletContext.getClassLoader();// 加载jar包中的SPI接口实现类
List orderedLibs =
(List) servletContext.getAttribute(ServletContext.ORDERED_LIBS);if (orderedLibs != null) {// handle ordered libs directly, ...for (String lib : orderedLibs) {
URL jarUrl = servletContext.getResource(LIB + lib);if (jarUrl == null) {// should not happen, just ignorecontinue;
}
String base = jarUrl.toExternalForm();
URL url;if (base.endsWith("/")) {
url = new URL(base + configFile);
} else {
url = UriUtil.buildJarUrl(base, configFile);
}try {
parseConfigFile(applicationServicesFound, url);
} catch (FileNotFoundException e) {// no provider file found, this is OK
}
}// and the parent ClassLoader for all others
loader = context.getParentClassLoader();
}// 加载SPI配置文件的内容
Enumeration resources;if (loader == null) {
resources = ClassLoader.getSystemResources(configFile);
} else {
resources = loader.getResources(configFile);
}// 解析SPI配置文件中的配置类while (resources.hasMoreElements()) {
parseConfigFile(containerServicesFound, resources.nextElement());
}// Filter the discovered container SCIs if requiredif (containerSciFilterPattern != null) {
Iterator iter = containerServicesFound.iterator();while (iter.hasNext()) {if (containerSciFilterPattern.matcher(iter.next()).find()) {
iter.remove();
}
}
}
containerServicesFound.addAll(applicationServicesFound);if (containerServicesFound.isEmpty()) {return Collections.emptyList();
}// 加载和实例化具体的实现类return loadServices(serviceType, containerServicesFound);
}// 根据解析到的class全限定名去实例化具体的实现类private List loadServices(Class serviceType, LinkedHashSet servicesFound)throws IOException {
ClassLoader loader = servletContext.getClassLoader();
List services = new ArrayList(servicesFound.size());for (String serviceClass : servicesFound) {try {// 反射加载实现类
Class> clazz = Class.forName(serviceClass, true, loader);// 初始化实现类
services.add(serviceType.cast(clazz.newInstance()));
} catch (Exception e) {// 省略异常
}
}return Collections.unmodifiableList(services);
}
}
通过上述SPI机制,在Tomcat容器启动的时候,会加载并初始化javax.servlet.ServiceContainerInitializer接口的实现类类,在SPI配置文件中可以看到,这个具体的实现类为:SpringServletContainerInitializer。然后调用其onStartup方法,完成SpringMVC框架的初始化过程。SpringMVC的具体启动过程,后面文章中会介绍。
六、Java原生SPI的优点和缺点
6.1、优点
- 不需要改动源码,就可以实现功能扩展,低耦合;
- 对某一个扩展的实现对原来的代码完全没有侵入性;
- 复合开闭原则,需要做功能扩展时,不需要修改原有实现,只需要新增实现即可;
6.2、缺点
通过上面的实例,也可以感受到这种原生SPI的如下几个弊端:
- 从上面的示例中可以看到,如果一个接口有多个实现,就只能遍历所有的实现,并全部实例化;
- 在配置文件中只是简单的罗列出了所有的扩展实现,每个实现并没有具体的名称,会导致在程序中很难去准确的引用它们,比如在实际使用过程中,如何去准确调用某一个具体的实现;
- 扩展如果依赖其他的扩展,做不到自动注入和装配,因为实例化过程完全是通过反射创建的;
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring中的Bean,原生的Java SPI并不支持;
七、Java原生SPI弊端如何解决
上述主要的弊端就是多个实现无法准确定位到某个调用,那么如果可以指定一个名称是不是就可以了呢?其实在Dubbo框架中就是这样实现的,对应的SPI配置文件类似于properties文件,一个名称对应一个实现,类似于如下的内容:
在使用的时候可以通过名称指定具体使用哪个实现。在SpringBoot中同样有类似的实现,通过定义spring.factories文件,里面也是类似于key value格式的内容,一个接口对应多个实现,在加载扩展类的时候,可以将自定义的扩展类加载进去,但是这种方式也没法区分具体的实现类。所以Java中原生提供的SPI只是一种规范,在具体使用的时候,如果这种规范不符合自己的使用场景,完全可以自己去扩展这种规范。
关注菜鸟封神记,定期分享技术干货!
点赞和在看是最大的支持,感谢↓↓↓