JDK中「SPI」原理分析

基于【JDK1.8】

一、SPI简介

1、概念

SPI即service-provider-interface的简写;

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

2、入门案例

2.1 定义接口

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

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

2.2 两个实现类

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

CatAnimal实现类;

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

DogAnimal实现类;

java 复制代码
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,即全限定接口名称;

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

java 复制代码
butte.program.basics.spi.impl.CatAnimal
butte.program.basics.spi.impl.DogAnimal

2.4 测试代码

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

java 复制代码
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:ragdoll
Dog-Animal:哈士奇 \n animal-name:husky

二、原理分析

1、ServiceLoader结构

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

java 复制代码
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内部类的方法;

java 复制代码
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判断方法,该方法通过解析最终会得到实现类的全限定名称;

java 复制代码
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对象,进而得到实例化对象,缓存并返回;

java 复制代码
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机制可以加载到这些驱动实现类;

java 复制代码
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日志组件;

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

断点截图:

四、参考源码

ruby 复制代码
文档仓库:
https://gitee.com/cicadasmile/butte-java-note

应用仓库:
https://gitee.com/cicadasmile/butte-flyer-parent
相关推荐
Rust研习社1 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒1 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro2 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH3 小时前
Koa框架的使用
后端
luckdewei4 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某5 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy5 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom5 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github