一、SPI简介
1.1 什么是SPI机制?
SPI机制是Java的一种服务发现机制,为了方便应用扩展。
那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。
你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制。这就是SPI机制( Service Provider Interface)
1.2 SPI和API的区别是什么
API图解:
SPI图解:
1.3 为什们要使用SPI
- 松耦合架构:SPI机制可以实现模块之间的松耦合。核心模块定义服务接口,而具体的实现由各个供应商按照接口规范提供。这种解耦方式使得系统更易于扩展、维护和升级。
- 可插拔扩展性:SPI允许开发者在不修改核心代码的情况下添加新功能或模块。通过实现并注册服务接口的不同提供者,可以灵活地增加、替换或升级功能,实现可插拔式的扩展性。
- 多样化实现选择:SPI机制支持多个供应商提供不同的实现,开发者可以根据需求选择合适的实现。这为应用程序提供了更多的选择和灵活性,可以根据具体场景选择性地加载适当的实现。
- 统一标准和规范:通过定义服务接口,SPI能够统一不同供应商的实现,并提供统一的调用方式和规范。这有助于保持代码的一致性、可读性和可维护性。
- 动态加载和替换:SPI机制支持运行时动态加载和替换服务提供者的实现。这意味着可以根据配置或条件选择性地加载适当的实现,而无需重新编译和部署应用程序。
总而言之,Java使用SPI机制主要是为了实现松耦合的架构、可插拔的扩展性和灵活的实现选择。它提供了一种规范和标准化的方式来管理和扩展应用程序的功能,使得系统更加可扩展、可维护和可定制化。
二、SPI机制实现约定
2.1 Jdk实现SPI
ServiceLoader
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class); Iterator<Driver> iterator = load.iterator(); public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } /** .... */ private static final String PREFIX = "META-INF/services/"; 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); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
类加载器:
- 启动类加载器(Bootstrap Class-Loader):负责加载 JAVA_HOME/lib 目录的
- 扩展类加载器(Extension Class-Loader): 负责加载 JAVA_HOME/lib/ext 目录的
- 应用程序类加载器(App Class-Loader): 负责加载用户路径 classpath上的类库
- 自定义类加载器: 加载应用之外的类文件
按照双亲委托的这个思路:
- 首先在使用JDBC时,rt.jar中的接口会被BootStrapClassLoader加载,然后它的实现类由AppClassLoader加载,因为导入的驱动是在classPath路径下的。
- 然后,在使用Driver时,因为父加载器所加载的类无法访问到子加载器所加载的类。也就是说在用Driver时,它就是个空壳子,找不到它的实现类。
线程上下文加载器:JVM 线程上下文类加载器 - 掘金
- jdk内部类用引导类加载器加载,调SPI接口的方法依赖外部JAR包用应用类加载器加载,父加载器访问不到子加载器的类。但是可以设置当前线程的上下文类加载器,把当前线程上下文类加载器加载的类一并纳入可视范围
2.2 Dubbo实现SPI
ExtensionLoader
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); Protocol dubbo = extensionLoader.getExtension("dubbo"); /** .... */ private static final String SERVICES_DIRECTORY = "META-INF/services/"; private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
2.3 Spring实现SPI
SpringFactoriesLoader
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); List<ApplicationContentInitialier> factories = SpringFactoriesLoader.loadFactories(EnableAutoConfiguration.class, classLoader); /** ... */ private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryImplementationName = var9[var11]; result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }
2.4 SPI 产品实现对比
2.5 SPI 产品优缺点对比
三、项目中实现示例
cdo-platform-okhttp-starter: