SPI 是 JAVA 提供的一种服务提供发现接口,其实就是一种面向接口的编程,为接口去匹配具体服务实现的机制
,这一点上与 IOC 的思想类似,都是把装配的控制权放到了程序之外,下面具体看看什么是 SPI。
一、什么是 SPI
SPI 全称为 Service Provider Interface
,即服务提供发现接口,这里的服务指的不是我们经常听到的微服务服务发现,这里的一个服务 Service 指的是一个接口或抽象类,服务提供方则是对这个接口或抽象类的实现
。SPI 是 ”基于接口的编程 + 策略模式 + 配置文件“ 组合实现的动态加载机制
二、为什么使用 SPI
模块化设计中,模块之间基于接口编程,把装配的控制权放到程序之外,实现系统的解耦
- 使用场景
适用于调用方根据实际需求启用、扩展、替换服务的策略实现。许多开源框架中都使用了 Java 的 SPI 机制,如 JDBC 的 SPI 加载模式、日志框架 SLF4J 加载不同提供商的日志实现、Spring 中也大量适用了 SPI、Dubbo 的扩张机制、ServiceComb Java Chassis (CSE) 的 Filter、异常处理等扩展机制
三、SPI 的实现
- SPI 的实现步骤
- 在类路径下的 META-INF/services 目录下,创建以服务接口的”全限定名“命名的文件,文件的内容为接口实现类的全限定名
- 实现类必须在当前程序的 classpath 下
- 使用
bash java.util.ServiceLoader 动态加载实现,会扫描 META-INF/services 下的配置文件加载实现类
- 先定义一个基接口
public interface BaseDriver {
void url();
}
并使用maven打包,并安装在本地maven仓库
- 新建maven项目,导入含有BaseDriver的maven坐标,编写一个接口实现类
public class MysqlDriver implements BaseDriver
{
@Override
public void url() {
System.out.println("this is a mysql url");
}
}
以下这一步很重要:
再次使用maven打包,并安装在本地maven仓库
- 可是多写一个实现类
public class OracleDriver implements BaseDriver {
public void url() {
System.out.println("this is an oracle url");
}
}
也要再次打包
- 新建maven项目使用
pom.xml
<dependency>
<groupId>com.spi</groupId>
<artifactId>MYSQL_SPI</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.spi</groupId>
<artifactId>ORACLE_SPI</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 测试
四、SPI机制在Tomcat中的应用
- 我们知道:Servlet 2.5 实现 webApp 加载的方式是 web.xml,
获取其中配置的ServletContextListener 的实现类,通过实现类的 contextInitialized 方法来加载 webapp。
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
- 而
Servlet 3.0 就无需配置 web.xml, 直接通过实现接口 ServletContainerInitializer 就可以实现 webApp 的加载
,就如类名一样,这个类的作用就是在 servlet 容器初始化过程中加入自定义操作,因为是自定义的,所以可扩展性就非常强,能干很多事情。
- 两者执行位置的区别
- 两个方法的执行位置都是在 Tomcat 启动过程中,Context 容器启动时。
具体方法是 StandardContext 类的 startInternal() 方法,一个 context 容器就代表了一个 webapp。
- ServletContainerInitializer源码
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
- 那么servlet3.0规范下,是如何不通过web.xml的方式,使用纯java代码的方式加载spring应用的呢
直接看到spring-web:5.3.13的源码:
没错,tomcat启动后,可以根据servlet3.0的规范,通过SPI机制,将实现了ServletContainerInitializer 接口的类全部进行加载,并排序后,依次调用onstartup方法
在看看javax.servlet.ServletContainerInitializer的内容:
org.springframework.web.SpringServletContainerInitializer
于是,我们直接找到SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
这里要先知道@HandleTypes这个注解的含义,该注解也是Servlet规范所定义的,作用就是:将注解指定的Class对象(包括其实现类及子类)作为参数传递到onStartup(也就是@HandleTypes只能作用在实现了ServletContainerInitializer 的类上)
//含义就是,将WebApplicationInitializer.class的子类获取实现类,
//作为一个Set<Class<?>>参数传入道到SpringServletContainerInitializer.onstartup方法中
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}
然后,我们就可以在onstartup方法中,进行我们的开发,例如初始化web容器,初始化DispatcherServlet等操作
- AbstractContextLoaderInitializer.onstartup:
public void onStartup(ServletContext servletContext) throws ServletException {
this.registerContextLoaderListener(servletContext);
}
- AbstractDispatcherServletInitializer.onstartup:
public void onStartup(ServletContext servletContext) throws ServletException {
//调用父类
super.onStartup(servletContext);
this.registerDispatcherServlet(servletContext);
}
五、在SpringBoot中的应用