理解Java中的SPI机制

SPI,是JDK内置的一种服务发现机制。
可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

比如说在jdbc中,由Java程序提供java.sql.Driver这个接口,然后由不同地数据库厂商提供实现类,这是为了约束不同的厂商所写的数据库连接工具。
image.png

SPI机制就是在META-INF下的services文件夹下放一个以需要被实现的接口命名的txt文件,在文件里写上需要实现类的类名,这样SPI机制就可以发现找到实现类。
image

自己实现一个简单的SPI机制

首先准备一个接口作为标准服务接口提供给不同的厂商。
image
这里我在testSPi包下建立了一个Hell接口,里面有一个抽象方法sayHello
作为生产商提供两个接口的实现类
image
两个实现类中分别对sayHello方法进行了重写:
image
image
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类(就是我们刚刚编写的实现类)。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。

package com.hello.dream;

import testSpi.Hell;

import java.util.ServiceLoader;

public class Test {
    public static void main(String[] args) {
        ServiceLoader<Hell> serviceLoader = ServiceLoader.load(Hell.class);
        for (Hell hell : serviceLoader) {
            hell.sayHello();
        }
    }
}

ServiceLoader实现了Iterale,所以可以通过循环来获取所有实现Hell接口的类
运行结果:Hello Spi
                 Hello World

为什么要放在META-INF下的services中呢?

翻看源码可以找到这一段

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;

        Provider<T> nextProvider;
        ServiceConfigurationError nextError;

        LazyClassPathLookupIterator() { }

        /**
         * Parse a single line from the given configuration file, adding the
         * name on the line to set of names if not already seen.
         */
        private int parseLine(URL u, BufferedReader r, int lc, Set<String> names)
            throws IOException
        {
            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);
                int start = Character.charCount(cp);
                for (int i = start; i < n; i += Character.charCount(cp)) {
                    cp = ln.codePointAt(i);
                    if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                        fail(service, u, lc, "Illegal provider-class name: " + ln);
                }
                if (providerNames.add(ln)) {
                    names.add(ln);
                }
            }
            return lc + 1;
        }

定义了一个静态变量PREFIX,制定了从当前位置查找。
找到指定文件的类名后再通过反射中Class.forName()方法加载类对象。

posted on 2026-07-03 15:30  转生成为全栈开发者  阅读(4)  评论(0)    收藏  举报