开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情
Java SPI
SPI 全称 Service Provider Interface,是 Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API 寻找服务实现。本质是通过基于接口的 编程+策略模式+配置文件 实现动态加载。可以实现 解耦 (接口和实现分离),提高框架的 可拓展性(第三方可以自己实现,达到插拔式的效果)。
我们来开发SPI看一下。
首先定义一个接口。
public interface HelloSpi {
String getName();
void handle();
}
定义不同的实现类。
public class OneHelloSpiImpl implements HelloSpi {
@Override
public String getName() {
return "One";
}
@Override
public void handle() {
System.out.println(getName() + "执行");
}
}
public class TwoHelloSpiImpl implements HelloSpi {
@Override
public String getName() {
return "Two";
}
@Override
public void handle() {
System.out.println(getName() + "执行");
}
}
在指定目录(META-INF.services)下创建文件,(为什么是这个目录接下来会讲到)。
文件名是接口的全类名,文件内容是实现类的全类名。
com.shuaijie.impl.OneHelloSpiImpl
com.shuaijie.impl.TwoHelloSpiImpl
编写测试单元测试。是通过ServiceLoader
这个类实现的功能,接下来会详细讲解这个类的实现原理。
@Test
public void testSpi() {
ServiceLoader<HelloSpi> load = ServiceLoader.load(HelloSpi.class);
Iterator<HelloSpi> iterator = load.iterator();
while (iterator.hasNext()) {
HelloSpi next = iterator.next();
System.out.println(next.getName() + " 准备执行");
next.handle();
}
System.out.println("执行结束");
}
执行结果为
One 准备执行
One执行
Two 准备执行
Two执行
执行结束
通过执行结果我们可以看出,HelloSpi
接口的所有实现类都得到了调用,我们可以通过这种机制根据不同的业务场景实现拓展的效果。示例是通过ServiceLoader
实现的,我们来看一下这个类。
ServiceLoader
ServiceLoader是一个简单的服务提供者加载工具。是JDK6引进的一个特性。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
load
方法是通过获取当前线程的 线程上下文类加载器 实例来加载的。Java应用运行的初始线程的上下文类加载器默认是系统类加载器。这里其实 破坏了双亲委派模型,因为Java应用收到类加载的请求时,按照双亲委派模型会向上请求父类加载器完成,这里并没有这么做(有些面试官会问到破坏双亲委派模型相关问题,简单了解)。
iterator.hasNext()
主要是通过 hasNextService()
来实现的,我们来看一下主要代码。
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
该方法会去加载 PREFIX 变量路径下的配置,PREFIX 是一个固定路径,这也就是我们为什么要在META-INF/services/下创建文件的原因。并根据 PREFIX 加上全类名获取到实现类所在的全路径。
Java中有许多我们常见的框架使用SPI机制的地方,JDBC,Dubbo,Logback等,Spring中也有使用。
Spring SPI
Spring SPI对 Java SPI 进行了封装增强。我们只需要在 META-INF/spring.factories 中配置接口实现类名,即可通过服务发现机制,在运行时加载接口的实现类。
我们将讲解 Java SPI 的例子通过Spring SPI来实现一下。
测试代码如下。
@Test
public void testSpringSpi() {
List<HelloSpi> helloSpiList = SpringFactoriesLoader.loadFactories(HelloSpi.class,this.getClass().getClassLoader());
Iterator<HelloSpi> iterator = helloSpiList.iterator();
while (iterator.hasNext()) {
HelloSpi next = iterator.next();
System.out.println(next.getName() + " 准备执行");
next.handle();
}
System.out.println("执行结束");
}
执行结果,与Java SPI执行结果一致。
One 准备执行
One执行
Two 准备执行
Two执行
执行结束
Springboot利用Spring SPI开发starter
刚接触 Springboot 的时候引入一个个starter依赖,当时十分好奇这个东西,这一个个的starter就是通过Spring SPI来实现的。
我们也可以自己编写一个starter提供一些功能,传入公司的maven仓库,需要用到的项目就可以直接引入,减少项目对某些模块的代码硬侵入。
先来编写接口和实现。
package com.shuaijie.service;
public interface SpringbootStarterService {
void handle();
}
package com.shuaijie.service.impl;
public class SpringbootStarterServiceImpl implements SpringbootStarterService {
@Override
public void handle() {
System.out.println("SpringbootStarterServiceImpl执行");
}
}
spring.factories中内容。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.shuaijie.service.impl.SpringbootStarterServiceImpl
pom.xml指定groupId,artifactId,version。
<groupId>com.shuaijie</groupId>
<artifactId>spring-boot-starter-shuaijie</artifactId>
<version>1.0.0</version>
然后通过maven打包传入maven仓库。其他的Springboot项目就可以直接使用了。
@Autowired
private SpringbootStarterService springbootStarterService;
@Test
public void testSpringbootStarter() {
springbootStarterService.handle();
}
这种方式是不是很实用呢?