Java的SPI机制以及基于SPI编程

背景

在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。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源码中,因为已经定义为配置路径,如果写在别的地方,类加载器就会找不到了。
Java的SPI机制以及基于SPI编程_第1张图片

案例实现

代码结构如下:

├─main
│ ├─java
│ │ └─com
│ │ ├─test
│ │ │ └─Apple
│ │ │ └─Fruit
│ └─resources
│ ├─META-INF
│ │ └─services
│ │ └─com.test.Fruit

  1. 创建接口Fruit,模拟服务提供方接口
package com.test;
public interface Fruit {
 
}
  1. 创建接口实现类Apple,实现Fruit接口;
package com.test;
public class Apple implements Fruit {
    static {
        System.out.println("hi,I am an apple!");
    }
}
  1. 在resources结构下创建META-INF/services目录,在这个目录下,创建以这个接口名命名的文件com.test.Fruit,同时在这个文件中写入这个实现类的全类名;
com.test.Apple
  1. 创建Test测试类,在测试类中创建一个类加载器ServiceLoader来实现本案例。
import com.test.Fruit;
import java.util.ServiceLoader;
public class Test {
    public static void main(String[] args) {
        ServiceLoader test = ServiceLoader.load(Fruit.class);
        for (Fruit item:test){
 
        }
    }
}
  1. 执行结果如下,表明本案例成功执行。
hi,I am an apple!

你可能感兴趣的:(java)