前言
近日,在浏览Dubbo官网时看到了Dubbo SPI 这个词。搜了搜,原来JAVA有个SPI机制。好奇心驱使我想知道,这到底是个什么东西。
JAVA SPI机制
如果我们要动态加载一个类,会怎么办?
- 调用 Class.forName("cn.test.Hello") 方法
- 调用某个 ClassLoader. loadClass("cn.test.Hello") 方法
动态加载的好处,就是能在运行期按需加载,需要什么类,就加载什么类,编译期不报错。这样带来的好处,就是我们可以动态配置运行期加载什么类。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它能够加载ClassPath路径下的META-INF/services文件夹下的文件中,配置的类。
1.如何使用
先定义一个接口
public interface HellloService { public void sayHello(); }
定义两个实现类:
public class ChineseHello implements HellloService { @Override public void sayHello() { System.out.println("中文说你好"); } } public class EnglishHello implements HellloService { @Override public void sayHello() { System.out.println("English hello"); } }
接着接着我们建一个META-INF/services的文件夹,在文件夹内新建一个以接口全限定名为名字的文件
并在文件中配置接口的实现类。
com.service.hi.servicehi.spi.ChineseHello com.service.hi.servicehi.spi.EnglishHello
然后使用ServiceLoader 在运行期动态的加载接口的实现类,调用其方法
public class SpiMain { public static void main(String[] args) { ServiceLoader<HellloService> services = ServiceLoader.load(HellloService.class); for (HellloService hellloService: services){ hellloService.sayHello(); } } }
中文说你好 English hello
由此看出: SPI就是一个“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制. ServiceLoader 就是动态加载动态的工具类。
所以:
- 往大了说SPI是一种服务发现机制,
- 往小了说SPI就是一个可配置化动态加载类的工具类。
2.源码分析
2.1ServiceLoader
我们再从源码的层面解开他的面目 ServiceLoader#load静态方法。
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
可以看出
- ServiceLoader#load静态方法调用另一个重载的load方法并默认把当前线程的 ClassLoader 作为参数传递过去。
- 从两个参数的load可以看出,我们可以指定其ClassLoader
- load静态方法最终是new 一个 ServiceLoader实例出来。
下面看看ServiceLoader的构造方法。
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(); } public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
发现没有加载配置文件的过程啊?
其实ServiceLoader使用懒加载的方式,也就是当我们在遍历的时候才去加载配置文件。LazyIterator 就是懒加载迭代器。
2.2LazyIterator
public S next() { if (acc == null) { return nextService(); } } private S nextService() { if (!hasNextService())//判断是否又下一个元素 throw new NoSuchElementException(); String cn = nextName; nextName = null;//下一个实现类的全限定名 Class<?> c = null; //使用反射获取实现类的Class对象 c = Class.forName(cn, false, loader); //创建一个对象 S p = service.cast(c.newInstance()); //放到缓存中 providers.put(cn, p); 返回 return p; } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { //获取文件名 String fullName = PREFIX + service.getName(); //加载文集URL 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; }
流程:
- 根据接口全限定名,结合META-INF/services/ ,拼接文件位置。这个是定死
- 使用ClassLoader 加载文件资源
- 解析出 对应的文件中配置了接口的哪些实现类
- 使用反射 根据解析出的类全限定名,实例化
- 放到缓存中。
- 返回
再次验证了:
- SPI 本质 就是动态加载类机制
- ServiceLoader 就是一个动态加载类的工具类
- 底层还是使用了我们常见的Class ,ClassLoader
3.熟悉又陌生的应用场景存在问题
SPI 其实对于我们来说一定不陌生。 以前我们需要手写Class.forName("com.mysql.jdbc.Driver")
加载驱动。
现在不用写了,其实就是使用了SPI技术。
4.存在问题
- 当我们想找某个类时,需要遍历,没有做到真正的按需加载,
- 多线程下不安全
似曾相似(Spring SPI)
SpringFactoriesLoader
其实当首次看到SPI的时候,突然看着很熟悉的感觉,好像在spring见过。
思索一番,最最经典不就是SpringFactoriesLoader
SpringFactoriesLoader
配置文件的文件夹目录 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; //加载META-INF/spring.factories 中的所有配置。 public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); } private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { //加载文件资源URL 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()) { List<String> factoryClassNames = Arrays.asList( StringUtils.commaDelimitedListToStringArray((String) entry.getValue())); result.addAll((String) entry.getKey(), factoryClassNames); } } //放入缓存 cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
spring.factories
# PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader
与JAVA SPI 不同的是,
- Spring spi 获取的是 固定
META-INF/spring.factories
文件中的配置。JAVA SPI是以某个接口的全限定路径为名的文件,需要开发人员自己定义 - Spring Spi 是以K-V的形式配置,
- Spring SPI 首次加载配置文件时,会把所有spring.factories配置文件中配置解析出来放到缓存中,以后获取时直接从缓存中按Key值,取Value
由此看出: Spring spi 比 JAVA spi 设计的更好。其本质也是 Class , ClassLoader的高级封装。
Dubbo SPI
看了JAVA SPI ,想了想Sprng SPI , 我似乎知道了 Dubbo SPI 是什么样子了。
ExtensionLoader
- Dubbo使用ExtensionLoader 做为动态加载配的工具。
- Dubbo的配置文件 放到
"META-INF/dubbo/"
目录下,并以具体扩展接口全名命名,类似 Java spi - Dubbo SPI 也是采用了K-V形式的配置,类似spring spi
- ExtensionLoader 提供了更多的方法,提供丰富的获取功能
- Dubbo SPI 还增加了 IOC 和 AOP 等特性
- 其本质也是 Class , ClassLoader的高级封装。
看出Dubbo 跟JAVA SPI ,Spring SPI 都哦相似之处,也许Dubbo设计之初就是参考了 JAVA SPI ,Spring SPI
本文并不讲Dubbo SPI 的更多内容,只想讲讲我对 SPI的理解,为以后读Dubbo源码打个前站。
总结
万变不离其宗,不管是 JAVA SPI ,Spring SPI ,Dubbo SPI 。其本质都是对反射的高级封装,Class, ClassLoader 才是核心。