Java-SPI

Posted by AlstonWilliams on February 17, 2019

很久之前,在做项目时,想要做插件式开发,但是当时并没有想到怎么实现.今天,在研究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

运行

进入到DictionaryDemobuild目录,执行下面的命令:

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也很简单,无非就是我们写一个接口,然后按照某种规范实现各个模块.