一位前辈在一次技术分享中指出咱们目前的包管理不规范,模块间职责有重叠,理解成本高不易维护,提出在开发过程当中应当明确按照职责将服务划分到对应的模块中。java
好比咱们把全部服务都放在service层,但其实服务也是分为基础服务和业务逻辑服务的,或许把相似业务数据查询组装服务放在service层,把具体业务逻辑服务统一放在business层会更好,更利于基础服务的复用。mysql
但当服务拆离到不一样模块进行复用时,可能在开发过程当中出现服务依赖的问题,这部分依赖问题的解耦能够用到Java的SPI机制。显然,我历来没有据说过SPI是什么,也不明白这有什么好处。sql
翻遍各类网上资料,来来回回都是车轱辘话,互相抄来抄去讲得并不通俗易懂,这里就用我本身的理解来解释。shell
SPI(Service Provider Interface)
,大意是“服务提供者接口”,是指在服务使用方角度提出的“接口要求”,是对“服务提供方”提出的约定,简单说就是:“我须要这样的服务,如今大家来知足”。编程
API(Application Programming Interface)
与之相对,是站在服务提供方角度提供的无需了解底层细节的操做入口,即“我有这样的服务能够给你使用”。微信
SPI与API的出发点大相径庭,但做用与目的是相同的,即面向接口编程,也就是解耦。同时SPI使用的是一种“插件思惟”,即服务提供者负责全部的使用维护,当替换服务提供方时不要说调用方不修改代码,连配置文件都不须要修改(不过可能要修改依赖的jar)。并发
模块化插件框架
举个例子,隔壁部门以为咱们的一个现有服务很棒,但愿咱们在其专用环境部署一份,同时但愿之后的全部迭代可以给他们也更新。可是使用的自研中间件咱们使用的内网版本他们使用公网版本,支付上咱们对接支付宝他们对接微信......在业务逻辑不变但切换基础服务时应该如何维护使成本最小?ide
方案 | 优势 | 缺点 |
---|---|---|
维护两套代码 | 逻辑一致 | 实现简单但维护成本高 |
同一套代码,在业务逻辑中区分环境 | 维护成本低,统一管理 | 逻辑复杂,须要硬编码,当再出现新环境时还得折腾 |
SPI“插件”方式 | 维护成本低,无需针对实现方硬编码,更多新环境或服务提供方变化时修改简单且不影响原有逻辑 | 理解成本提升 |
这也许就是一些框架在发展过程当中经历过的阶段,能够发现使用“插件”能更好知足这个需求。模块化
试想一下,若是要实现这样的解耦方式,理想状况下应该如何作?不外乎就是如下几点:
这是一种与IOC相同的思路,将装配控制权转移至程序外,由配置决定,切换成本低。
java.util.ServiceLoader提供的SPI加载方式
这个类很是简单,是原生支持的SPI加载方式,实际代码量也就200行左右。
关键点:
public static <S> ServiceLoader<S> load(Class<S> service)
ServiceLoader<SomeService> loader = ServiceLoader.load(SomeService.class);
可设置接入对应的接口private static final String PREFIX = "META-INF/services/";
META-INF/services/
目录META-INF/services/
下,文件名应为接口全限定名,内容每行为一个实现类全限定名public final class ServiceLoader<S> implements Iterable<S>
private class LazyIterator implements Iterator<S>
,实现了懒加载迭代,即迭代到对应的类才加载对应的类private boolean hasNextService()
和private S nextService()
hasNext()
方法和next()
方法Class.forName
加载类,使用newInstance
初始化实例,cast
进行强制类型转换最终获得实例,所以实现类必须提供无参构造方法清楚原理后,使用方式就很好理解。
step.1 调用方定义接口
package com.xxx;
public interface IHelloWorld {
void sayHello();
}
复制代码
step.? 使用API方式实现接口
非必选,对照看一下非SPI的方式。
package com.xxx;
public class HelloWorldApi implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello API!");
}
}
复制代码
step.2 调用方在业务代码中使用ServiceLoader
package com.xxx;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
// 使用API
IHelloWorld helloWorldApi = new HelloWorldApi();
helloWorldApi.sayHello();
// 使用SPI
ServiceLoader<IHelloWorld> loader = ServiceLoader.load(IHelloWorld.class);
for (IHelloWorld helloWorldSpi : loader) {
helloWorldSpi.sayHello();
}
}
}
复制代码
主要区别在于SPI方式并不须要知道实现类是谁,彻底面向接口使用,相似RPC调用的状况;而API要求在业务方代码/配置中指明实现类。
step.3 提供方实现接口
这里提供两个实现类。
package com.xxx;
public class HelloWorldSpi1 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 1!");
}
}
复制代码
package com.xxx;
public class HelloWorldSpi2 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 2!");
}
}
复制代码
能够看出,实现方式与API方式彻底一致。
step.4 提供方提供配置
文件位于/resources/META-INF/services
,文件名为com.xxx.IHelloWorld
即接口全限定名称。
/resources/META-INF/services/com.xxx.IHelloWorld
的内容为两个实现类的全限定名称:
com.xxx.HelloWorldSpi1
com.xxx.HelloWorldSpi2
复制代码
ps. 一般调用方与提供方不在同一个jar中
输出结果
Hello API!
Hello SPI 1!
Hello SPI 2!
复制代码
参考咱们经常使用的JDBC,咱们在同一套代码中可能须要利用相同接口但不一样实现的状况下,能够在代码中利用SPI接入面向接口编程,在业务中不考虑具体的底层实现。
具体的底层实现能够分离出来,将每组实现和SPI配置文件打包成不一样的jar,在具体使用时根据须要使用不一样的jar便可。
具体实现可随时替换,不修改业务代码或配置
在mysql-connector-java:5.1.47
包的META-INF/services/
目录下有个java.sql.Driver
文件,内容为:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
复制代码
这是JDBC 4.0以后使用SPI机制直接获取实现,避免以前使用Class.forName("com.mysql.jdbc.Driver")
方式加载MySQL驱动时的硬编码。详情可见java.sql.DriverManager
类中的静态代码块:
static {
loadInitialDrivers(); // 这里使用ServiceLoader获取具体的Driver接口实现
println("JDBC DriverManager initialized");
}
复制代码
本文搬自个人博客,欢迎参观!