共计 1755 个字符,预计需要花费 5 分钟才能阅读完成。
背景
在面向对象的设计准则中,个别举荐模块之间基于接口编程,通常状况下调用方模块是不会感知到被调用方模块的外部具体实现。一旦代码外面波及具体实现类,就违反了开闭准则。如果须要替换一种实现,就须要批改代码。
为了实现在模块拆卸的时候不必在程序外面动静指明,这就须要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点相似 IOC 的思维,将拆卸的控制权移交到了程序之外。
SPI 英文为 Service Provider Interface,字面意思就是:“服务提供者的接口”,能够了解为专门提供给服务调用者或者扩大框架性能的开发者去应用的一个接口。
SPI 将服务接口和具体的服务实现拆散开来,将服务调用方和服务实现者解耦,可能晋升程序的扩展性、可维护性。批改或者替换服务实现并不需要批改调用方。
应用场景
很多框架都应用了 Java 的 SPI 机制,比方:数据库加载驱动,日志接口,以及 dubbo 的扩大实现等等。拿日志接口来说,Spring 框架提供的日志服务 SLF4J 其实只是一个日志接口,然而 SLF4J 的具体实现能够有几种,比方:Logback、Log4j、Log4j2 等等,而且还能够切换,在切换日志具体实现的时候咱们是不须要更改我的项目代码的,只须要在 Maven 依赖外面批改一些 pom 依赖就好了。
与 API 的区别
API:是指能够用来实现某项性能的类、接口或者办法。提供方提供实现形式,调用方只需调用即可。
SPI:是指用来继承、扩大,实现自定义性能的类、接口或者办法。调用方可抉择应用提供方提供的内置实现,也能够本人实现。
SPI 原理
Java SPI 的具体约定为:当服务的提供者提供了服务接口后,在 jar 包的 META-INF/services 目录下同时创立一个以服务接口全类名命名的文件,该文件的内容就是实现该服务接口具体实现类的全名,当然这个服务接口实现类也必须在这个 jar 中。当内部程序拆卸这个模块的时候,就能通过该 jar 包 META-INF/services 下的配置文件找到具体的实现类,并装载实例化,实现模块的注入。Java 就是通过扫描 META-INF/services 文件夹目录上面的文件,把实现类加载到 servciceLoader 外面。
简略来讲,META-INF/services/ 下的配置文件名字就是接口的全类名,实现类的全类名就是问这个文件的内容,如果有多个实现类,能够全副写在这个文件中,同时在实现类中要实现这个接口。
不定义在 META-INF/services 上面行不行?就想定义在别的中央能够吗?
答案是不行。看下图 JDK 源码中,因为曾经定义为配置门路,如果写在别的中央,类加载器就会找不到了。
案例实现
代码构造如下:
├─main
│ ├─java
│ │ └─com
│ │ ├─test
│ │ │ └─Apple
│ │ │ └─Fruit
│ └─resources
│ ├─META-INF
│ │ └─services
│ │ └─com.test.Fruit
- 创立接口 Fruit,模仿服务提供方接口
package com.test;
public interface Fruit {}
- 创立接口实现类 Apple,实现 Fruit 接口;
package com.test;
public class Apple implements Fruit {
static {System.out.println("hi,I am an apple!");
}
}
- 在 resources 构造下创立 META-INF/services 目录,在这个目录下,创立以这个接口名命名的文件 com.test.Fruit,同时在这个文件中写入这个实现类的全类名;
com.test.Apple
- 创立 Test 测试类,在测试类中创立一个类加载器 ServiceLoader 来实现本案例。
import com.test.Fruit;
import java.util.ServiceLoader;
public class Test {public static void main(String[] args) {ServiceLoader<Fruit> test = ServiceLoader.load(Fruit.class);
for (Fruit item:test){}}
}
- 执行后果如下,表明本案例胜利执行。
hi,I am an apple!