SPI机制代码分析

SPI机制代码分析

在之前的文章里提到Spring Boot的类加载中有一种是SPI机制。但为了不冲淡主题,没有写SPI机制的详细代码分析。于是打算重新写一篇文章,把SPI机制的源码分析一遍。于我而言,是思考的总结,也是防止遗忘的备忘录。

SPI(Service Provider Interface)是 Java 提供的一种服务发现机制,它允许程序在运行时动态地为某个接口找到具体的实现类,而无需在代码中硬编码实现类名。下面我们追随代码,看看它的工作原理。 如下代码是LoggerFactory.java 类通过ServiceLoader加载具体日志处理类的入口

java 复制代码
LoggerFactory.java
private static ServiceLoader<SLF4JServiceProvider> getServiceLoader(final ClassLoader classLoaderOfLoggerFactory) {  
    ServiceLoader<SLF4JServiceProvider> serviceLoader;  
    SecurityManager securityManager = System.getSecurityManager();  
    if(securityManager == null) {  
        serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);  
     } else {  
        final PrivilegedAction<ServiceLoader<SLF4JServiceProvider>> action = () -> ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);  
        serviceLoader = AccessController.doPrivileged(action);  
    }  
    return serviceLoader;  
}

第6行,ServiceLoader.load函数实际是ServiceLoader对象的初始化函数,代码如下

java 复制代码
ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service,  
ClassLoader loader)  
{  
    return new ServiceLoader<>(Reflection.getCallerClass(), service, loader);  
}

得到ServiceLoader对象serviceLoader后,再获得serviceLoader的iterator对象(下文代码第18行)。通过iterator,循环实例化provider对象(下文代码第20行)

java 复制代码
LoggerFactory.java
static List<SLF4JServiceProvider> findServiceProviders() {  
    List<SLF4JServiceProvider> providerList = new ArrayList<>();  
  
    // retain behaviour similar to that of 1.7 series and earlier. More specifically, use the class loader that  
    // loaded the present class to search for services  
    final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();  
  
    SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);  
    if(explicitProvider != null) {  
        providerList.add(explicitProvider);  
        return providerList;  
    }  
  
  
    ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);  
  
    Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();  
    while (iterator.hasNext()) {  
        safelyInstantiate(providerList, iterator);  
    }  
    return providerList;  
}

下面来看看ServiceLoader的iterator函数

java 复制代码
public Iterator<S> iterator() {  
  
    // create lookup iterator if needed  
    if (lookupIterator1 == null) {  
        lookupIterator1 = newLookupIterator();  
    }  
  
    return new Iterator<S>() {  
  
        // record reload count  
        final int expectedReloadCount = ServiceLoader.this.reloadCount;  
  
        // index into the cached providers list  
        int index;  
  
       /**  
         * Throws ConcurrentModificationException if the list of cached  
         * providers has been cleared by reload.  
        */  
        private void checkReloadCount() {  
            if (ServiceLoader.this.reloadCount != expectedReloadCount)  
                throw new ConcurrentModificationException();  
        }  
  
        @Override  
        public boolean hasNext() {  
            checkReloadCount();  
            if (index < instantiatedProviders.size())  
                return true;  
            return lookupIterator1.hasNext();  
        }  
  
        @Override  
        public S next() {  
            checkReloadCount();  
            S next;  
            if (index < instantiatedProviders.size()) {  
                next = instantiatedProviders.get(index);  
            } else {  
                next = lookupIterator1.next().get();  
                instantiatedProviders.add(next);  
            }  
            index++;  
            return next;  
        }  
  
    };  
}

在第5行,新建了一个newLookupIterator对象,赋值给lookupIterator1。它包含了两个Iterator,如下文代码第6行第7行所示

java 复制代码
private Iterator<Provider<S>> newLookupIterator() {  
    assert layer == null || loader == null;  
    if (layer != null) {  
        return new LayerLookupIterator<>();  
    } else {  
        Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();  
        Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();  
        return new Iterator<Provider<S>>() {  
            @Override  
            public boolean hasNext() {  
                return (first.hasNext() || second.hasNext());  
            }  
            @Override  
            public Provider<S> next() {  
                if (first.hasNext()) {  
                    return first.next();  
                } else if (second.hasNext()) {  
                    return second.next();  
                } else {  
                    throw new NoSuchElementException();  
                }  
            }  
        };  
    }  
}

所以,调用iterator.hasNext()时,会调用lookupIterator1.hasNext(),最后调用代码return (first.hasNext() || second.hasNext());

first这个iterator,会在module的各个layer中去查找provider。如果查找不到,就使用second这个iterator,在各个classLoader的classpath中查找provider。由于Spring Boot只是兼容模块化,但是它本身并不适用模块化,故在first中是查找不到provider的。之后second.hasNext()就会被调用。second对象的类是LazyClassPathLookupIterator,它的hasNext()方法如下

java 复制代码
public boolean hasNext() {  
    if (acc == null) {  
        return hasNextService();  
    } else {  
        PrivilegedAction<Boolean> action = new PrivilegedAction<>() {  
        public Boolean run() { return hasNextService(); }  
    };  
    return AccessController.doPrivileged(action, acc);  
}

第3行,就是SPI查找及初始化查找出的provider的具体逻辑

java 复制代码
private boolean hasNextService() {  
    while (nextProvider == null && nextError == null) {  
        try {
            //查找provider类
            Class<?> clazz = nextProviderClass();  
            if (clazz == null)  
                return false;  
  
            if (clazz.getModule().isNamed()) {  
                // ignore class if in named module  
                continue;  
            }  
  
            if (service.isAssignableFrom(clazz)) {  
            // 初始化provider对象
                Class<? extends S> type = (Class<? extends S>) clazz;  
                Constructor<? extends S> ctor  
= (Constructor<? extends S>)getConstructor(clazz);  
                ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);  
                nextProvider = (ProviderImpl<T>) p;  
            } else {  
                fail(service, clazz.getName() + " not a subtype");  
            }  
        } catch (ServiceConfigurationError e) {  
            nextError = e;  
        }  
    }  
return true;  
}

第5行是调用nextProviderClass函数查找provider,第15行之后,是初始化provider对象。nextProviderClass函数的代码是:

java 复制代码
static final String PREFIX = "META-INF/services/";
private Class<?> nextProviderClass() {  
    if (configs == null) {  
        try {  
            String fullName = PREFIX + service.getName();  
            if (loader == null) {  
                configs = ClassLoader.getSystemResources(fullName);  
            } else if (loader == ClassLoaders.platformClassLoader()) {  
                // The platform classloader doesn't have a class path,  
                // but the boot loader might.  
                if (BootLoader.hasClassPath()) {  
                    configs = BootLoader.findResources(fullName);  
                } else {  
                    configs = Collections.emptyEnumeration();  
                }  
            } else {  
                configs = loader.getResources(fullName);  
            }  
        } catch (IOException x) {  
            fail(service, "Error locating configuration files", x);  
        }  
    }  
    while ((pending == null) || !pending.hasNext()) {  
        if (!configs.hasMoreElements()) {  
            return null;  
        }  
        pending = parse(configs.nextElement());  
    }  
    String cn = pending.next();  
    try {  
        return Class.forName(cn, false, loader);  
    } catch (ClassNotFoundException x) {  
        fail(service, "Provider " + cn + " not found");  
        return null;  
    }  
}

nextProviderClass函数的逻辑是,首先拼接fullName,因为loader为LaunchedClassLoader,故代码会进入第17行。调用LaunchedClassLoader的getResources函数。LaunchedClassLoader的getResources函数继承自ClassLoader,代码如下

java 复制代码
public Enumeration<URL> getResources(String name) throws IOException {  
    Objects.requireNonNull(name);  
    @SuppressWarnings("unchecked")  
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];  
    if (parent != null) {  
        tmp[0] = parent.getResources(name);  
    } else {  
        tmp[0] = BootLoader.findResources(name);  
    }  
    tmp[1] = findResources(name);  
  
    return new CompoundEnumeration<>(tmp);  
}

从第五六行可以看出,会递归查找所有父类和当前类的module和classpath中是否有该文件。以日志系统为例,传入的类是SLF4JServiceProvider,即代码serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);那么查找的文件fullName就是META-INF/services/org.slf4j.spi.SLF4JServiceProvider,最终查找到的文件为nested:/磁盘路径/项目路径/项目名/target/firstWebProject-0.0.1-SNAPSHOT.jar/!BOOT-INF/lib/logback-classic-1.5.18.jar!/META-INF/services/org.slf4j.spi.SLF4JServiceProvider。再读取org.slf4j.spi.SLF4JServiceProvider文件中的值,根据这个值加载class。最终加载的provider就是ch.qos.logback.classic.spi.LogbackServiceProvider。 最后贴一张图,看看该文件在jar包中的样子。

当我们要更换provider时,只需要调整文件里的内容,就可以了。这样就做到了解耦。

相关推荐
KNeeg_2 小时前
黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐
java·redis·后端·spring·面试·职场和发展·黑马点评
牛奶2 小时前
开发者的"奇技淫巧":那些让你效率翻倍的实战技巧
前端·后端·程序员
泉城老铁2 小时前
springboot实现word转换pdf
vue.js·后端
铁皮饭盒2 小时前
今天你会学到这些关键词
前端·后端
oil欧哟2 小时前
🤔 很长时间没写文章了,分享一下最近的一些思考
前端·后端
Walter先生2 小时前
MCP行情数据接入配置踩坑全记录:从Claude Code到Zed八大客户端适配实战
后端·websocket·架构·实时行情数据源
2401_878820473 小时前
Sa-Token基础篇
java·spring boot·后端·sa-token
覆东流3 小时前
第10天:python元组
开发语言·后端·python
万事大吉CC3 小时前
【5】Django 的模板语言:页面架构设计
后端·python·django