neuqdarcy

导航

Java SPI 到底是什么

一、Java 扩展机制

在介绍 SPI 机制之前,首先要了解 Java 的扩展机制(The extension mechanism)。“扩展机制” 指的是一种标准(或规范),通过遵循这种标准,用户可以自定义接口,达到丰富功能的目的。“扩展”的表现形式,就是一组 Java 包或者 Java 类。“扩展” 就像热拔插设备一样,Java 可以在运行时加载,而不需要提前在代码中声明扩展类的全类名或者路径。
img

二、SPI 介绍

SPI 就是 Java 扩展机制的具体实现方式。SPI 全称为 Service Provider Interface,其中

  • Service: 指提供某种功能的类或接口。Service 可以定义接口,然后获取对应的实现类。Service 本身并不会实现具体的功能,而是调用具体的实现,这些具体的实现就是 Service Provider。以词典功能为例,词典服务的功能就是,用户输入一个单词,服务输出查询结果,但是底层的中文、英文词库,并不是 Service 的范畴,
  • Service provider interface(SPI):指 Service 定义的接口或抽象类。
  • Service provider: SPI 的具体实现,也是为 Service 的底层依赖。Provider 通常有扩展的需求,比如在词典服务中新增德文、法文词库。

三、使用场景

Java SPI机制是一种服务提供者发现的机制,适用于需要在多个实现中选择一个进行使用的场景。

  1. 加载数据库驱动程序。JDBC为了实现可插拔的数据库驱动,在Java.sql.Driver接口中定义了一组标准的API规范,而具体的数据库厂商则需要实现这个接口,以提供自己的数据库驱动程序。在Java中,JDBC驱动程序的加载就是通过SPI机制实现的。
  2. Spring 框架。Spring框架中的Bean加载机制就使用了SPI思想,通过读取classpath下的META-INF/spring.factories文件来加载各种自定义的Bean。
  3. Dubbo 框架。Dubbo框架也使用了SPI思想,通过接口注解@SPI声明扩展点接口,并在classpath下的META-INF/dubbo目录中提供实现类的配置文件,来实现扩展点的动态加载。

四、应用实战

本小节介绍如何实现一个“词典服务”,通过示例来体会 SPI 的效果。代码已上传 github: https://github.com/neuqDarcy/java-spi-demo.
(例子来自于 Oracle 官网,用到了 ant, 有一些学习成本,笔者用 maven 重新实现了功能。)
简单介绍下功能背景,“词典服务”的功能是翻译词汇,针对不同的场景,比如通用词典、专业词典,我们可以分别实现对应的词库,而这些词库是可拔插的、容易扩展的,稍后可以看到效果。

项目结构

├── DictionaryDemo 客户端,可以调用和验证功能。
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │   └── java
│   │   ├── DictionaryDemo.java
│   │   └── dictionary
│   └── target
│   ├── classes
│   │   └── DictionaryDemo.class
│   └── generated-sources
│   └── annotations
├── DictionaryServiceProvider 实际上对应于上文提到的 Service
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │   └── java
│   │   ├── DictionaryService.java 对应于上文提到的 Service
│   │   └── dictionary
│   │   └── api
│   │   └── Dictionary.java 对应于上文提到的 Service Provider Interface(SPI)
│   └── target
│   ├── classes
│   │   ├── DictionaryService.class
│   │   └── dictionary
│   │   └── api
│   │   └── Dictionary.class
│   └── generated-sources
│   └── annotations
├── ExtendedDictionary 扩展词库1
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │   ├── java
│   │   │   └── dictionary
│   │   │   └── ExtendedDictionary.java 扩展类,对应于 Service Provider
│   │   └── resources
│   │   └── META-INF
│   │   └── services
│   │   └── dictionary.api.Dictionary 配置文件
│   └── target
│   ├── classes
│   │   ├── META-INF
│   │   │   └── services
│   │   │   └── dictionary.api.Dictionary
│   │   └── dictionary
│   │   └── ExtendedDictionary.class
│   └── generated-sources
│   └── annotations
├── GeneralDictionary 扩展词库2
│   ├── pom.xml
│   ├── src
│   │   └── main
│   │   ├── java
│   │   │   └── dictionary
│   │   │   └── GeneralDictionary.java 扩展类,对应于 Service Provider
│   │   └── resources
│   │   └── META-INF
│   │   └── services
│   │   └── dictionary.api.Dictionary
│   └── target
│   ├── classes
│   │   ├── META-INF
│   │   │   └── services
│   │   │   └── dictionary.api.Dictionary
│   │   └── dictionary
│   │   └── GeneralDictionary.class
│   └── generated-sources
│   └── annotations
└── pom.xml

步骤详解

1. 定义 SPI

package dictionary.spi;

public interface Dictionary {
    public String getDefinition(String word);
}

2. 定义服务类

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);
    }

}

3. 实现 Srvice Provider

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);
    }
}

4. 注册 Service Provider

在 META-INF/services 路径下(必须是这个路径),

  • 创建配置文件,文件名为 SPI 接口的全类名(必须)。本例中是 dictionary.api.Dictionary
  • 配置文件内容为,Service Provider 的全类名(必须)。本例中是 dictionary.ExtendedDictionary

5. 实现客户端

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;
    }
  }
}

六、验证功能

  1. 可以通过 maven 来控制是否加载扩展类。这里我们做对照实验, DictionaryDemo 引用 ExtendedDictionary 依赖,不引用 GeneralDictionary 依赖。
  2. 执行 DictionaryDemo.java 方法即可。
  3. 执行结果:
    book:Cannot find definition for this word!!
    editor:Cannot find definition for this word!!
    xml:可延伸标记语言
    REST:一种接口风格

从可以看到,ExtendedDictionary 正常提供服务,GeneralDictionary 没有提供服务。这是符合预期。
如果需要挂载 GeneralDictionary 也很简单,直接在 POM 文件中引用 GeneralDictionary 即可,不需要改动代码逻辑。

五、源码解析

Java 版本是 1.8

核心类

  1. java.util.ServiceLoader
  2. 内部类 LazyIterator。关注点比较多,先把整体代码贴上来。
// Private inner class implementing fully-lazy provider lookup
    //
    private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

关注点

  1. 还记得配置文件一定要放在 "META-INF/services/" 文件夹下吗,答案在这里。此处是“约定大于配置” 思想的体现。
private static final String PREFIX = "META-INF/services/";
  1. 还记得 SPI 常见的使用方式吗?是通过迭代器,获取到具体实现的。SPI 在内部实现了迭代器,其中有两点可以关注。一,在迭代器 next() 方法时才生成实现的对象,这种机制被称为“懒加载”。二,SPI 是通过反射的方式,根据“配置路径 + 配置内容”,生成实现类。
    2.1 SPI 的常见使用方式
    public String getDefinition(String word) {
        String definition = null;
        Iterator<Dictionary> dictionaries = loader.iterator();
        while (definition == null && dictionaries.hasNext()) {
            Dictionary d = dictionaries.next();
            definition = d.getDefinition(word);
        }
        return definition;
    }
    
    2.2 判断配置文件中的 Service Provider 是否存在
    private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
    
    2.3 通过反射,生成实现类
        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }   
    
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }
    

参考

  1. jdk 8
  2. jdk 17
  3. 博客: https://blog.csdn.net/qq_40915439/article/details/131566026?spm=1001.2014.3001.5501
  4. 官网 https://docs.oracle.com/javase/tutorial/ext/basics/spi.html https://docs.oracle.com/javase/tutorial/ext/index.html

posted on 2024-03-05 21:01  爱吃橘子的胖达  阅读(68)  评论(0编辑  收藏  举报