ServiceLoader(二)源码解析

源码分析

java作为一个把interface看得很重要的语言,发展出了各种自动发现机制。比如eureka服务发现,slf4j日志自动发现日志实现,亦或者jdbc定义的接口自动根据URL选择对应的jdbc实现等。自动发现机制将接口和实现很好地隔离开,使得代码高度解耦合,是一种非常好地设计思路。

前面文章中,我们简单了解了java内置的自动发现(ServiceLoader)是怎么使用的:https://www.cnblogs.com/lay2017/p/11194625.html

本文接续使用的内容,将打开ServiceLoader的源代码,阅读一下它的加载逻辑,为此,我们从以下这两行代码开始

ServiceLoader<Speaker> speakers = ServiceLoader.load(Speaker.class);
speakers.forEach(Speaker::say);

 

ServiceLoader.load()加载配置

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

继续跟进load,可以看到实例化了一个ServiceLoader的实例

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

继续跟进构造方法,重点落在了reload方法上

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

跟进reload方法,providers缓存了已经加载的实现类,以全路径名作为key。

clear方法在每次load的时候作清空,然后初始化一个Iterator迭代器(Lazy),可以想象后面的代码就是迭代这个迭代器,迭代一个加载一个实现类

private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

再进入LazyIterator构造方法看看,单纯的赋值操作

Class<S> service;
ClassLoader loader;

private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

ServiceLoader方法就是单纯地清空原有地缓存,然后初始化一个迭代器而已,比较简单。

 

迭代LazyIterator

ServiceLoader方法初始化LazyIterator,那么我们看看这个迭代器地迭代过程做了什么。

hasNextService方法

首先看看判断是否有下一个Service的方法

private static final String PREFIX = "META-INF/services/";

Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }

    // 从类路径加载配置文件地URL
    if (configs == null) {
        try {
            // 拼接全路径 META-INF/services/接口类全路径名
            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;
        }
        // 迭代URL地址,获取实现类的全路径名
        pending = parse(service, configs.nextElement());
    }
    // 将获取到的全路径名作为下一个加载的类全路径名
    nextName = pending.next();
    return true;
}

hasNextService先根据接口类的全路径名获取了所有配置的URL地址

然后迭代一个URL地址,通过parse方法解析出下一个待加载的配置文件中包含的实现类集合。我们跟进parse方法,看看它是怎么把URL地址解析成全路径配置的。

private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError{
    InputStream in = null;
    BufferedReader r = null;
    // 集合包含多个全路径名
    ArrayList<String> names = new ArrayList<>();
    try {
        // 文件流
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        // 解析文件流
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    // 返回迭代器
    return names.iterator();
}

parseLine执行一次,则解析一行配置,也就获得一个实现类的全路径,跟进parseLine看看

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError{
    // 读取1行
    String ln = r.readLine();
    if (ln == null) {
        // 空文件
        return -1;
    }

    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        // 先从缓存判断是否存在该实现类,再从names中判断是否包含(二者加在一起,防止相同配置文件重复配置,也防止不同配置文件重复配置)
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    // 下一行
    return lc + 1;
}

readline一行一行地读取配置文件的实现类配置,如果没有被加载过,那么就添加为本次待加载地类。

hasNextService方法,其实就是从配置文件中不断地读取配置到集合当中,供nextService使用

nextService加载实现类

hasNextService做了前置工作,获取了全路径名供迭代。而nextService方法将负责加载该类到虚拟机当中。我们打开nextService看看

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

方法比较简单,单纯地通过Class.forName加载类,并反射获取实例对象强转了一次,并返回。

 

总结

ServiceLoader作为JAVA内置地spi机制,使用起来非常地简单。如同前面文章说的,其实就是面向接口 + 配置文件 + 多态,我们再一次见到了java从面向对象不断延伸地东西有多么灵活、干净。

posted @ 2020-01-12 16:19  __lay  阅读(538)  评论(0)    收藏  举报