星语课程网
一文读懂SPI机制
来源:本站编辑
2023-04-07 21:37
71
本文长度为4155 **字**,建议阅读**11分钟** 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,服务提供者接口。服务是接口或者抽象类,服务提供者负责实现。在做插件化功能时很实用。 **ClassLoader** 在开始SPI测试之前,需要先对jvm的类加载机制有一个了解。首先先看一下类加载器的结构图,这对java的SPI很重要,之前如果不了解类加载器也没关系,先大概有个印象。  在本图中。列举了BootStrap ClassLoader(引导类加载器)、Extension ClassLoader(拓展类加载)、Application ClassLoader(应用类加载器)。 根据类型,ClassLoader可以分为两类,一个是BootStrapClassLoader(引导类加载器),负责加载java核心包,下面列举了一些引导类加载器加载的位置。 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。 /** * Demo service. Implements should be use SPI. */ public interface DemoService { String sayHello(); } 2.编写实现类,DemoServiceProvider实现了DemoService。 /** * Implement for DemoService */ public class DemoServiceProvider implements DemoService { @Override public String sayHello() { return "hello world"; } } 3.在实现类所属的工程下类路径下添加文件:   4\. 加载使用 public class App { public static void main(String[] args) throws Exception{ ServiceLoader
demoServices = ServiceLoader.load(DemoService.class); Iterator
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具体代码是怎么使用的: Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc://xxxx", "root", "xxx"); 这样就拿到了数据库连接对象。貌似也没什么特别的配置,就是把mysql的驱动jar包依赖一下,然后就获取到连接对象呗。 那为什么放mysql驱动可以获取到mysql的连接,放oracle的驱动就能获取到oracle的连接? 既然是通过DriverManager获取到的连接对象,我们就进去DriverManager的源码中去看一下。 DriverManager 第100行如下: /** * 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行: ServiceLoader
loadedDrivers = ServiceLoader.load(Driver.class); Iterator
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类加载器有关,这里我们先猜测这行是关键代码。 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给自己开的后门,能够不遵守类加载器的双亲委派原则。
点赞
热门评论
最新评论
匿名用户
+1
-1
·
回复TA
暂无热门评论
相关推荐
阅读更多资讯
热门评论 最新评论
暂无热门评论