Java SPI

作者: AlstonWilliams | 来源:发表于2017-11-23 21:27 被阅读36次

很久之前,在做项目时,想要做插件式开发,但是当时并没有想到怎么实现.今天,在研究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也很简单,无非就是我们写一个接口,然后按照某种规范实现各个模块.

相关文章

  • Java - SPI机制

    Java - SPI机制 SPI是什么 SPI全称Service Provider Interface,是Java...

  • dubbo 源码分析 -SPI

    dubbo 中大量使用SPI,在看源码之前必须先了解dubbo的SPI 1、Java原生spi java原生spi...

  • spi

    java中有一些包结尾为spi,如java.nio.channels.spi spi - service prov...

  • 4.Dubbo的SPI扩展点加载机制

    4.1 加载机制概述 4.1.1 Java SPI 在讲Dubbo SPI之前,先来了解一下Java SPI,SP...

  • Dubbo SPI的认识

    Dubbo是基于Java原生SPI机制思想的一个改进. 关于JAVA 的SPI机制 SPI全称(service p...

  • Dubbo SPI 的使用方法(一)- 扩展点自动包装

    开篇 前面有说到 Java SPI 的介绍与使用方法, 而本篇要说的 Dubbo SPI 是基于 Java SPI...

  • dubbo spi机制源码阅读

    dubbo的扩展能力很强大。他是通过扩展Java的spi机制得到的。 Java Spi机制介绍 SPI是Servi...

  • dubbo的spi机制

    dubbo的spi机制 dubbo的扩展点加载机制源自于java的spi扩展机制。那么,何为java的spi扩展机...

  • dubbo的内核剖析

    SPI机制 Java SPI SPI(service provider interface),在运行时,动态加载接...

  • SPI实现接口多实现路由

    SPI简介 SPI是Service Provider Interfaces的简称。根据Java的SPI规范,我们可...

网友评论

      本文标题:Java SPI

      本文链接:https://www.haomeiwen.com/subject/txdtbxtx.html