前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂什么是SPI机制

一文读懂什么是SPI机制

作者头像
凯哥的Java技术活
发布2022-07-08 14:09:07
5370
发布2022-07-08 14:09:07
举报
文章被收录于专栏:凯哥的Java技术活

A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service.

SPI全称,Service Provider Interface,服务提供者接口。服务是接口或者抽象类,服务提供者负责实现。在做插件化功能时很实用。I

ClassLoader

在开始SPI测试之前,需要先对jvm的类加载机制有一个了解。首先先看一下类加载器的结构图,这对java的SPI很重要,之前如果不了解类加载器也没关系,先大概有个印象。

在本图中。列举了BootStrap ClassLoader(引导类加载器)、Extension ClassLoader(拓展类加载)、Application ClassLoader(应用类加载器)。

根据类型,ClassLoader可以分为两类,一个是BootStrapClassLoader(引导类加载器),负责加载java核心包,下面列举了一些引导类加载器加载的位置。

代码语言:javascript
复制
resources.jar
rt.jar
sunrsasign.jar
..

例如常用的String类等,都是由BootstrapClassLoader来加载的。

另外一种是用户定义类加载器,包括ExtClassLoader扩展类加载器,和AppClassLoader应用类加载器,以及我们自定义的类加载器。其中AppClassLoader负责加载用户ClassPath路径下的类,也就是说你写的类,都是由它加载的。

ClassLoader有几个原则,分别是:

Parent Delegate:又被翻译成双亲委派模型。该原则保证了所有要加载的类,都要经过Boostrap ClassLoader这个老大哥,能防止自定义的类替换掉java核心类,例如String类。

Visibility:可见性。子类加载器能够访问父加载器加载的类,反过来父加载器不能访问子加载器加载的类。

Unique:唯一性,在同一命名空间内,一个类只会被加载一次。

SPI使用方法

使用SPI,可以简单的分为4步。

  1. 定义接口/抽象类。
  2. 实现类
  3. 实现方在META-INF/services下,创建一个以接口的全限定名为名称的文件,内容是提供是该接口的实现类的全限定名。
  4. 使用ServiceLoader.load()方法来加载实现类。

代码说明:

1.定义接口,以下定义了一个DemoService,只有一个方法,sayHello。

代码语言:javascript
复制
/**
 * Demo service. Implements should be use SPI.
 */
public interface DemoService {

    String sayHello();

}

2.编写实现类,DemoServiceProvider实现了DemoService。

代码语言:javascript
复制
/**
 * Implement for DemoService
 */
public class DemoServiceProvider implements DemoService {
    
    @Override
    public String sayHello() {
        return "hello world";
    }
}

3.在实现类所属的工程下类路径下添加文件:

4. 加载使用

代码语言:javascript
复制
public class App {

    public static void main(String[] args) throws Exception{
        ServiceLoader<DemoService> demoServices = ServiceLoader.load(DemoService.class);
        Iterator<DemoService> iterator = demoServices.iterator();
        while (iterator.hasNext()) {
            DemoService demoService = iterator.next();
            System.out.println(demoService.getClass().getName());
            System.out.println(demoService.getClass().getClassLoader());
            System.out.printf(demoService.sayHello());
        }

    }
}

运行程序,输入结果如下:

当然可以编写其他实现类来为DemoService提供服务。然后按照规范在META-INF/services下面新建相应的文件即可。

可以看到SPI能够发挥定义接口,其他项目提供插件化的实现的功能,能够有效的扩展代码。

SPI在java大佬手里是怎么用的?

最经典的SPI实现莫过于JDBC了,回想一下JDBC具体代码是怎么使用的:

代码语言:javascript
复制
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc://xxxx", "root", "xxx");

这样就拿到了数据库连接对象。貌似也没什么特别的配置,就是把mysql的驱动jar包依赖一下,然后就获取到连接对象呗。

那为什么放mysql驱动可以获取到mysql的连接,放oracle的驱动就能获取到oracle的连接?

既然是通过DriverManager获取到的连接对象,我们就进去DriverManager的源码中去看一下。

DriverManager 第100行如下:

代码语言:javascript
复制
/**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

可以看到,DriverManger被初始化的时候,会执行loadInitialDrivers()方法。

DriverManager 第586行:

代码语言:javascript
复制
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

可以看到使用到了SPI方法,ServiceLoader.load() 和上面我们写的demo一样,加载Driver这个接口的实现类。

那Driver的实现类肯定是在mysql的驱动包内放着

确实没错,再次证实的SPI的用法。

双亲委派原则带来的问题

我们刚才讲过,类加载器的三个原则:双亲委托,可见性,唯一性。

再回过头看刚才JDBC是怎么使用SPI的,貌似不对啊,违反了双亲委托原则!

DriverManager是java的核心包,是在rt.jar包内,应该是被BootStrapClassLoader加载的,那么它在初始化的时候,根本加载不到mysql的jar包,因为mysql的jar包是在你的classpath下,是要被AppClassLoader加载的。

难道双亲委托是假的?还是说另有玄机?

带着疑问,打开ServiceLoader.load()的源码,看看是如何进行加载的。

看到其中有一行代码,貌似和ClassLoader类加载器有关,这里我们先猜测这行是关键代码。

代码语言:javascript
复制
ClassLoader cl = Thread.currentThread().getContextClassLoader();

我们打断点进去看看这行代码得到的是什么ClassLoader。

可以看到获取到的是AppClassLoader。那也就是说,在加载DriverManager的时候,通过这行代码,获取到了AppClassLoader来加载mysql驱动包下的类。这种方式获取类加载器成为线程上下文类加载器。这其实是java给自己开的后门,线程上下文ClassLoader可以自己设置,这样就能不遵守双亲委托模型,即使是在父加载器加载类过程当中,也可以用AppClassLoader加载子加载器需要加载的类。

Tomcat怎么用的SPI?

如果你没有使用SpringBoot,还是使用的SpringMVC结合Tomcat,这里指的是打包,然后放到tomcat容器中运行的项目。

那么你打开spring-web项目的源码,会发现spring-web下有spi的配置:

有点眼熟,这不是我们上面写的SPI吗?这里猜测一波,是Tomcat在启动的时候加载Spring在META-INF/services中的的类。

为了证明猜测,可以下载Tomcat源码进行查看,可以看到Tomcat的源码里确实是去META-INF/services下找需要加载的类。

如此一来,SpringMVC配置了ServletContainerInitializer,在项目部署到Tomcat后,Tomcat回调该方法,SpringMVC完成父子容器的加载。

之所以SpringBoot没有这个操作,是因为SpringBoot内置了Tomcat到自己的容器中,不需要Tomcat的回调初始化容器这些操作。而是SpringBoot在初始化容器的时候,去启动内置Tomcat。

SpringBoot怎么用的SPI?

当你使用@SpringBootApplication注解时,会开始自动配置,而启动配置则会去扫描META-INF/spring.factories下的配置类。

SpringBoot会去META-INF/spring.factories中查找配置类,我们可以根据这个规则进行自定义配置对SpringBoot进行扩展。

总结

SPI是可插件化实现的一种方式,在各个框架中得到了广泛的应用,其实很多并没有和ClassLoader有关系,只是使用了这种方式而已。像JDBC这种内置于java核心库的SPI,则使用了线程上下文类加载器,实际上是java给自己开的后门,能够不遵守类加载器的双亲委派原则。

END

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 凯哥的Java技术活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档