深入理解java中的SPI机制

1. 开始感受SPI机制的作用

SPI(Service Provider Interface),翻译成为中文叫做"服务提供接口"。它的作用是什么呢?它的作用其实就是根据一个接口,找到项目中要使用的实现类,用户可以根据实际需要启用或者是扩展原来的默认策略。

下面先以jdk提供的ServiceLoader这个类库去进行举例吧。

通过ServiceLoader.load方法中传入指定的接口,它会加载这个接口下的指定实现类,那么这个指定实现类具体是指什么?需要在模块的resources/META-INF/services/下创建一个文件,文件名为接口的全类名,文件中的内容为需要通过SPI拿到的实现类的全类名(可以指定多个,每行一个)。

我们编写如下这样一个接口

public interface SPIService {
    public void serve();
}

再编写两个它的实现类吧

public class SPIServiceImpl1 implements SPIService {
    @Override
    public void serve() {
        System.out.println("SPIServiceImpl1 is serve...");
    }
}
//.....................................分隔符..........................................
public class SPIServiceImpl2 implements SPIService {
    @Override
    public void serve() {
        System.out.println("SPIServiceImpl2 is serve...");
    }
}

resources目录下建立META-INF/services/目录,并创建一个文件com.wanna.spi.SPIService指定的是服务接口的全类名,在文件中配置的内容是这个接口的类的实现类com.wanna.spi.SPIServiceImpl1com.wanna.spi.SPIServiceImpl2

24412352ccf0bbed551b74b9.png

预备工作完成,下面我们要做的,就是使用SPI机制,获取这两个实现类?编写如下的测试代码:

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader serviceLoader = ServiceLoader.load(SPIService.class);
        serviceLoader.stream().forEach(e -> e.get().serve());
    }
}

得到运行结果:

SPIServiceImpl1 is serve...
SPIServiceImpl2 is serve...

也就是说,通过ServiceLoader,成功地加载到了我们编写的两个实现类。

2. 开始了解ServiceLoader的源码?

直接打开ServiceLoader.load方法的源码

    public static  ServiceLoader load(Class service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }

获取当前线程的上下文加载器、调用方类和Service类,从而去创建一个ServiceLoader对象,然后在构造器中就没做什么。

接着我们主要关心它的迭代器的创建:

    public Iterator iterator() {

        // create lookup iterator if needed
        if (lookupIterator1 == null) {
            lookupIterator1 = newLookupIterator();
        }

        return new Iterator() {

            // 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;
            }

        };
    }

最主要的组件在于它创建的lookupIterator1字段:

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

主要创建了两个迭代器,主要关注LazyClassPathLookupIterator,我们就可以猜测它是负责加载META-INF/services/下的Provider的加载。

    private final class LazyClassPathLookupIterator
        implements Iterator>
    {
        static final String PREFIX = "META-INF/services/";

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

        Provider nextProvider;
        ServiceConfigurationError nextError;
        //---------------------------------------------------------------------

        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;
            }
        }
  }

我们还可以看到,最终就是使用ClassLoader.getResources去完成的相关实现类的URL的获取,最后保存到pending队列中,然后使用Class.forName返回一个加载到的Class。

3. SPI机制在JdbcDriver中的使用?

我们打开mysql的驱动jar包,我们发现也是在META-INF/services/下存放了一个java.sql.Driver文件,里面放入了一个Driver的实现类com.mysql.cj.jdbc.Driver

image.png

DriverManager.getDriver方法中,会有如下一步,确保Driver被初始化完成。

public static Driver getDriver(String url) throws SQLException {
        ensureDriversInitialized();
        //-------------以下代码省略-------------------
}

在这个方法中有如下两行代码,就是使用到的ServiceLoader去进行加载Driver的实现类。

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

4. SPI机制在Spring中的使用?

在Spring Framework的spring-web模块中,有如下的文件,主要是用来找到Spring的Servlet容器的初始化器。

image.png

我们可以手写一个SpringServletContainerInitializer,Spring容器在启动的过程中,就会回调它的onStartup方法,从而完成Servlet等配置工作,这个过程也就是传统的SpringMVC的注解版的使用方法。

public class MyMvcStarter implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //Spring会给我们传入servletContext

        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        DispatcherServlet servlet = new DispatcherServlet(context);
        final ServletRegistration.Dynamic app = servletContext.addServlet("app", servlet);
        app.setLoadOnStartup(1);
        app.addMapping("/");  //指定servlet的映射路径
    }
}

还有一个典型应用是在SpringBoot中自定义场景启动器时,需要在META-INF/目录下配置一个spring.factories文件,去进行相关的配置从而往容器中添加合适的组件,不过这个使用的不是ServiceLoader,而是自定义了一个路径,并且自定义了自己的加载器去完成相关组件的加载。

这个可以参考我的另外一篇帖子:SpringBoot自定义场景启动器(starter)

5. SPI机制在其它地方的使用?

其实很多地方都有使用到SPI机制,比如日志组件、Dubbo、jar包方式进行gradle插件的导入等。

通过jar包导入gradle插件需要的步骤:

  • 1.在jar包的META-INF/gradle-plugins/路径下存在一个xxx.properties的文件,这个xxx.properties文件中有如下内容:implementation-class=xxx.xxx.xxx(真正插件的主类名)。
  • 2.在buildscript中引入jar包的gav坐标。
  • 3.使用apply plugin 'xxx'引用该gradle插件。

你可能感兴趣的:(深入理解java中的SPI机制)