java SPI 与 ServiceLoader

java SPI机制

SPI的全名为Service Provider Interface。简单来说就是通过配置文件指定接口的实现类。在java.util.ServiceLoader的文档里有比较详细的介绍。
我们系统里抽象的各个模块,往往有很多不同的实现方案。那么我们来看看下面这个例子你就能明白了。

例子代码

比如一个电视机厂商,要生产3中不同规格的电视机,小型电视机、中型电视机、大型电视机。
那么我们可以设计:电视机模型接口、3种不同电视机的实现类
模型接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.shan.test;

/**
* <pre>
* TODO 电视机模型接口
* </pre>
* @author adan
* @version 1.0, 2019年11月20日
*/
public interface TelevisionService
{
/**
* <pre>
* 生产
* </pre>
*/
public void production();
}

电视机实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.shan.test;

/**
* <pre>
* TODO 小型电视机
* </pre>
* @author adan
* @version 1.0, 2019年11月20日
*/
public class SmallTelevisionServiceImp implements TelevisionService
{
public void production()
{
System.out.println("生产小型电视机");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* <pre>
* TODO 中型电视机
* </pre>
* @author adan
* @version 1.0, 2019年11月20日
*/
public class MidsizeTelevisionServiceImp implements TelevisionService
{
public void production()
{
System.out.println("生产中型电视机");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* <pre>
* TODO 大型电视机
* </pre>
* @author adan
* @version 1.0, 2019年11月20日
*/
public class LargeTelevisionServiceImpl implements TelevisionService
{
public void production()
{
System.out.println("生产大型电视机");
}

}

上面已经定义好了,下面我们来实现生产3种不同电视机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.shan.test;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
App app=new App();
app.production2();
}

//生产
public void production1() {
TelevisionService small=new SmallTelevisionServiceImp();
small.production();
TelevisionService midsize=new MidsizeTelevisionServiceImp();
midsize.production();
TelevisionService large=new LargeTelevisionServiceImpl();
large.production();
}
}

打印如下:
生产小型电视机
生产中型电视机
生产大型电视机

这个时候,这个电视机厂商扩展业务了,要提供了代加工电视机业务,帮别人生产电视机。
我们要约定生产电视机的标准了,让其他厂商比较方便接入。这就是SPI
规定:

  1. 创建接口实现
  2. 在resources资源目录下创建META-INF/services文件夹
  3. 在services文件夹中创建,以接口类全名命名的文件
  4. 在文件中加入接口实现类全名

我们改造一下项目
第一步,在resources中创建META-INF/services文件夹,并在services中创建com.shan.test.TelevisionService文件
第二步:在com.shan.test.TelevisionService文件中写入

1
2
3
com.shan.test.LargeTelevisionServiceImpl
com.shan.test.MidsizeTelevisionServiceImp
com.shan.test.SmallTelevisionServiceImp

第三步:改造代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.shan.test;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
App app=new App();
app.production2();
}
//平常实现方式
public void production1() {
TelevisionService small=new SmallTelevisionServiceImp();
small.production();
TelevisionService midsize=new MidsizeTelevisionServiceImp();
midsize.production();
TelevisionService large=new LargeTelevisionServiceImpl();
large.production();
}
//通过ServiceLoader来实现
public void production2() {
ServiceLoader<TelevisionService> loadedImpl = ServiceLoader.load(TelevisionService.class);
Iterator<TelevisionService> it = loadedImpl.iterator();
while (it.hasNext()) {
TelevisionService service = it.next();
service.production();
}
}
}

运行打印如下:
生产小型电视机
生产中型电视机
生产大型电视机

博主,你貌似描述清楚SPI的意思,但其他厂商怎么接入呢?
别急,马上来实现,再次改造项目(这个时候可能需要看源码了)
源码地址:https://github.com/adanblog/sourceCode.git

第一步:改造项目,把原来的一个项目拆分成2两个
SPITestApi,把电视机模型接口TelevisionService.java移到里面。
SPITest项目引用SPITestApi项目。

第二步:新厂商需要实现我们生产电视机的接口,
新建项目SPITest2,引用SPITestApi项目。
创建电视机的实现类,现在这个厂商要生产50寸的电视机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.shan.test;

/**
* <pre>
* TODO 50寸电视机
* </pre>
* @author adan
* @version 1.0, 2019年11月20日
*/
public class FiftyTelevisionServiceImpl implements TelevisionService
{
public void production()
{
System.out.println("生产50寸电视机");
}
}

在resources中创建META-INF/services文件夹,并创建com.shan.test.TelevisionService文件
写入com.shan.test.FiftyTelevisionServiceImpl

第三步,在SPITest项目中引用SPITest2项目
第四步,运行SPITest项目中app类中的main方法你会神奇的发现50寸电视机也生产出来了。

打印如下:
生产大型电视机
生产中型电视机
生产小型电视机
生产50寸电视机

小结

SPI被广泛使用在第三方插件式类库的加载,最常见的如JDBC、JNDI、JCE(Java加密模块扩展)等类库。理解ServiceLoader的工作原理有助于编写扩展性良好的可插拔的类库。
springBoot启动也是这种思想来实现的。

源码地址

https://github.com/adanblog/sourceCode.git

-------------本文结束感谢您的阅读-------------