Java Service Provider Interface 之 SPI机制

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的API寻找服务实现。Java中一个经典的SPI实现就是JDBC的Driver的加载机制,在最初我们学习JDBC时,我们需要使用Class.forName("com.mysql.jdbc.Driver")加载数据库驱动。其实在最新版本的JDK中我们可以省略该步骤,该步骤已经在JDK中通过SPI进行了驱动的加载。在DriverManager类中有一个静态块用于加载数据库驱动,代码如下:

static {
    //加载并初始化驱动
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

这里我们详细的分析loadInitialDrivers();方法,该方法主要逻辑为两部分,一部分是从系统属性jdbc.drivers中获取驱动的全类名,然后加载初始化,第二种则是使用SPI机制加载驱动类。代码如下所示:

private static void loadInitialDrivers() {
    
    //从系统属性jdbc.drivers获取驱动的全类名,代码不再展示
       
    //通过SPI机制加载初始化驱动
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //通过ServiceLoader加载驱动
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                //初始化实例化驱动
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
                return null;
        }
    });
   ......
   //加载实例化从系统属性获取的驱动,代码不再展示
}

如上代码,我们主要关注通过ServiceLoader加载驱动,也就是所谓的SPI机制。ServiceLoader加载META-INF/services目录下名称为全接口名的文件,而文件的内容为接口的实现类。比如我们的mysql-connection.jar,在其META-INF/services目录下有一个名为java.sql.Driver的文件,里面的内容为com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.Driver。下面我们介绍他是如何加载初始化这两个实例的。在上面的方法中有一个driversIterator.hasNext()方法。其源代码如下:

public boolean hasNext() {
    if (acc == null) {
        //调用hasNextService()方法
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            //调用hasNextService()方法
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private boolean hasNextService() {
    if (nextName != null) {
       return true;
    }
    if (configs == null) {
       try {
           //加载META-INF/service下的名称为接口全名称的文件
           String fullName = PREFIX + service.getName();
           if (loader == null)
               configs = ClassLoader.getSystemResources(fullName);
           else
                        configs = loader.getResources(fullName);
           } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
           }
      }
    //其他逻辑,解析文件
           
}

上面的方法加载了META-INF/services的名称为接口的全名称的文件,后续的逻辑就是解析文件吗,然后初始并实例化文件中配置的实现类。后面的代码我们不再介绍,下面我们使用ServiceLoader做一个示例,如下我们创建一个接口SPIService和两个实现类,代码如下所示:

public interface SPIService {
    void printSpiService();
}
public class CustomizedSPIService implements SPIService {
    public void printSpiService() {
        System.out.println("I am CustomizedSPIService");
    }
}
public class MicroserviceSPIService implements SPIService {
    public void printSpiService() {
        System.out.println("I am MicroserviceSPIService");
    }
}

下面我们不是使用new去实例化上面的两个实现类,而是通过ServiceLoader去加载,因为实现类实现的是SPIService,因此我们先在META-INF/services下创建一个名称为cn.org.microservice.inter.SPIService的文件,并且将两个实现类的全类名写进去,如下所示:

 如下为测试代码,会调用两个实现类的方法,然后打印出内容:

public class ServiceLoaderAp {
    public static void main(String[] args) {
        ServiceLoader<SPIService> services = ServiceLoader.load(SPIService.class);
        services.forEach((spiService) ->{
            spiService.printSpiService();
        });
    }
}

Java的SPI机制可以让我们在不改变源码包的情况下扩展我们应用程序,但是他也有一些缺点,比如无法容器化,即无法将实例化的对象交给容器管理。因此很多框架都是自定义SPI机制,比如spring boot和Alibaba开源的dubbo。下面我们会介绍这两种的SPI机制。首先我们介绍Dubbo的SPI机制。

Dubbo的SPI相关逻辑被封装在ExtensionLoader类中,通过ExtensionLoader我们可以加载指定的实现类。Dubbo会从META-INF/services、META-INF/dubbo、META-INF/dubbo/internal目录加载以接口全路径名命名的文件,与JDK内置SPI机制不同的是,dubbo提供了key-value键值对的方式。我们还是以上面的接口为例。我们在META-INF目录下创建dubbo目录,不过文件内容修改为如下:

customizedSpiService=cn.org.microservice.inter.impl.CustomizedSPIService
microserviceSpiService=cn.org.microservice.inter.impl.MicroserviceSPIService

如下代码为dubbo的SPI的测试代码,至于dubbo ExtensionLoader的实现,我们这里不作讲解,后续会详细讲解dubbo的扩展机制。这里只是介绍ExtensionLoader的使用。

ExtensionLoader<> extensionLoader = ExtensionLoader.getExtensionLoader(SPIService.class);
SPIService spiService = extensionLoader.getExtension("customizedSpiServcice")
spiService.printSpiService()

dubbo加载的是META-INF/services、META-INF/dubbo、META-INF/dubbo/internal目录加载以接口全路径名命名的文件,而Spring Boot则是使用SpringFactoiesLoader实现。它加载的是META-INF目录下的spring.factories文件的内容,Spring Boot SPI机制的实现,这里不作详解,可以参考博客:Spring Boot 原理解析—自动装配原理。里面详细的介绍了Spring Boot的加载机制。

那么我们是否可以创建自己的SPI机制呢,答案是当前可以,这里不再介绍具体的案例,只是介绍如果获取classpath下的所有的目录下的文件。 ClassLoader实例中有一个getResources()方法用于获取所有的文件,在这之前需要清除JVM的类加载机制与ClassLoader的机制。代码如下所示:

 SpringApplication.class.getClassLoader().getResources("META-INF/microservice/event.factories");

Java的SPI机制我们就介绍到这里,后续我们会详细介绍Dubbo的扩展机制,以及dubbo中的SPI的源码。

猜你喜欢

转载自blog.csdn.net/wk19920726/article/details/108580441
今日推荐