JAVA的SPI机制
1.什么是SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
3.SPI的简单实现
下面我们来简单实现一个jdk的SPI的简单实现。
首先第一步,定义一组接口:
1 public interface UploadCDN { 2 void upload(String url); 3 }
这个接口分别有两个实现:
1 public class QiyiCDN implements UploadCDN { //上传爱奇艺cdn 2 @Override 3 public void upload(String url) { 4 System.out.println("upload to qiyi cdn"); 5 } 6 } 7 8 public class ChinaNetCDN implements UploadCDN {//上传网宿cdn 9 @Override 10 public void upload(String url) { 11 System.out.println("upload to chinaNet cdn"); 12 } 13 }
然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名:
这时,通过serviceLoader加载实现类并调用:
1 public static void main(String[] args) { 2 ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class); 3 for (UploadCDN u : uploadCDN) { 4 u.upload("filePath"); 5 } 6 }
输出如下:
这样一个简单的spi的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。
4. SPI原理解析
通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:
1 public final class ServiceLoader<S> implements Iterable<S> { 2 3 4 //扫描目录前缀 5 private static final String PREFIX = "META-INF/services/"; 6 7 // 被加载的类或接口 8 private final Class<S> service; 9 10 // 用于定位、加载和实例化实现方实现的类的类加载器 11 private final ClassLoader loader; 12 13 // 上下文对象 14 private final AccessControlContext acc; 15 16 // 按照实例化的顺序缓存已经实例化的类 17 private LinkedHashMap<String, S> providers = new LinkedHashMap<>(); 18 19 // 懒查找迭代器 20 private java.util.ServiceLoader.LazyIterator lookupIterator; 21 22 // 私有内部类,提供对所有的service的类的加载与实例化 23 private class LazyIterator implements Iterator<S> { 24 Class<S> service; 25 ClassLoader loader; 26 Enumeration<URL> configs = null; 27 String nextName = null; 28 29 //... 30 private boolean hasNextService() { 31 if (configs == null) { 32 try { 33 //获取目录下所有的类 34 String fullName = PREFIX + service.getName(); 35 if (loader == null) 36 configs = ClassLoader.getSystemResources(fullName); 37 else 38 configs = loader.getResources(fullName); 39 } catch (IOException x) { 40 //... 41 } 42 //.... 43 } 44 } 45 46 private S nextService() { 47 String cn = nextName; 48 nextName = null; 49 Class<?> c = null; 50 try { 51 //反射加载类 52 c = Class.forName(cn, false, loader); 53 } catch (ClassNotFoundException x) { 54 } 55 try { 56 //实例化 57 S p = service.cast(c.newInstance()); 58 //放进缓存 59 providers.put(cn, p); 60 return p; 61 } catch (Throwable x) { 62 //.. 63 } 64 //.. 65 } 66 } 67 }
上面的代码只贴出了部分关键的实现,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:
Spring Boot的扩展机制之Spring Factories
Spring Boot中的SPI机制
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的SPI机制是Spring Boot Starter实现的基础。
![](https://img2020.cnblogs.com/blog/1087466/202111/1087466-20211104103959663-153864223.png)
Spring Factories实现原理
spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
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 factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件。也就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
com.xxx.interface=com.xxx.classname
如果一个接口希望配置多个实现类,可以使用’,’进行分割
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
2019-11-04 将博客搬至CSDN
2019-11-04 excel转pdf,itextpdf转换excel