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写的是实现类和资源文件。
注:打包时需要从父项目打包。
接口
首先我们定义一个接口
package com.gavinzh.learn.spi.interfaces;
/**
* @author gavin
*/
public interface SPIService {
public String hello(String message);
}
实现
我们再来定义两个实现类
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;
}
}
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
。内容为:
com.gavinzh.learn.spi.cluster.impl.SPIServiceCN
com.gavinzh.learn.spi.cluster.impl.SPIServiceEN
使用
一般情况下,使用SPI都是在接口所在的包中。所以我们在接口所在包中新建一个Main类。
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
,在文件末尾增加
Main-Class: com.gavinzh.learn.spi.Main
Class-Path: *.jar
第一行的作用是指定该jar包的运行入口,第二行的作用是在运行是jvm需要从应用类路径下加载的jar包,其实我们要加载的就是learn-spi-cluster-1.0-SNAPSHOT.jar
。
最后,将两个jar包放到同一个目录下,在该目录下执行
java -jar learn-spi-1.0-SNAPSHOT.jar
你就会看到输出以下内容
你好 SPI
HELLO SPI
总结
从jdk1.6开始,java支持了SPI,使用java.util.ServiceLoader
从META-INF/services/
寻找接口全限定名对应的文件,在文件中的每一行加载其实现类并实例化。
留一个问题,如何通过SPI技术扩展spring呢?以后有时间再写一篇关于SPI扩展spring的文章。