「Java 路线」| 服务发现框架 ServiceLoader | 七日打卡

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 「Java 路线」| 服务发现框架 ServiceLoader | 七日打卡

学习路线图:


image.png

1. 认识服务发现?


1.1 什么是服务发现


服务发现(Service Provider Interface,SPI)是一个服务的注册与发现机制,通过解耦服务提供者与服务使用者,实现了服务创建 & 服务使用的关注点分离。 服务提供模式可以为我们带来以下好处:


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

服务发现示意图

image.png

1.2 服务发现和依赖注入的区别


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


  • 1、服务提供模式: 从外部服务容器抓取依赖对象,调用方可以 “主动” 控制请求依赖对象的时机;
  • 2、依赖注入: 并以参数的形式注入依赖对象,调用方 “被动” 接收外部注入的依赖对象。


2. JDK ServiceLoader 的使用步骤


在分析 ServiceLoader 的使用原理之前,我们先来介绍下 ServiceLoader 的使用步骤。

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


以下为使用步骤,具体分为 5 个步骤:


  • 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()
复制代码


下面,我们一步步手写 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() {
    ...
    // 读取 "jdbc.drivers" 属性
    String drivers = System.getProperty("jdbc.drivers");
    // 1、使用 ServiceLoader 遍历 Driver 服务接口的实现类
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    // 2、获得迭代器
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    // 3、迭代(ServiceLoader 内部会通过反射)
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
    return null;
    ...
}
复制代码

可以看到,DriverManager 被类加载时(static{})会调用 loadInitialDrivers() 。这个方法内部通过 ServiceLoader 提供的迭代器 Iterator 遍历了所有驱动实现类。那么,ServiceLoader 是如何实例化 Driver 接口的实现类的呢?下一节,我们会深入 ServiceLoader  的源码来解答这个问题。


3. ServiceLoader 源码解析


3.1 ServiceLoader 入口方法


ServiceLoader 提供了三个静态泛型工厂方法,内部最终将调用 ServiceLoader.load(Class, ClassLoader),其中第一个参数就是服务接口的 Class 对象。


ServiceLoader.java


// 方法 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);
}
复制代码


可以看到,三个方法仅在传入的类加载器不同,最终只是返回了一个面向服务接口 S 的 ServiceLoader 对象。我们先看一下构造器里做了什么工作。


3.2 ServiceLoader 构造方法


ServiceLoader.java


// 已简化
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 迭代器的实例,这是一个「懒加载」的迭代器。这个迭代器里做了什么呢?我们继续往下看


3.3 LazyIterator 迭代器


ServiceLoader.java


// -> 3、实例化 LazyIterator
// 前文提到的配置文件路径
private static final String PREFIX = "META-INF/services/";
private class LazyIterator implements Iterator<S> {
    // 服务接口 Class 对象
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    // pending、nextName:用于解析配置文件中的服务实现类名
    Iterator<String> pending = null;
    String nextName = null;
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    // 3.1 判断是否有下一个服务实现
    @Override
    public boolean hasNext() {
        return hasNextService();
    }
    // 3.2 返回下一个服务实现
    @Override
    public S next() {
        return nextService();
    }
    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
    // -> 3.1 判断是否有下一个服务实现
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            // 3.1.1 拼接配置文件路径:META-INF/services/服务接口的全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        }
        // 3.1.2 parse:解析配置文件资源的迭代器
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        // 3.1.3 下一个实现类的全限定名
        nextName = pending.next();
        return true;
    }
    // 3.2 返回下一个服务实现
    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        // 3.2.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);
        }
        // 3.2.2 使用反射创建服务类实例
        S p = service.cast(c.newInstance());
        // 3.2.3 服务实现类缓存到 providers
        providers.put(cn, p);
        return p;
    }
}
// -> 3.1.2 parse:解析配置文件资源的迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    // 使用 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.1 拼接配置文件路径:「META-INF/services/服务接口的全限定名」;
  • 3.1.2 解析配置文件资源的迭代器;
  • 3.1.3 找到下一个实现类的全限定名。
  • next() 逻辑:
  • 3.2.1 使用类加载器 loader 加载(不执行初始化);
  • 3.2.2 使用反射创建服务类实例;
  • 3.2.3 服务实现类缓存到 providers。


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


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


3.4 包装迭代器


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

ServiceLoader.java


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>() {
        // providers 就是上一节 next() 中缓存的服务实现
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        @Override
        public boolean hasNext() {
            // 4.1 优先从 knownProviders 取,再从 LazyIterator 取
            if (knownProviders.hasNext()) return true;
            return lookupIterator.hasNext();
        }
        @Override
        public S next() {
            // 4.2 优先从 knownProviders 取,再从 LazyIterator 取
            if (knownProviders.hasNext()) return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}
复制代码


可以看到,ServiceLoader 里有一个泛型方法 Iterator<S> iterator(),它包装了 providers 集合迭代器和 lookupIterator 两个迭代器。对于已经 “发现” 的服务实现类会被缓存到 providers 集合中,包装类的作用就是优先读取缓存而已。


4. ServiceLoader 源码分析总结


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


4.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")) )。


4.2 懒加载


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


4.3 内存缓存


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

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


4.4 没有服务注销机制


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


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


4.5 没有服务筛选机制


当存在多个提供者时,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;
    }
}
复制代码



5. 总结


  • 服务发现 SPI 是控制反转 IoC 的实现方式之一,而 ServiceLoader 是 JDK 中实现的 SPI 框架。ServiceLoader 本身就是一个 Iterable 接口,迭代时会从 META-INF/services 配置中解析接口实现类的全限定类名,使用反射创建服务实现类对象;
  • ServiceLoader 是 JDK 自带的服务发现框架,原理也相对简单,比如 Charset、AnnocationProcessor 等功能都是基于 ServiceLoader 实现的。另一方面,ServiceLoader 是一个相对简易的框架,为了满足复杂业务的需要,一般会使用其他第三方框架,例如后台的 Dubbo、客户端的 ARouter 与 WMRouter等。
相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
1月前
|
算法 Java 数据处理
Java集合框架的优缺点
Java集合框架的优缺点
|
1月前
|
运维 监控 Java
推荐一款好用的Java分布式任务调度框架!
推荐一款好用的Java分布式任务调度框架!
168 0
|
2天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
2天前
|
Java Nacos 开发者
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例
|
2天前
|
Dubbo Java 应用服务中间件
Java从入门到精通:3.2.2分布式与并发编程——了解分布式系统的基本概念,学习使用Dubbo、Spring Cloud等分布式框架
Java从入门到精通:3.2.2分布式与并发编程——了解分布式系统的基本概念,学习使用Dubbo、Spring Cloud等分布式框架
|
7天前
|
Java Maven 开发工具
《Java 简易速速上手小册》第5章:Java 开发工具和框架(2024 最新版)
《Java 简易速速上手小册》第5章:Java 开发工具和框架(2024 最新版)
28 1
|
12天前
|
Java 大数据 云计算
Spring框架:Java后台开发的核心
【4月更文挑战第15天】Spring框架在Java后台开发中占据核心位置,因其控制反转(IoC)、面向切面编程(AOP)、事务管理等特性提升效率和质量。Spring提供数据访问集成、RESTful Web服务和WebSocket支持。优势包括高效开发、灵活扩展、强大生态圈和广泛应用。应用于企业级应用、微服务架构及云计算大数据场景。掌握Spring对Java开发者至关重要。
|
14天前
|
存储 Java 编译器
Java集合丛林:深入了解集合框架的秘密
Java集合丛林:深入了解集合框架的秘密
16 0
Java集合丛林:深入了解集合框架的秘密
|
18天前
|
存储 Java 数据库连接
java使用mp持久化框架,写入5000个字符,但是VARCHAR(255) 会报错
使用Java的MyBatis Plus框架时,如果尝试将超过VARCHAR(255)限制的字符串(如5000个字符)存入数据库,会抛出异常。解决方法是将列类型改为TEXT。可通过在实体类属性上添加`@TableField(typeHandler = JdbcType.CLOB)`注解,如`private String content;`,将属性映射到CLOB类型列,以存储更长字符串。
9 0
|
18天前
|
存储 Java
java反射——设计框架的灵魂
java反射——设计框架的灵魂