「Java 路线」| 服务发现框架 ServiceLoader

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)


目录


1. 前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


2. 什么是服务发现?

服务发现(Service Provider Interface,SPI)是一个服务的注册与发现机制,通过 解耦服务提供者与服务使用者,实现了服务创建 & 服务使用的关注点分离。

服务发现可以理解为控制反转的一种实现形式,即:当「调用方对象」需要使用「依赖项」时,不再直接「构造对象」,取而代之的是由外部 IoC 容器来创建并注入调用方。 IoC 可以认为是一种设计模式,但是由于理论成熟的时间相对较晚,所以没有包含在《设计模式·GoF》之中。

控制反转的实现方式主要有两种:

  • 1、服务提供模式:从外部服务容器抓取依赖对象
  • 2、依赖注入:并以参数的形式注入依赖对象

两者的本质区别是:使用服务提供模式时,调用方可以控制请求依赖对象的时机;而使用依赖项注入方式时,一般由外部主动注入所需对象。

服务提供模式可以为我们带来以下好处:

  • 1、在外部注入或配置依赖项,因此我们可以重用这些组件。当我们需要修改依赖项的实现时,不需要大量修改很多处代码,只需要修改一小部分代码;
  • 2、可以注入依赖项的模拟实现,让代码测试更加容易。

3. ServiceLoader 使用步骤

在分析 ServiceLoader 的使用原理之前,我们先来介绍下 ServiceLoader 的使用步骤。我们直接以 JDBC 作为例子,具体如下:

我们都知道 JDBC 编程有五大基本步骤:

  • 1、(非必须)执行数据库驱动类加载:
Class.forName("com.mysql.jdbc.driver")
  • 2、连接数据库:
DriverManager.getConnection(url, user, password) 
  • 3、创建SQL语句:
Connection#.creatstatement();
  • 4、执行SQL语句并处理结果集:
Statement#executeQuery()
  • 5、释放资源:
ResultSet#close()
Statement#close()
Connection#close()

其中「2、连接数据库」内部就是用了 ServiceLoader。为什么连接数据库需要使用 SPI 设计思想呢?因为操作数据库需要使用厂商提供的数据库驱动程序,如果直接使用厂商的驱动耦合太强了,而使用 SPI 设计就能够实现服务提供者与服务使用者解耦。

下面,我们一步步手写 JDBC 中关于 ServiceLoader 的相关源码:

步骤1:定义服务接口

抽象出一个服务接口,这个接口将由数据库驱动实现类实现:

public interface Driver {
    创建数据库连接
    Connection connect(String url, java.util.Properties info);
    ...
}

步骤2:实现服务接口

数据库厂商提供一个或多个实现 Driver 接口的驱动实现类,以 mysql 和 oracle 为例:

  • mysqlcom.mysql.cj.jdbc.Driver.java
已简化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        注册驱动
        java.sql.DriverManager.registerDriver(new Driver());
    }
    ...
}
  • oracleoracle.jdbc.driver.OracleDriver.java
已简化
public class OracleDriver implements Driver {
    private static OracleDriver defaultDriver = null;
    static {
        if (defaultDriver == null) {
            1、单例
            defaultDriver = new OracleDriver();
            注册驱动
            DriverManager.registerDriver(defaultDriver);
        }
    }
    ...
}

步骤3:注册实现类到配置文件

在工程目录 java 的同级目录中新建目录resources/META-INF/services,新建一个配置文件java.sql.Driver(文件名为服务接口的全限定名),文件中每一行是实现类的全限定名,例如:

com.mysql.cj.jdbc.Driver

我们可以解压mysql-connector-java-8.0.19.jar包,找到对应的 META-INF 文件夹。

步骤4:(使用方)加载服务

DriverManaer.java

已简化
static {
    loadInitialDrivers();
}

private static void loadInitialDrivers() {
    ...
    1、读取 "jdbc.drivers" 属性
    String drivers = System.getProperty("jdbc.drivers");


    1、使用ServiceLoader遍历实现类
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    2、获得迭代器
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    3、迭代
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
    return null;
    ...
}

可以看到,DriverManager 被类加载时(static{})会调用 loadInitialDrivers()。这个方法内部通过 ServiceLoader 提供的迭代器 Iterator<Driver> 遍历了所有驱动实现类。

那么,ServiceLoader 是如何实例化 Driver 接口的实现类的呢?下一节,我们会深入 ServiceLoader 的源码来解答这个问题。


4. ServiceLoader 源码解析

4.1 入口方法

ServiceLoader 提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader)

方法 1:
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    使用 SystemClassLoader 类加载器
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

方法 2:
public static <S> ServiceLoader<S> load(Class<S> service) {
    使用线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

可以看到,三个方法仅在传入的类加载器不同。

4.2 构造方法

已简化
private final Class<S> service;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

private ServiceLoader(Class<S> svc, ClassLoader cl) { 
    1、类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    2、清空 providers
    providers.clear();
    3、实例化 LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}

可以看到,ServiceLoader 的构造器中实例化了一个 LazyIterator 迭代器的实例,这是一个「懒加载」的迭代器。这个迭代器里做了什么呢?

4.3 LazyIterator 迭代器

3、实例化 LazyIterator

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

private class LazyIterator implements Iterator<S> {

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    
    @Override
    public boolean hasNext() {
        return hasNextService();
    }

    @Override
    public S next() {
        return nextService();
    }

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

    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            // configs 未初始化才执行
            3.1 配置文件:META-INF/services/服务接口的全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        }
        
        3.2 解析配置文件资源的迭代器
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        3.3 下一个实现类的全限定名
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;

        4.1 使用类加载器loader加载(不执行初始化)
        Class<?> c = Class.forName(cn, false, loader);
        if (!service.isAssignableFrom(c)) { 检查是否实现 S 接口
            ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
            fail(service, "Provider " + cn  + " not a subtype", cce);
        }

        4.2 使用反射创建服务类实例
        S p = service.cast(c.newInstance());

        4.3 服务实现类缓存到 providers
        providers.put(cn, p);
        return p;
    }
}

-> 3.2 解析配置文件资源的迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    3.2.1 使用 UTF-8 编码输入配置文件资源
    InputStream in = u.openStream();
    BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
    ArrayList<String> names = new ArrayList<>();
    int lc = 1;
    while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    return names.iterator();
}

以上代码已经非常简化了,LazyIterator 的要点如下:

  • hasNext() 判断逻辑:

    • 3.1 配置文件:「META-INF/services/服务接口的全限定名」;
    • 3.2 解析配置文件资源的迭代器;
    • 3.3 下一个实现类的全限定名。
  • next() 逻辑:

    • 4.1 使用类加载器 loader 加载(不执行初始化);
    • 4.2 使用反射创建服务类实例;
    • 4.3 服务实现类缓存到 providers。

小结一下:

LazyInterator 会解析「META-INF/services/服务接口的全限定名」配置,遍历每个服务实现类全限定类名,执行类加载(未初始化),最后将服务实现类缓存到 providers。

这个迭代器在哪里使用的呢?继续往下看~

4.4 包装迭代器

其实 ServiceLoader 本身就是实现 Iterable 接口的:

public final class ServiceLoader<S> implements Iterable<S>

让我们来看看 ServiceLoader 中的 Iterable#iterator() 是如何实现的:

private LazyIterator lookupIterator;

4、返回一个新的迭代器,包装了providers和lookupIterator
public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

        @Override
        public boolean hasNext() {
            4.1 优先从knownProviders取
            if (knownProviders.hasNext()) return true;
            return lookupIterator.hasNext();
        }

        @Override
        public S next() {
            4.2 优先从knownProviders取
            if (knownProviders.hasNext()) return knownProviders.next().getValue();
            return lookupIterator.next();
        }

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

可以看到,ServiceLoader 里有一个泛型方法 Iterator<S> iterator(),它包装了 providers 集合迭代器和 lookupIterator 两个迭代器,迭代过程中优先从 providers 获取元素。providers 就是存放 LazyLoader 中存放类的地方。

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

5. ServiceLoader 源码小结

理解 ServiceLoader 源码之后,我们总结要点如下:

5.1 约束

1、服务实现类必须实现服务接口 S(if (!service.isAssignableFrom(c)));
2、服务实现类需包含无参的构造器,LazyInterator 是反射创建实现类市里的(S p = service.cast(c.newInstance()));
3、配置文件需要使用 UTF-8 编码(new BufferedReader(new InputStreamReader(in, "utf-8")))。

5.2 懒加载

ServiceLoader 使用「懒加载」的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例(nextService())。

5.3 内存缓存

ServiceLoader 使用 LinkedHashMap 缓存创建的服务实现类实例。

提示: LinkedHashMap 在迭代时会按照 Map#put 执行顺序遍历。

5.4 没有服务注销机制

服务实现类实例被创建后,它的垃圾回收的行为与 Java 中的其他对象一样,只有这个对象没有到 GC Root 的强引用,才能作为垃圾回收。而 ServiceLoader 内部只有一个方法来清楚 provices 内存缓存。

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

6. 服务实现的选择

当存在多个提供者时,不一定使用全部的服务实现类,而是需要根据某些特性筛选一种最佳实现。ServiceLoader 机制只能在遍历整个迭代器的过程中,从发现的实现类中决策出一个最佳实现。

举个例子,我们可以使用字符集的表示符号来获得一个对应的 Charset 对象:Charset.forName(String),这个方法里面就只会选择匹配的 Charaset 对象。

CharsetProvider.java

服务接口
public abstract class CharsetProvider {
    public abstract Charset charsetForName(String charsetName);
    // 省略其他方法...
}

Charset.java

public static Charset forName(String charsetName) {
    以下只摘要与ServiceLoader有关的逻辑...

    ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);
    Iterator<CharsetProvider> i = sl.iterator();
    for (Iterator<CharsetProvider> i = providers(); i.hasNext();) {
        CharsetProvider cp = i.next();
        满足匹配条件,return
        Charset cs = cp.charsetForName(charsetName);
        if (cs != null)
            return cs;
    }
}

7. 总结

  • 服务发现 SPI 是控制反转 IoC 的实现方式之一,而 ServiceLoader 是 JDK 中实现的 SPI 框架;

  • ServiceLoader 本身就是一个 Iterable 接口,迭代时会从 META-INF/services 配置中解析接口实现类的全限定类名,使用反射创建服务实现类对象;

  • SPI 是一个相对简易的框架,为了满足复杂业务的需要,一般会使用其他第三方框架,例如后台的 Dubbo、客户端的 ARouter 与 WMRouter等。

在后面的文章中,我将与你探讨 Android 组件化实践,并附带阿里 ARouter 源码分析。点击关注,赶紧上车~ 「Android 路线」| 导读 —— 从零到无穷大


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

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