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步。
- 定义接口/抽象类。
- 实现类
- 实现方在META-INF/services下,创建一个以接口的全限定名为名称的文件,内容是提供是该接口的实现类的全限定名。
- 使用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