深入了解Spring与Java的SPI机制

SPI(service provider interface)机制是JDK内置的一种服务发现机制,可以动态的发现服务

JDK的SPI机制

jdk 的 spi 机制是通过 ServiceLoader 类来加载扩展的服务提供类

为什么需要这种机制呢?

大家可以从类加载器文章中双亲委派的破坏了解原因
双亲委派模型破坏

原因

简单的说,类加载器中的双亲委派模型的工作原理是对于类的加载,先交给父类加载器完成,如果父类加载器无法完成加载,子类加载器才进行加载。JDK中的一些基础类接口,例如 JDBC,如果按照双亲委派模型,JDBC只能用启动类加载器完成驱动的加载,但是驱动的实现是在用户目录的 jar 包实现,所以启动类加载器就无法加载用户的目录的驱动类了(下面可以从代码实例了解一下)

ServiceLoader 的源码中的介绍如下

A service is a well-known set of interfaces and (usually abstract) classes. 
A service provider 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.

大体上的意思是

服务是一组众所周知的接口和类(通常是抽象的)。
服务提供者是一个服务的特定实现。提供者中的类通常是实现服务接口
和子类的具体实现。服务提供者可以以扩展的形式在 Java 平台中实现,
也就是放在任何常用jar文件扩展目录中。提供者也可以通过将它们添加
到应用程序的 classpath 或其他特定于平台的方法。

ServiceLoader 的作用是通过接口或抽象类,找到 class path 的 META-INF/services/ 路径下对应的文件,把文件中描述全路径的实现类(服务提供者)加载起来

案例分析

SpiInterface接口

package spi;

public interface SpiInterface {
    String getInfo();
}

文件路径,文件命名是接口的路径

在这里插入图片描述

spi.SpiInterface文件内容为两个接口的实现类

spi.SpiInfo
spi.SpiMessage

SpiInfo 和 SpiMessage实现类

package spi;

public class SpiInfo implements SpiInterface {

    @Override
    public String getInfo() {
        return "SpiInfo";
    }
}
package spi;

public class SpiMessage implements SpiInterface {
    @Override
    public String getInfo() {
        return "SpiMessage";
    }
}

再来看看实际使用的输出结果

package spi;

import java.util.ServiceLoader;

public class Main {

    public static void main(String[] args) {
        ServiceLoader<SpiInterface> loader = ServiceLoader.load(SpiInterface.class);
        for (SpiInterface spi : loader) {
            System.out.println(spi.getInfo());
        }
        // 输出:
        // SpiInfo
        // SpiMessage
    }
}

通过 JDK 提供的 SPI 机制,就可以通过 SpiInterface 接口和实现类文件的描述,把具体的实现类加载了

SPI 与 双亲委派模型破坏

这种 SPI 机制又与类加载器的双亲委派模型破坏有什么关系的(大家可以想一下)

SPi 机制对于双亲委派模型的破坏在于增加了线程上下文类加载器,它的作用是可以逆向加载,例如需要用启动类加载器目录下的类,可以通过线程上下文类加载器来加载

假如没有线程上下文类加载器,加载的方式如下

package spi;

public class Main {

    public static void main(String[] args) throws Exception {
    		// 假如 SpiInterface 在 rt.jar 中,那么类加载器是启动类加载器,
    		// 启动类加载器是无法加载用户路径("spi.SpiInfo")的类的
        Class<SpiInterface> spiInfo = (Class<SpiInterface>) 
        			SpiInterface.class.getClassLoader().loadClass("spi.SpiInfo");
        System.out.println(spiInfo.newInstance().getInfo());
    }
}

但自从有了线程上下文类加载器,加载方式可以如下

package spi;

public class Main {

    public static void main(String[] args) throws Exception {
        Class<SpiInterface> spiInfo = (Class<SpiInterface>) 
        		Thread.currentThread().getContextClassLoader().loadClass("spi.SpiInfo");
        System.out.println(spiInfo.newInstance().getInfo());
        // 无论 SpiInterface 在哪个路径,线程上下文类加载器都可以加载类
        // 完美输出:SpiInfo
    }
}

这样大家是不是对于 JDK 的 SPi 机制很熟悉了呢,如果对于类加载过程与类加载器还不熟悉的,建议看看上面链接的文章(重点在于类加载,类加载器),肯定大有收获

Sping 的 SPi 机制

介绍完了 JDK 的 SPi 机制,我们还可以在 Sping 中发现这种机制的应用,Spring 的 SPi 机制可以把 spring.factories 文件中的类实例化,注册到 Spring 容器中,该机制在 Sping 是这样应用的:

  1. 服务:接口或抽象类

  2. 服务提供者:实现服务接口的实现类

  3. 服务与服务提供者的文件描述:在 class path 的 META-INF/spring.factories 文件中,该文件可以在任意 jar 包存在,所以不同的 jar 包不会存在冲突,spring 启动的时候会扫描所有 jar 包的 spring.factories 文件

  4. 加载类:SpringFactoriesLoader(相当于 JDK 的 ServiceLoader)

源码分析

从 SpringFactories 的源码可以看出

public abstract class SpringFactoriesLoader {
	/**
	 * The location to look for factories.
	 * 

Can be present in multiple JAR files. */ public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; /** * @param factoryClass 工厂类接口或抽象类 * @param classLoader 类加载器 * @return 实现 factoryClass 的所有实例 */ public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) { Assert.notNull(factoryClass, "'factoryClass' must not be null"); ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse); if (logger.isTraceEnabled()) { logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames); } List<T> result = new ArrayList<T>(factoryNames.size()); for (String factoryName : factoryNames) { result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse)); } AnnotationAwareOrderComparator.sort(result); return result; } }

其中 spring.factories 是这样定义的,实现类通过逗号分隔

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration

org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration

dubbo 的 SPI 机制

有兴趣的还可以了解 dubbo 的 spi 机制,来自 Dubbo 官方文档

Dubbo SPI 实现了一套功能更强的 SPI 机制

  1. 通过键值对方式进行配置,这样可以按需加载指定类
  2. 可以实现实例的依赖注入
  3. 可以对实例进行包装,采用装饰者模式增强类

喜欢小K的请多多关注微信公众号
深入了解Spring与Java的SPI机制_第1张图片

你可能感兴趣的:(java)