彻底搞懂JAVA SPI

之前做项目的时候,用到了LiteFlow规则引擎,LiteFlow中支持了标准关系型结构化数据库的配置源,使用后发现基于这种方式启动项目时LiteFlow会通过以下固定的SQL去加载相关组件

不难发现,这两个SQL加载了同一租户的所有数据,做开发的都知道,我们更多的情况是去加载有效数据(非逻辑删除的数据),虽然在后续的版本中作者也增加了相应的字段来过滤有效数据,但是有的公司数据库通用字段都是统一的(如逻辑删除标志位、创建人信息等等),不满足我们公司的场景,通过阅读源码后发现作者提供了解析器SPI插件接口,然后通过重写了SQL解析器,可以看到现在的SQL满足了要求

通过该案例,主要是为了引出分享的主要内容:Java SPI机制

大家平时开发的时候有留意的话,就会发现我们使用的框架中有大量使用了java SPI技术(JDBC加载不同类型的数据库驱动、SLF4J加载不同提供商的日志实现类、Spring、Dubbo等等)

SPI (Service Provider Interface)是JDK内置的一种服务提供发现机制,这种机制通常用于插件架构或模块化系统,使得开发者可以轻松地添加或替换系统中的组件。简单来说,接口的定义(调用方提供)与具体实现是隔离的(服务提供方提供),使用接口的实现类需要依赖某种服务发现机制

这里我们不得不提一下API

API (Application Programming Interface) 服务提供方提供的接口与其实现方法

JAVA SPI机制实现

我们平时上下班通过打卡机的时候,打卡机可能是指纹打卡、人脸识别打卡等等。

  • 定义接口
java 复制代码
public interface Clock {

    public void clock();
    
}
  • 指纹打卡
java 复制代码
public class FingerprintClock implements Clock{

    @Override
    public void clock() {

        System.out.println("//// 指纹打卡");

    }
}
  • 人脸识别打卡
java 复制代码
public class FaceRecognitionClock implements Clock{
    @Override
    public void clock() {
    
        System.out.println("//// 人脸识别打卡");
        
    }
}
  • resources下新建META-INF/services/目录能放到别的目录下吗?,新建接口全限定名的文件:com.tx.general.spi.Clock,里面加上需要用到的实现类(我这里配了指纹打卡实现类)
  • 使用ServiceLoader.load()加载接口
java 复制代码
/**
 * 通过ServiceLoader获取所有实现了Clock接口的时钟对象,并依次调用它们的clock方法
 */
public static void main(String[] args) {
    // 创建ServiceLoader类对象
    ServiceLoader<Clock> serviceLoaders = ServiceLoader.load(Clock.class);
    // 返回LazyIterator迭代器的匿名迭代器对象
    Iterator<Clock> it = serviceLoaders.iterator();
    while (it.hasNext()){
        Clock clock = it.next();
        clock.clock();
    }
}

可以看到输出的结果是我们预期的,但是调用方式和平时我们调用方法有所不同,通过ServiceLoader类使用Java的类加载器机制从META-INF/services/目录下加载和实例化服务提供者,完成了对方法的调用。

JAVA SPI机制原理

前面提到了通过ServiceLoader类使用Java的类加载器机制从META-INF/services/目录下加载和实例化服务提供者,我们主要分析ServiceLoader类

load(Class<S> svc, ClassLoader cl)方法

java 复制代码
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();


public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    // 创建一个ServiceLoader对象
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // loader如果为空,那么就使用ClassLoader.getSystemClassLoader(),即系统类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    // providers是一个Map结构的链表LinkedHashMap,clear()清空了缓存,这里是什么时候有值的呢?
    providers.clear();
    // 创建一个Lazy懒加载形式的迭代器
    lookupIterator = new LazyIterator(service, loader);
}

该方法创建一个可加载接口服务提供者实例的ServiceLoader类对象,其内部创建一个具有延迟加载功能的迭代器LazyIterator(实现了Iterator迭代器接口

iterator()方法

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;
                // lookupIterator,前文创建的LazyIterator迭代器对象的引用
                return lookupIterator.hasNext();
            }

            /**
             * 获取下一个元素。
             */
            public S next() {
                // 判断缓存是否为空
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // lookupIterator,前文创建的LazyIterator迭代器对象的引用
                return lookupIterator.next();
            }
        };
    }

该方法创建一个实现了Iterator接口的匿名内部类实例对象,并返回该实例对象作为一个迭代器。 hasNext()方法调用LazyIterator.hasNextService()方法,next()方法调用LazyIterator.nextService()方法

LazyIterator.hasNextService()方法

java 复制代码
private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
        
            // 拼接文件路径
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                 // 通过类加载器读取该文件资源
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
         // 解析读取配置文件类名,将读取到的类名存储到ArrayList,包装成iterator返回
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

可以看到在这方法里面我们去读取了META-INF/services/下的文件,并将service实现类的名字列表返回

LazyIterator.nextService()方法

java 复制代码
        /**
         * 获取下一个服务提供者
         * 如果没有下一个服务提供者,则抛出`NoSuchElementException`异常
         * 
         * @return 下一个服务提供者
         */
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            
            // 获取下一个服务提供者的类名
            String cn = nextName;
            nextName = null;
            
            Class<?> c = null;
            try {
                // 根据类名获取Class对象
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                // 类名未找到,抛出异常
                fail(service, "Provider " + cn + " not found");
            }
            
            // 检查服务提供者是否是所指定的服务的子类型
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn + " not a subtype");
            }
            
            try {
                // 根据Class对象创建服务提供者的实例
                S p = service.cast(c.newInstance());
                // 将实例添加到缓存中
                providers.put(cn, p);
                // 返回服务提供者实例
                return p;
            } catch (Throwable x) {
                // 创建实例失败,抛出异常
                fail(service, "Provider " + cn + " could not be instantiated", x);
            }
            
            // 不应该执行到这里,抛出`Error`异常
            throw new Error();  
        }

通过反射方法Class.forName()加载类对象,并用newInstance()方法将类实例化,把实例化后的类缓存到providers对象中,到此,我们就搞懂了java SPI的原理

总结

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。但是我们也看到了通过ServiceLoader类将接口的实现类全部加载并实例化一遍。

相关推荐
Asthenia04122 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz96520 分钟前
ovs patch port 对比 veth pair
后端
Asthenia041230 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom44 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫