Java | 带你理解 ServiceLoader 的原理与设计思想

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Java | 带你理解 ServiceLoader 的原理与设计思想

前言


  • ServiceLoaderJava提供的一套**SPI(Service Provider Interface,常译:服务发现)**框架,用于实现服务提供方与服务使用方解耦
  • 在这篇文章里,我将带你理解ServiceLoader的原理与设计思想,希望能帮上忙。请点赞,你的点赞和关注真的对我非常重要!


目录


image.png

1. SPI 简介


  • 定义一个服务的注册与发现机制
  • 作用 通过解耦服务提供者与服务使用者,帮助实现模块化、组件化


image.png

2. ServiceLoader 使用步骤


我们直接使用JDBC的例子,帮助各位建立起对ServiceLoader 的基本了解,具体如下:


我们都知道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()


操作数据库需要使用厂商提供的数据库驱动程序,直接使用厂商的驱动耦合太强了,更推荐的方法是使用DriveManager管理类:


步骤1:定义服务接口


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


public interface Driver {
    // 创建数据库连接
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    // 省略其他方法...
}
复制代码

步骤2:实现服务接口


服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:


  • mysqlcom.mysql.cj.jdbc.Driver.java


public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // 注册驱动
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    // 省略...
}
复制代码


  • oracleoracle.jdbc.driver.OracleDriver.java


public class OracleDriver implements Driver {
    private static OracleDriver defaultDriver = null;
    static {
        try {
            if (defaultDriver == null) {
                //1. 单例
                defaultDriver = new OracleDriver();
                // 注册驱动
                DriverManager.registerDriver(defaultDriver);
            }
        } catch (RuntimeException localRuntimeException) {
            ;
        } catch (SQLException localSQLException) {
            ;
        }
    }
    // 省略...
}
复制代码

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


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


com.mysql.cj.jdbc.Driver
复制代码

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


步骤4:加载服务


// DriverManager.java
static {
    loadInitialDrivers();
}
private static void loadInitialDrivers() {
    // 省略次要代码...
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 使用ServiceLoader遍历实现类
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 获得迭代器
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            // 迭代
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                    // 疑问:为什么没有任何处理?
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    // 省略次要代码...
}
复制代码


可以看到,DriverManager的静态代码块调用loadInitialDrivers (),方法内部通过ServiceLoader提供的迭代器Iterator<Driver> 遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?


while(driversIterator.hasNext()) {
    driversIterator.next();
    // 疑问:为什么没有任何处理?
}
复制代码


在下一节,我们深入ServiceLoader的源码来解答这个问题。


3. ServiceLoader 源码解析


# 提示 #

ServiceLoader中有一些源码使用了安全检测,如AccessController.doPrivileged(),在以下代码摘要中省略

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


// 1.
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    // 使用双亲委派模型中最顶层的ClassLoader
    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);
}
复制代码


可以看到,三个方法仅在传入的ClassLoader参数有区别,若还不了解ClassLoader,请务必阅读[《Java | 带你理解 ClassLoader 的原理与设计思想》](Editting...)

  • 构造方法


private final Class<S> service;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
private ServiceLoader(Class<S> svc, ClassLoader cl) { 
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    reload();
}
public void reload() {
    // 清空 providers
    providers.clear();
    // 实例化 LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}
复制代码


可以看到,ServiceLoader的构造器中创建了LazyIterator迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~


  • 外部迭代器


private LazyIterator lookupIterator;
// 返回一个新的迭代器,包装了providers和lookupIterator
public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
            // 优先从knownProviders取
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        public S next() {
            // 优先从knownProviders取
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}
复制代码


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

为什么要优先从providers集合中取元素呢?阅读源码发现,LazyIterator#next()会将每轮迭代中取到的元素putproviders集合中,providers其实是LazyIterator的内存缓存。


  • 内部迭代器
# 提示 #

以下代码摘要中省略了源码中的try-catch


// ServiceLoader.java
private static final String PREFIX = "META-INF/services/";
private class LazyIteratorimplements 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;
    }
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            // configs 未初始化才执行
            // 配置文件:META-INF/services/服务接口的全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 分析点1:解析配置文件资源
            pending = parse(service, configs.nextElement());
        }
        // nextName:下一个实现类的全限定名
        nextName = pending.next();
        return true;
    }
    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        // 1. 使用类加载器loader加载
        Class<?> c = Class.forName(cn, false, loader);
        if (!service.isAssignableFrom(c)) {
            ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
            fail(service, "Provider " + cn  + " not a subtype", cce);
        }
        // 2. 根据Class实例化服务实现类
        S p = service.cast(c.newInstance());
        // 3. 服务实现类缓存到 providers
        providers.put(cn, p);
        return p;
    }
    public boolean hasNext() {
        return hasNextService();
    }
    public S next() {
        return nextService();
    }
    public void remove() {
        throw new UnsupportedOperationException();
    }
}
// 分析点1:解析配置文件资源,实现类的全限定名列表迭代器
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();
}
复制代码


4.  ServiceLoader 要点


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


  • 约束
  • 服务实现类必须实现服务接口S,参见源码:if (!service.isAssignableFrom(c))
  • 服务实现类需包含无参的构造器,ServiceLoader将通过该无参的构造器来创建服务实现者的实例,参见源码:S p = service.cast(c.newInstance());
  • 配置文件需要使用UTF-8编码,参见源码:new BufferedReader(new InputStreamReader(in, "utf-8"));
  • 懒加载ServiceLoader使用“懒加载”的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例,参见源码:nextService()
  • 内存缓存ServiceLoader使用LinkedHashMap缓存创建的服务实现类实例,LinkedHashMap在二次迭代时会按照Map#put执行顺序遍历
  • 服务实现的选择当存在多个提供者时,服务消费者模块不一定要全部使用,而是需要根据某些特性筛选一种最佳实现。ServiceLoader的机制只能在遍历整个迭代器的过程中,从发现的实现类中决策出一个最佳实现,例如使用Charset.forName(String)获得Charset实现类:


// 服务接口
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;
    }
}
复制代码
  • ServiceLoader没有提供服务的注销机制服务实现类实例被创建后,它的垃圾回收的行为与Java中的其他对象一样,只有这个对象没有到GC Root的强引用,才能作为垃圾回收。


5. 问题回归


现在我们回到阅读DriverManager源码提出的疑问:


while(driversIterator.hasNext()) {
    driversIterator.next();
    // 疑问:为什么没有任何处理?
}
复制代码


为什么next()操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()的源码:


// ServiceLoader.java
// next() 直接调用 nextService()
private S nextService() {
    if (!hasNextService()) throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    // 1. 使用类加载器loader加载
    Class<?> c = Class.forName(cn, false, loader);
    if (!service.isAssignableFrom(c)) {
        ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
        fail(service, "Provider " + cn  + " not a subtype", cce);
    }
    // 2. 根据Class实例化服务实现类
    S p = service.cast(c.newInstance());
    // 3. 服务实现类缓存到 providers
    providers.put(cn, p);
    return p;
}
复制代码
  1. 使用类加载器loader加载:Class<?> c = Class.forName(cn, false, loader);这里传参使用false,类加载器将执行加载 -> 链接,不会执行初始化
  2. 根据 Class 实例化服务实现类 由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行


回过头看com.mysql.cj.jdbc.Driveroracle.jdbc.driver.OracleDriver源码,我们都发现了类似的静态代码块:


static {
    try {
        // 注册驱动
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
复制代码


可以看到,它们都调用了DriverManager#registerDriver注册了一个服务实现类实例,保存在CopyOnWriteArrayList中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?


6. 总结


  1. ServiceLoader基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化组件化的一种实现方式
  2. ServiceLoader是一个相对简易的框架,往往只在Java源码中使用,为了满足复杂业务的需要,一般会使用提供SPI功能的第三方框架,例如后台的Dubbo、客户端的ARouterWMRouter


在后面的文章中,我将与你探讨ARouterWMRouter的源码实现,欢迎关注彭旭锐的博客。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
16天前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
12天前
|
算法 Java
JAVA并发编程系列(8)CountDownLatch核心原理
面试中的编程题目“模拟拼团”,我们通过使用CountDownLatch来实现多线程条件下的拼团逻辑。此外,深入解析了CountDownLatch的核心原理及其内部实现机制,特别是`await()`方法的具体工作流程。通过详细分析源码与内部结构,帮助读者更好地理解并发编程的关键概念。
|
11天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
4天前
|
安全 Java 编译器
Java反射的原理
Java 反射是一种强大的特性,允许程序在运行时动态加载、查询和操作类及其成员。通过 `java.lang.reflect` 包中的类,可以获取类的信息并调用其方法。反射基于类加载器和 `Class` 对象,可通过类名、`getClass()` 或 `loadClass()` 获取 `Class` 对象。反射可用来获取构造函数、方法和字段,并动态创建实例、调用方法和访问字段。虽然提供灵活性,但反射会增加性能开销,应谨慎使用。常见应用场景包括框架开发、动态代理、注解处理和测试框架。
|
11天前
|
Java
Java的aop是如何实现的?原理是什么?
Java的aop是如何实现的?原理是什么?
15 4
|
15天前
|
存储 Java
JAVA并发编程AQS原理剖析
很多小朋友面试时候,面试官考察并发编程部分,都会被问:说一下AQS原理。面对并发编程基础和面试经验,专栏采用通俗简洁无废话无八股文方式,已陆续梳理分享了《一文看懂全部锁机制》、《JUC包之CAS原理》、《volatile核心原理》、《synchronized全能王的原理》,希望可以帮到大家巩固相关核心技术原理。今天我们聊聊AQS....
|
12天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
21天前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇
|
10天前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
15天前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
下一篇
无影云桌面