Java SPI 机制

Java SPI 机制

什么是SPI机制

在Java生态系统中,SPI(Service Provider Interface)是一种服务发现机制,它允许第三方为一个接口或抽象类提供具体的实现。这种机制广泛应用于框架和库的设计中,为开发者提供了极大的灵活性和扩展性。

SPI机制是Java平台的一个特性,用于将接口实现类的全限定名配置在文件中,并由JDK提供的API去加载这些实现类。这是一种解耦合的方式,通过定义服务提供者接口,然后让第三方来实现这个接口,最终达到服务动态查找的目的。

SPI机制主要原理

SPI的核心在于 java.util.ServiceLoader类,它是用来加载服务提供者的工厂类。当程序需要某个接口的具体实现时,可以通过 ServiceLoader加载配置文件中指定的实现类。配置文件通常位于 META-INF/services/目录下,文件名即为接口的全限定名,文件内容则是实现类的全限定名。

以获取数据库驱动为例,当初次调用 DriverManager.getConnection(url); 时会初始化 DriverManager 类,在其中存在如下静态代码块:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

其中, loadInitialDrivers() 方法中存在如下代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

// ......

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}

其实现核心为第一行代码,这里会通过SPI机制加载服务提供者配置的实现类,并创建为 Driver.class 的实现类。内部代码如下:

    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // cl为当前线程获取到的类加载器,此时不为null,因此loader即为当前线程的类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        // 默认情况下 System.getSecurityManager() 返回null,此时 acc为null
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

至此,第一行代码执行完成。随后,调用前一行代码返回的 loadedDrivers 获取迭代器。其中 iterator() 方法实现如下:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        // 这里为knownProviders赋值,初始状态下为空
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 初次调用时,knownProviders为空,此时返回lookupIterator.hasNext()
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            // 初次调用时,knownProviders为空,此时返回lookupIterator.next()
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

随后,系统调用 driversIterator.hasNext()driversIterator.next() 两个方法。如上代码注释,初次调用时,knownProviders为空,此时返回 lookupIterator.hasNext()

private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            // 初次调用时,nextName为null
            if (nextName != null) {
                return true;
            }
            // 初次调用时,configs为null
            if (configs == null) {
                try {
                    // PREFIX 为 ServiceLoader中定义的前缀,值为 META-INF/services/
                    // PREFIX + service.getName() 为 META-INF/services/ + 接口名
                    // 在加载数据库驱动时,这里读取的是 META-INF/services/java.sql.Driver
                    String fullName = PREFIX + service.getName();
                    // loader 为前面传进来的当前线程的类加载器,不为null
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        // 根据提供的信息获取资源文件
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            // 初次执行时,pending为null
            while ((pending == null) || !pending.hasNext()) {
                // 初次执行时不会进入if代码块
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 调用ServiceLoader中的parse方法
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        // 初次调用时会执行这里的代码
        public boolean hasNext() {
            // 分析上文可知,此处进入if。方法调用 hasNextService()
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
}
  • ServiceLoader#parse()
    //  ServlceLoader中的parse方法
    private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            // 根据传入的文件资源获取输入流
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));

            // 解析输入流
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        // 在执行完解析后,返回一个迭代器,迭代器中包含解析后的结果
        return names.iterator();
    }

    // Parse a single line from the given configuration file, adding the name
    // on the line to the names list.
    // 
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        // 按行读取文件内容
        // 在mysql驱动中为 com.mysql.cj.jdbc.Driver
        String ln = r.readLine();
        // 执行一系列校验和处理
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 如果该类没有被加载过,则加入到names中
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

至此,前面 driversIterator.hasNext() 相关代码执行完毕。随后调用 driversIterator.next(); 代码实现如下:


        private S nextService() {
            // nextName为前面获取的数据,不为null
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            // 通过反射获取接口实现类的字节码对象
            Class<?> c = null;
            try {
                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 {
                // 创建实例并检查是否派生
                S p = service.cast(c.newInstance());
                // 将创建的实例缓存到 providers 中
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

至此,整个通过SPI机制获取实现类的过程结束。

SPI机制在spring中的应用

在springboot环境下,创建一个starter时需要在 src/main/resources/META-INF/ 目录下添加 spring.factories 文件,在这个文件中可以配置当前starter的启动配置类。以 druid-spring-boot-starter 为例,其配置文件如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

这里的实现类似于SPI机制,在文件中定义了druid需要注入的配置类,区别在于这里上层为注解,springboot实现了自己的扫描机制,且配置文件统一命名为 spring.factories

类似文章