Java SPI 源码解析

:代码环境基于 JDK 1.8

一、SPI 是什么?

SPI(Service Provider Interface):是一个可以被第三方扩展或实现的 API,它可以用来实现框架扩展和可替换的模块,优势是实现解耦。简单来说就是推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。若在代码里涉及具体的实现类就违反了可挺拔的原则。从而 java SPI 提供了这种服务发现机制:为某个接口寻找服务实现的机制。

二、SPI 与 API 的区别

  • API 直接为提供了功能,使用 API 就能完成任务。
  • API 和 SPI 都是相对的概念,差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩张人员使用。
  • API 大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。SPI 是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方。

三、SPI 使用及示例

  1. 服务调用方通过 ServiceLoader.load 加载服务接口的实现类实例
  2. 服务提供方实现服务接口后, 在自己Jar包的 META-INF/services 目录下新建一个接口名全名的文件, 并将具体实现类全名写入。

示例:

1. 创建接口

public interface Search {
    List<String> searchDoc(String keyword);
}

2. 创建 DatabaseSearch 实现类

public class DatabaseSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.printf("数据库搜索:" + keyword);
        return null;
    }
}

3. 创建 FileSearch 实现类

public class FileSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索:" + keyword);
        return null;
    }
}

4. META-INF.services 中创建接口全限定名文件:spi.learn.Search

spi.learn.FileSearch
spi.learn.DatabaseSearch

5. 测试类

public static void main(String[] args) {
    ServiceLoader<Search> s = ServiceLoader.load(Search.class);
    Iterator<Search> iterator = s.iterator();
    while (iterator.hasNext()) {
        Search search = iterator.next();
        search.searchDoc("spi");
    }
}

-----输出:-----
文件搜索:spi
数据库搜索:spi

四、源码解读

先来看下 ServiceLoader 类的全局变量:

//spi 默认加载的路径
private static final String PREFIX = "META-INF/services/";

// 表示正在被加载的类或接口
// The class or interface representing the service being loaded
private final Class<S> service;

// 用于定位、装入和实例化提供程序的类加载器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// 权限控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// 基于实例的顺序缓存类的实现实例,其中Key为实现类的全限定类名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 当前的"懒查找"迭代器,ServiceLoader的核心
// The current lazy-lookup iterator
private LazyIterator lookupIterator;

ServiceLoader.load(Search.class) 加载入口:

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) {
    // 因为 ServiceLoader 的构造为私有,这里只能依赖此静态方法来访问私有构造实例化,典型的静态工厂方法。
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // svc 为 null,抛 NullPointerException
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 若没有指定加载器,默认使用系统加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // Java安全管理器  
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    // 清空实例化好的缓存。
    providers.clear();
    // "懒查找",ServiceLoader 的核心。
    lookupIterator = new LazyIterator(service, loader);
}

LazyIterator 为 ServiceLoader 的核心,来看看具体源码:

private class LazyIterator implements Iterator<S> {
    Class<S> service;
    ClassLoader loader;
    // 加载资源的URL集合
    Enumeration<URL> configs = null; 
    // 需加载的实现类的全限定类名的集合
    Iterator<String> pending = null;
    // 下一个需要加载的实现类的全限定类名
    String nextName = null;

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

    private boolean hasNextService() {
        //资源已存在,无需加载
        if (nextName != null) {
            return true;
        }
        // 资源为null,尝试加载
        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;
            }
            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 {
            // 反射构造 Class 实例
            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());
            // 实例完成,添加缓存,Key:实现类全限定类名,Value:实现类实例
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated", x);
        }
        throw new Error();          // This cannot happen
    }

    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

LazyIterator 机制总结:LazyIterator 也实现了 Iterator接口的实现,Lazy特性体现在只有在使用 ServiceLoader 调用 iterator() 方法获取 Iterator 接口匿名实现类后, 再调用 hasNext() 方法时,才会"懒判断"或者"懒加载"下一个实现类的实例。调用的入口,也就是示例 main 方法中 while 那一步,我们再看下 iterator() 的 Iterator 匿名实现类源码:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        
        public boolean hasNext() {
            if (knownProviders.hasNext()) return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())   return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

调用链分析完,最后看下 hasNextService 中解析限定名文件的 parse 方法,主要检查文件内容的字符合法性、缓存过滤避免重复加载。

private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    InputStream in = null;
    BufferedReader r = null;
    // 存放 META-INF/services 下文件中的实现类的全类名
    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();
}

//具体解析资源文件中每一行内容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) 
        throws IOException, ServiceConfigurationError {
    String ln = r.readLine();
    if (ln == null) {
        return -1;   //-1表示解析完成
    }
    // 如果存在'#'字符,截取第一个'#'字符串之前的内容,'#'字符之后的属于注释内容
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        //不合法的标识:' '、'\t'
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        //判断第一个 char 是否一个合法的 Java 起始标识符
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        //判断所有其他字符串是否属于合法的Java标识符
        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);
        }
        //不存在则缓存
        if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln);
    }
    return lc + 1;
}

五、总结

优点

  1. 使用 Java SPI 机制的优势是实现解耦,使第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
  2. 相比使用提供接口 jar 包供第三方服务使用的方式,SPI 使得源框架不必关心接口的实现类的路径,可以不使用硬编码 import 导入实现类。

缺点

  1. 虽然 ServiceLoader 使用了懒加载,但结果还是通过遍历获取,基本上可以说是全部实例化了一遍,所以说,这个懒加载机制在此场景下是浪费的。
  2. 由于是遍历获取,所以获取实现类的方式不够灵活。
  3. 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350