写点什么

JDK 中「SPI」原理分析

作者:知了一笑
  • 2023-08-05
    浙江
  • 本文字数:3435 字

    阅读完需:约 11 分钟

JDK中「SPI」原理分析

基于【JDK1.8】

一、SPI 简介

1、概念

SPI 即service-provider-interface的简写;


JDK 内置的服务提供加载机制,可以为服务接口加载实现类,解耦是其核心思想,也是很多框架和组件的常用手段;

2、入门案例

2.1 定义接口

就是普通的接口,在 SPI 的机制中称为【service】,即服务;


public interface Animal {    String animalName () ;}
复制代码

2.2 两个实现类

提供两个模拟用来测试,就是普通的接口实现类,在 SPI 的机制中称为【service-provider】即服务提供方;


CatAnimal实现类;


public class CatAnimal implements Animal {    @Override    public String animalName() {        System.out.println("Cat-Animal:布偶猫");        return "Ragdoll";    }}
复制代码


DogAnimal实现类;


public class DogAnimal implements Animal {    @Override    public String animalName() {        System.out.println("Dog-Animal:哈士奇");        return "husky";    }}
复制代码

2.3 配置文件

文件目录:在代码工程中创建META-INF.services文件夹;


文件命名:butte.program.basics.spi.inf.Animal,即全限定接口名称;


文件内容:添加相应实现类的全限定命名;


butte.program.basics.spi.impl.CatAnimalbutte.program.basics.spi.impl.DogAnimal
复制代码

2.4 测试代码

通过ServiceLoader加载配置文件中指定的服务实现类,然后遍历并调用Animal接口方法,从而执行不同服务提供方的具体逻辑;


public class SpiAnaly {    public static void main(String[] args) {        ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);        Iterator<Animal> animalIterator = serviceLoader.iterator();        while(animalIterator.hasNext()) {            Animal animal = animalIterator.next();            System.out.println("animal-name:" + animal.animalName());        }    }}
复制代码


结果输出


Cat-Animal:布偶猫 \n animal-name:ragdollDog-Animal:哈士奇 \n animal-name:husky
复制代码

二、原理分析

1、ServiceLoader 结构

很显然,分析 SPI 机制的原理,从ServiceLoader源码中load方法切入即可,但是需要先从核心类的结构开始分析;


public final class ServiceLoader<S> implements Iterable<S> {    // 配置文件目录    private static final String PREFIX = "META-INF/services/";    // 表示正在加载的服务的类或接口    private final Class<S> service;    // 类加载器用来定位,加载,实例化服务提供方    private final ClassLoader loader;    // 创建ServiceLoader时采用的访问控制上下文    private final AccessControlContext acc;    // 按实例化的顺序缓存服务提供方    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();    // 惰性查找迭代器    private LazyIterator lookupIterator;    /**     * service:表示服务的接口或抽象类     * loader: 类加载器     */    public static <S> ServiceLoader<S> load(Class<S> service) {        ClassLoader cl = Thread.currentThread().getContextClassLoader();        return ServiceLoader.load(service, cl);    }    /**     * ServiceLoader构造方法     */    private ServiceLoader(Class<S> svc, ClassLoader cl) {        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;        reload();    }    public void reload() {        providers.clear();        // 实例化迭代器        lookupIterator = new LazyIterator(service, loader);    }    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {        return new ServiceLoader<>(service, loader);    }
private class LazyIterator implements Iterator<S> { // 服务接口 Class<S> service; // 类加载器 ClassLoader loader; // 实现类URL Enumeration<URL> configs = null; // 实现类全名 Iterator<String> pending = null; // 下个实现类全名 String nextName = null; }}
复制代码


断点截图:


2、iterator 迭代方法

ServiceLoader类的迭代器方法中,实际使用的是LazyIterator内部类的方法;


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

3、hasNextService 方法

从上面迭代方法的源码中可知,最终执行的是LazyIterator#hasNextService判断方法,该方法通过解析最终会得到实现类的全限定名称;


private class LazyIterator implements Iterator<S> {    private boolean hasNextService() {        // 1、拼接名称        String fullName = PREFIX + service.getName();        // 2、加载资源文件        configs = loader.getResources(fullName);        // 3、解析文件内容        pending = parse(service, configs.nextElement());        nextName = pending.next();        return true;    }}
复制代码


断点截图:


4、nextService 方法

迭代器的next方法最终执行的是LazyIterator#nextService获取方法,会基于上面hasNextService方法获取的实现类全限定名称,获取其Class对象,进而得到实例化对象,缓存并返回;


private class LazyIterator implements Iterator<S> {    private S nextService() {        // 1、通过全限定命名获取Class对象        String cn = nextName;        Class<?> c = Class.forName(cn, false, loader);        // 2、实例化对象        S p = service.cast(c.newInstance());        // 3、放入缓存并返回该对象        providers.put(cn, p);        return p;    }}
复制代码


断点截图:


三、SPI 实践

1、Driver 驱动接口

在 JDK 中提供了数据库驱动接口java.sql.Driver,无论是 MySQL 驱动包还是 Druid 连接池,都提供了该接口的实现类,通过 SPI 机制可以加载到这些驱动实现类;


public class DriverManager {    private static void loadInitialDrivers() {        AccessController.doPrivileged(new PrivilegedAction<Void>() {            public Void run() {                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);                Iterator<Driver> driversIterator = loadedDrivers.iterator();            }        });    }}
复制代码


断点截图:


2、Slf4j 日志接口

SLF4J 是门面模式的日志组件,提供了标准的日志服务SLF4JServiceProvider接口,在LogFactory日志工厂类中,负责加载具体的日志实现类,比如常用的 Log4j 或 Logback 日志组件;


public final class LoggerFactory {    static List<SLF4JServiceProvider> findServiceProviders() {        // 服务加载        ClassLoader classLoaderOfLoggerFactory = org.slf4j.LoggerFactory.class.getClassLoader();        // 重点看该方法:【getServiceLoader()】        ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);        // 迭代方法        List<SLF4JServiceProvider> providerList = new ArrayList();        Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();        while(iterator.hasNext()) {            safelyInstantiate(providerList, iterator);        }        return providerList;    }}
复制代码


断点截图:


四、参考源码

文档仓库:https://gitee.com/cicadasmile/butte-java-note
应用仓库:https://gitee.com/cicadasmile/butte-flyer-parent
复制代码


发布于: 4 小时前阅读数: 14
用户头像

知了一笑

关注

公众号:知了一笑 2020-04-08 加入

源码仓库:https://gitee.com/cicadasmile

评论

发布
暂无评论
JDK中「SPI」原理分析_Java_知了一笑_InfoQ写作社区