编程语言
首页 > 编程语言> > Dubbo源码阅读前夜-SPI的本质

Dubbo源码阅读前夜-SPI的本质

作者:互联网

SPI

阅读源码可能不会提高我的编码能力,但至少我在用它的时候,心里通畅。

前言

近日,在浏览Dubbo官网时看到了Dubbo SPI 这个词。搜了搜,原来JAVA有个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

我们再从源码的层面解开他的面目
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 就是懒加载迭代器。

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;
}

流程:

  1. 根据接口全限定名,结合META-INF/services/ ,拼接文件位置。这个是定死
  2. 使用ClassLoader 加载文件资源
  3. 解析出 对应的文件中配置了接口的哪些实现类
  4. 使用反射 根据解析出的类全限定名,实例化
  5. 放到缓存中。
  6. 返回

再次验证了:

熟悉又陌生的应用场景

SPI 其实对于我们来说一定不陌生。
以前我们需要手写Class.forName("com.mysql.jdbc.Driver")加载驱动。

现在不用写了,其实就是使用了SPI技术。

存在问题

似曾相似(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 比 JAVA spi 设计的更好。
其本质也是 Class , ClassLoader的高级封装

Dubbo SPI

看了JAVA SPI ,想了想Sprng SPI , 我似乎知道了 Dubbo SPI 是什么样子了。

ExtensionLoader

看出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-Eureka服务端的秘密
SpringCloud源码阅读2-Eureka客户端原理
SpringCloud源码阅读3:Ribbon实现客户端负载均衡(上)
Springcloud源码阅读4:Ribbon客户端负载均衡(下)
欢迎大家关注我的公众号【源码行动】,最新个人理解及时奉送。

标签:Dubbo,ClassLoader,public,SPI,源码,ServiceLoader,加载
来源: https://blog.51cto.com/u_8958931/2817484