saowu's Blog

Java SPI机制

Java SPI机制
2023-05-24 · 7 min read
Java

A service is a well-known set of interfaces and (usually abstract) classes. A service provider(SPI) is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application's class path or by some other platform-specific means.

SPI 机制介绍

SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。本文介绍了 Java SPI 机制以及在模块化非模块化项目中的实现方式(此处的模块化指 Java9 引入的模块化)。

SPI 和 API 的区别

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现,可用于启用框架扩展和可替换组件。 从使用人员上来说,SPI 被框架扩展人员使用。

SPI 的约定

服务提供方需要通过一些约定告诉系统自己所提供的服务的位置,Java9 之后一共有两种约定方式:

  • 非模块化:通过在 META-INF/services/ 目录下配置相关文件实现;
  • 模块化:通过在模块化文件 module-info.java 中添加导出语句指定服务位置实现;
package java.util;

public final class ServiceLoader<S> implements Iterable<S>{
    private final class LazyClassPathLookupIterator<T> implements Iterator<Provider<T>>{
        static final String PREFIX = "META-INF/services/";

        Set<String> providerNames = new HashSet<>();  // to avoid duplicates
        Enumeration<URL> configs;
        Iterator<String> pending;

        //.........
    }
}
package java.lang;

public final class Module implements AnnotatedElement {

private Class<?> loadModuleInfoClass() {
        Class<?> clazz = null;
        try (InputStream in = getResourceAsStream("module-info.class")) {
            if (in != null)
                clazz = loadModuleInfoClass(in);
        } catch (Exception ignore) { }
        return clazz;
    }

    /**
     * Loads module-info.class as a package-private interface in a class loader
     * that is a child of this module's class loader.
     */
    private Class<?> loadModuleInfoClass(InputStream in) throws IOException {
        final String MODULE_INFO = "module-info";
        // . . .
    }

}

SPI 原理

  • 类全称限定名的获取,即知道哪些类是服务提供者;
  • 类加载,把获取到的类加载到内存中,涉及上下文类加载器;

类加载

获取到 SPI 服务实现类的文件之后,就可以使用类加载器将对应的类加载到内存中, 问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的双亲委派模型无法解决这个问题。所以 java 采用了线程上下文类加载器。破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,从而实现 SPI 服务的加载。线程上下文类加载器的实现如下:

  • 在 ThreaLocal 中通过 setContextClassLoader​(ClassLoader cl)存储当前线程中的类加载器,默认为 AppClassLoader。
  • Java 核心库中的程序在需要加载 SPI 实现类的时候,会首先通过 ThreaLocal 中的 getContextClassLoader​(ClassLoader cl)方法获取上下文类加载器,然后通过该类加载器加载 SPI 的实现类。
// ServiceLoader 是JDK用于加载 SPI 服务实现类的工具,可以处理 0 个、1 个或者多个服务提供商的情况。
package java.util;

public final class ServiceLoader<S> implements Iterable<S>{
    @CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }
}

Spring SPI是和JDK SPI的原理相同

Spring中使用的类是SpringFactoriesLoader,文件路径不同 spring配置放在 META-INF/spring.factories中:

package org.springframework.core.io.support;

public final class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
    private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap();

}

Dubbo SPI 设计理念

Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

用户能够基于 Dubbo 提供的扩展能力,很方便基于自身需求扩展其他协议、过滤器、路由等。下面介绍下 Dubbo 扩展能力的特性。

  • 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用扩展类实例化,减少资源浪费。
  • 增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。
  • 增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。
  • 具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。
  • 可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
  • 提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。

Dubbo SPI 概述

参考链接

Copyright © 2020 - 2024 saowu. All Right Reserved
Powered by Gridea