SPI——一种“规范的定义与实现”的极好思路
Java SPI是通过ServiceLoader实现的。不过像SpringFactoriesLoader这样的相当于自己实现ServiceLoader。SPI实现不难,不过他为我们提供了一个非常好的设计思路。
实现
1.定义标准接口:在Project-A
package com.example.spi;
public interface TestInterface {
String test(String arg) ;
}
2.实现该标准:在project-B
package com.example.spi.test;
public class MySpi implements com.example.spi.TestInterface{
public String test(String arg) {
return arg+"-----------------spi";
}
}
配置该实现:在project-B
#文件位置:
# META-INF
# └─services
# └─com.example.spi.TestInterface
com.example.spi.test.MySpi
3.使用该服务:在Project-C
public class DemoApplication {
public static void main(String[] args) {
ServiceLoader<TestInterface> serviceLoader=ServiceLoader.load(TestInterface.class);
serviceLoader.forEach(i->{
System.out.println(i.test("aaaa"));
});
}
}
注:这个实现必须要有一个无参构造,因为ServiceLoader加载到之后会默认构造出该实现的实例,默认是使用无参构造器。c.newInstance()。这个是懒加载的,只有调用了next相关的方法才会加载,他的Iterator接口相关的功能是委托给内部属性(LazyIterator,他是ServiceLoader的一个private内部类)实现的。ServiceLoader里面有一个PREFIX常量:META-INF/services/,这个是和SpringFactoriesLoader思路是一样的。应该说它必须这么做。
原理
下面会通过ServiceLoader和SpringFactoriesLoader为例解析SPI的原理。
ServiceLoader< S >
1.定义标准,这里的标准有俩个:第一是配置的位置,第二是配置的命名规范。
这里的位置是通过一个final限定的PREFIX来约束的,位置是META-INF/services/。第二个命名规范必须是全限定类目,在寻找资源的时候会用到。另外还有配置内容的规范,一行一个实现类的全限定类目。
2.实现获取标准实现的逻辑:
本质上来说:ServiceLoader.load(Class)是在配置ServiceLoader通过配置构建出ServiceLoader对象。这个ServiceLoader对象对资源是懒加载的,内部委托给LazyIterator实现,在使用iterator的时候才会正式加载,LazyIterator有俩个重要的方法:
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;
}
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) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//构造对象
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
SpringFactoriesLoader
SpringFactoriesLoader在自动配置的地位很高,基本思路是一样的,定义标准,实现读取标准逻辑。
1.定义标准:配置文件位置:META-INF/spring.factories,内容格式(properties)。
2.读取标准的逻辑:读取文件,解析文件。不过SpringFactoriesLoader只需要读取一个spring.factories全部的内容都是配置在这里面的,同时它也是有缓存的,解析过的会存Map(Spring很多地方都会这么处理)。它有一个重要的方法:
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);
}
}
破坏双亲委派模型
java.util.ServiceLoader和java.sql.DriverManager都是rt.jar下的类,在双亲委派模型下是有BootstrapClassLoader(null,非实际Java类实现)加载的,而常规情况下我们或者说第三方的类是由sun.misc.Launcher.AppClassLoader加载的。也就是说ServiceLoader无法通过常规手段获取到AppClassLoader(也叫SystemClassLoader)的实例,于是Java团队来在Thread对象里面存储一个ContextClassLoader这样如果设置了ClassLoader就可以获取到当前线程的ClassLoader如果为空则ClassLoader.getSystemClassLoader()获取默认的AppClassLoader。通过这样的操作ServiceLoader又是通过AppClassLoader或者用户自定义的ClassLoader获取类这样可以解决ServiceLoader无法获取三方SPI类的问题。而自定义的ClassLoader也只需要按照双亲委派模型实现就行了。于是一个底层类加载器的加载的类的内部可以加载本是由子类加载器加载的类;说实话在我看来这恰恰是为了遵循委派模型而设计的,毕竟至始至终底层类加载器都没有去加载用户自定义类。
注:通过将你的类库(jar)设置进lib/ext(或者java.ext.dirs)目录,是可以实现让sun.misc.Launcher.ExtClassLoader加载器加载你的类的,同理通过设置vm参数-Dbootclasspath可以将你的类交给BootstrapClassLoader加载。
注:在Java9之后ClassLoader除了开放getSystemClassLoader()用以获取AppClassLoader以外还开放了getPlatformClassLoader(),他负责加载平台类,用以替代ExtClassLoader(是的,Java9以后它没了),这是由于java9的模块化而引入的东西,模块化增强了Java平台的扩展性同时又减小了jre的尺寸。同时Java9开始,java开放了一个BootClassLoader类它是对BootstrapClassLoader的抽象实现,不过对用户来说依然是隐藏(null)的。
更多会在后续热替换、热部署与OSGi的探索中提到。
启示
我一直持有这样一个观点:标准先行。SPI提供了一个很好的实现标准先行原则的方案。java.sql.Driver是SPI实现的Springboot的AutoConfiguration也是这么实现的。它们有一个共同的特点:定义标准的适合只能是定义标准而无法得知它人提供的实现是怎么样的。然而,即使我们无法知道实现是怎么样的但是通过标准的定义我们依然对服务提供方进行的强约束,不论是配置格式还是接口定义都进行了约束,并且在我们无法得知对方的时候我们依旧可以很好的合作,同时我们的合作不会因为非标准的变更而一起做改变(也就是解耦)。这是一个非常棒的多项目合作的思路。
注:有意见或者建议请评论留言。转载请标明出处与作者。