近日,在浏览Dubbo官网时看到了Dubbo SPI 这个词。搜了搜,原来JAVA有个SPI机制。好奇心驱使我想知道,这到底是个什么东西。
如果我们要动态加载一个类,会怎么办?
动态加载的好处,就是能在运行期 按需加载 ,需要什么类,就加载什么类,编译期不报错。这样带来的好处,就是我们可以动态配置运行期加载什么类。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它能够加载ClassPath路径下的META-INF/services文件夹下的文件中,配置的类。
先定义一个接口
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 就是动态加载动态的工具类。
所以:
我们再从源码的层面解开他的面目 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的构造方法。
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 就是懒加载迭代器。
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;
}
复制代码
流程:
再次验证了:
SPI 其实对于我们来说一定不陌生。 以前我们需要手写 Class.forName("com.mysql.jdbc.Driver") 加载驱动。
现在不用写了,其实就是使用了SPI技术。
其实当首次看到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 不同的是,
META-INF/spring.factories
由此看出: Spring spi 比 JAVA spi 设计的更好。 其本质也是 Class , ClassLoader的高级封装 。
看了JAVA SPI ,想了想Sprng SPI , 我似乎知道了 Dubbo SPI 是什么样子了。
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 才是核心。
推荐阅读:
SpringCloud源码阅读0-SpringCloud必备知识
SpringCloud源码阅读1-EurekaServer源码的秘密
SpringCloud源码阅读2-Eureka客户端的秘密
SpringCloud源码阅读3-Ribbon负载均衡(上)
Springcloud源码阅读4-Ribbon负载均衡(下)
发送http请求(1):发送http请求的几种方式
发送http请求(2):RestTemplate发送http请求
@LoadBalanced注解RestTemplate拥有负载均衡的能力
欢迎大家关注我的公众号【源码行动】,最新个人理解及时奉送。