SPI的简单应用

在查看java.sql.DriverManager源码时,发现有这么一个静态方法叫做loadInitialDrivers()。在这个方法里,我发现在项目启动中驱动管理器会从系统变量jdbc.drivers中获取具体的驱动实现并注册,其次会使用SPI注册驱动。这些我在 再谈驱动注册 中已经讲过了,那么什么是SPI?怎么用?

什么是java的SPI

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现,例如spring框架,dubbo和sofa分布式服务框架。简单来说,它就是一种动态发现的机制,举个例子来说,有个接口,想运行时动态的给它添加实现,你只需要添加一个实现即可。

具体是在JAR包的/META-INF/services/目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名。

运用场景

比如你想扩展一些框架,如spring的一些功能,就需要实现它接口,然后在运行时将你的jar包放到类路径下。

简单例子

假设我们有一个接口,方法是String hello(String message),我们允许不同的语言的hello返回不同的内容,例如中文返回的结果是你好 message,而英文返回HELLO message。那么这个SPI应用该怎么写呢?

我这有两个maven子项目,分别是learn-spi和learn-spi-cluster。learn-spi中写的是接口和main方法,learn-spi-cluster写的是实现类和资源文件。

注:打包时需要从父项目打包。

接口

首先我们定义一个接口

1
2
3
4
5
6
7
8
package com.gavinzh.learn.spi.interfaces;

/**
* @author gavin
*/
public interface SPIService {
public String hello(String message);
}

实现

我们再来定义两个实现类

1
2
3
4
5
6
7
8
9
10
11
12
package com.gavinzh.learn.spi.cluster.impl;

import com.gavinzh.learn.spi.interfaces.SPIService;

/**
* @author gavin
*/
public class SPIServiceCN implements SPIService {
public String hello(String message) {
return "你好 " + message;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.gavinzh.learn.spi.cluster.impl;

import com.gavinzh.learn.spi.interfaces.SPIService;

/**
* @author gavin
*/
public class SPIServiceEN implements SPIService {
public String hello(String message) {
return "HELLO " + message;
}
}

在实现的项目的resources目录下创建META-INF/services目录,并在该目录下创建一个文件,名为接口的全限定名com.gavinzh.learn.spi.interfaces.SPIService。内容为:

1
2
com.gavinzh.learn.spi.cluster.impl.SPIServiceCN
com.gavinzh.learn.spi.cluster.impl.SPIServiceEN

使用

一般情况下,使用SPI都是在接口所在的包中。所以我们在接口所在包中新建一个Main类。

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.gavinzh.learn.spi;


import com.gavinzh.learn.spi.interfaces.SPIService;
import sun.misc.ClassLoaderUtil;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
* @author gavin
*/
public class Main {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
SPIService spiService = null;
ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService.class, classLoader);
Iterator<SPIService> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
spiService = iterator.next();
System.out.println(spiService.hello("SPI"));
}
}
}

运行

我们将两个项目分别打包。在我的例子中,接口所在的包叫做learn-spi-1.0-SNAPSHOT.jar,实现类所在的包叫做learn-spi-cluster-1.0-SNAPSHOT.jar

接下来打开learn-spi-1.0-SNAPSHOT.jar,修改META-INF/MAINIFEST.MF,在文件末尾增加

1
2
Main-Class: com.gavinzh.learn.spi.Main
Class-Path: *.jar

第一行的作用是指定该jar包的运行入口,第二行的作用是在运行是jvm需要从应用类路径下加载的jar包,其实我们要加载的就是learn-spi-cluster-1.0-SNAPSHOT.jar

最后,将两个jar包放到同一个目录下,在该目录下执行

1
java -jar learn-spi-1.0-SNAPSHOT.jar

你就会看到输出以下内容

1
2
你好 SPI
HELLO SPI

总结

从jdk1.6开始,java支持了SPI,使用java.util.ServiceLoaderMETA-INF/services/寻找接口全限定名对应的文件,在文件中的每一行加载其实现类并实例化。

留一个问题,如何通过SPI技术扩展spring呢?以后有时间再写一篇关于SPI扩展spring的文章。