很久之前,在做项目时,想要做插件式开发,但是当时并没有想到怎么实现.今天,在研究Spring的源码时,发现有SPI的身影出现.
其实SPI这个东西,之前也看到过它的身影,也有看过Oracle的官方文档来了解它.但是,由于没有记录下来,所以在过了一段时间之后,就忘记了.
今天,又重新研究了一下SPI,这里就记录下来.正好最近也打算做一个项目,就打算采用插件化开发的方式.
介绍
如果各位看过一些比较出名的PHP项目的源码,或者用过一些比较出名的PHP项目并且看过这些项目的文档,你会发现,它们基本上都支持插件化开发.
这就意味着,我们如果想向这些项目中加入一些新的功能,不需要修改源代码,只需要按照某些特定的格式编写一个模块即可.
在Java中,SPI就帮助我们实现这种功能.
目的
在这篇文章中,我们会对Oracle官方文档中的文字查询器这个小项目进行简化.实现的功能跟那个类似,但是结构上简化了一些.
在Oracle官方文档中,使用了ant,虽然这是一个非常厉害的工具,但是这里我们并不使用这个工具来构建这个项目,而是手动编译.
项目结构
项目的结构也非常简单:
- 一个父接口,所有的模块都要实现这个接口
- 两个不同的文字查询模块,分别提供不同的查询方式
- 一个客户端
有点难理解?没关系.继续看下去你就会发现其实很简单.
步骤
定义父接口
首先,我们需要定义一个父接口.这个父接口很重要,只有各个模块实现了这个接口并且再写一些其他的元数据,这些模块才能被SPI感知到.
我们创建一个目录,其名为DictionaryServiceProvider.在这个目录下,我们在创建一些其他的目录,其结构如下:
-
|-build
|-src
|- dictionary
|- spi
我们这个父接口就存放在src->dictionary->spi这里面.
我们在src->dictionary->spi这个文件夹里面,新建一个名为Dictionary.java的文件,其内容为:
package dictionary.spi;
public interface Dictionary {
public String getDefinition(String word);
}
这样父接口就写完了.
创建Service
上面的那个父接口很简单,但是我们可能有好几个模块实现了这个接口,那么SPI如何知道到底应该使用哪个模块呢?
所以,我们还需要定义这么一个Service,告诉SPI它应该如何对待这些模块.
在src->dictionary中,创建DictionaryService.java,其内容如下:
package dictionary;
import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
public class DictionaryService {
private static DictionaryService service;
private ServiceLoader<Dictionary> loader;
private DictionaryService() {
loader = ServiceLoader.load(Dictionary.class);
}
public static synchronized DictionaryService getInstance() {
if(service == null) service = new DictionaryService();
return service;
}
public String getDefinition(String word) {
String definition = null;
try {
Iterator<Dictionary> dictionaries = loader.iterator();
while (definition == null && dictionaries.hasNext()) {
Dictionary d = dictionaries.next();
definition = d.getDefinition(word);
}
} catch(ServiceConfigurationError serviceError) {
definition = null;
serviceError.printStackTrace();
}
return definition;
}
}
我们可以看到,在上面的代码中,也有一个getDefinition(String word),就跟src->dictionary->spi->Dictionary.java中的那个方法一样.
但是,这两个方法实际上并没有关系,只是我们为了统一,写成了一样的而已.
在上面的代码中,我们可以看到,通过ServiceLoader.load(Dictionary.class)方法来加载各个模块,然后在getDefinition(String word)方法中,通过枚举各个模块来查找我们想要的单词.
实际上,在客户端上,就是通过这个DictionaryService来查找单词的.
编译DictionaryServiceProvider
在DictionaryServiceProvider这个目录下,通过下面的命令即可编译成功:
javac -d build/ src/dictionary/spi/Dictionary.java
javac -d build/ -cp .:build src/dictionary/DictionaryService.java
编写第一个模块
创建一个和DictionaryServiceProvider同级的目录GeneralDictionary,其目录结构和DictionaryServiceProvider类似,但是也添加一些新的内容,如下所示:
-
|-build
|-src
|- dictionary
|- META-INF
|- services
我们在src->dictionary目录下,创建一个名为GeneralDictionary.java的文件,其内容如下:
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class GeneralDictionary implements Dictionary {
private SortedMap<String, String> map;
public GeneralDictionary() {
map = new TreeMap<String, String>();
map.put("book", "a set of written or printed pages, usually bound with a protective cover");
map.put("editor", "a person who edits");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
我们可以看到,它实现了Dictionary这个接口.
我们还需要在src->META-INF->services下面创建一个名为父接口的完全限定类名的文件,这里为dictionary.spi.Dictionary,其内容为这个模块中实现父接口的那个文件的完全限定类名,这里为dictionary.GeneralDictionary.
然后编译这个项目,采用下面的命令:
javac -d build/ -cp ../DictionaryServiceProvider/build src/dictionary/GeneralDictionary.java
cp -r src/META-INF build/
编写第二个模块
我们创建一个跟GeneralDictionary同级的目录ExtendedDictionary,其结构跟GeneralDictionary相同.
我们在src->dictionary目录下,创建一个名为ExtendedDictionary.java的文件,其内容为:
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class ExtendedDictionary implements Dictionary {
private SortedMap<String, String> map;
public ExtendedDictionary() {
map = new TreeMap<String, String>();
map.put("xml", "a document standard often used in web services, among other things");
map.put("REST", "an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
然后,同样在src->META-INF->services下,创建一个名为dictionary.spi.Dictionary的文件,其内容为:
dictionary.ExtendedDictionary
使用跟上面类似的命令编译这个项目.
javac -d build/ -cp ../DictionaryServiceProvider/build/ src/dictionary/ExtendedDictionary.java
cp -r src/META-INF/ build/
编写客户端
我们创建一个跟ExtendedDictionary同级的名为DictionaryDemo的目录,其项目结构如下:
-
|-build
|-src
|- dictionary
在src->dictionary下,创建一个名为DictionaryDemo.java的文件,内容如下:
package dictionary;
import dictionary.DictionaryService;
public class DictionaryDemo {
public static void main(String[] args) {
DictionaryService dictionary = DictionaryService.getInstance();
System.out.println(DictionaryDemo.lookup(dictionary, "book"));
System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
}
public static String lookup(DictionaryService dictionary, String word) {
String outputString = word + ": ";
String definition = dictionary.getDefinition(word);
if(definition == null) {
return outputString + "Cannot find definition for this word.";
} else {
return outputString + definition;
}
}
}
代码也很简单,不解释.
使用下面的命令进行编译:
javac -d build/ -cp ../DictionaryServiceProvider/build src/dictionary/DictionaryDemo.java
运行
进入到DictionaryDemo的build目录,执行下面的命令:
java -cp .:../../DictionaryServiceProvider/build dictionary.DictionaryDemo
你会看到下面的结果:
book: Cannot find definition for this word.
editor: Cannot find definition for this word.
xml: Cannot find definition for this word.
REST: Cannot find definition for this word.
这是因为,你的模块,SPI找不到.
所以,我们把这些模块加入到CLASSPATH中,执行那个下面的命令,看一下结果:
java -cp .:../../DictionaryServiceProvider/build:../../ExtendedDictionary/build/:../../GeneralDictionary/build/ dictionary.DictionaryDemo
结果如下:
book: a set of written or printed pages, usually bound with a protective cover
editor: a person who edits
xml: a document standard often used in web services, among other things
REST: an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer
这就大功告成啦.
总结
其实SPI也很简单,无非就是我们写一个接口,然后按照某种规范实现各个模块.