> 文档中心 > Java SPI 浅析一二

Java SPI 浅析一二

文章目录

  • Java SPI
    • 1. 为何需要SPI
    • 2. 默认约定
    • 3. 使用案例
      • 3.1 common-logging
      • 3.2 Spring
      • 3.3 JDBC
      • 3.4 Eclipse插件
    • 4. 自己用ServiceLoader实现SPI
    • 5. ServiceLoader源码
  • 总结

Java SPI

SPI : Service Provider Interface 为某个接口寻找服务实现的机制。

1. 为何需要SPI

基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

Usually API and SPI are separate. For example, in JDBC the Driver class is part of the SPI: If you simply want to use JDBC, you don’t need to use it directly, but everyone who implements a JDBC driver must implement that class.

2. 默认约定

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。

3. 使用案例

3.1 common-logging

common-logging apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,来发现日志实现类。只要我们的日志实现里包含了这个文件,并在文件里指定了 LogFactory工厂接口的实现类即可。

3.2 Spring

在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。

3.3 JDBC

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。

  • JDBC接口定义
    首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。

  • mysql实现
    在mysql的jar包中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。

  • postgresql实现
    同样在postgresql的jar包中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。

3.4 Eclipse插件

Eclipse使用OSGi作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。 一般来说,插件的文件结构必须在指定目录下包含以下三个文件: META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等.

4. 自己用ServiceLoader实现SPI

定义一个接口和相关的实现类

public interface Save {    void save(String message);}public class FileSaver implements Save{    @Override    public void save(String message) { System.out.println("saveit in file");    }}public class DBSaver implements Save{    @Override    public void save(String message) { System.out.println("saveit in DB");    }}

在Resource目录下建目录 META-INF/services/ 和文件 com.spi.Save文件。注意文件名就是接口的全路径名称。
Java SPI 浅析一二
然后在文件内写明实现类。

com.spi.DBSavercom.spi.FileSaver

这样子在Main里实现接口实现类的自动查找与实例化。

public class Main {    public static void main(String[] args) { ServiceLoader<Save> load = ServiceLoader.load(Save.class); Iterator<Save> iterator = load.iterator(); while (iterator.hasNext()){     Save save = iterator.next();     save.save("Hello"); }    }}

运行结果:

saveit in DBsaveit in file

5. ServiceLoader源码

ServiceLoader类内的迭代器有一个LazyIterator,其中会根据文本加载类:
c = Class.forName(cn, false, loader);并且实例化:S p = service.cast(c.newInstance());

  private class LazyIterator implements Iterator<S>    {  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 }}

总结

SPI将装配的控制权移到了程序之外。
只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想。

java基础 系列在github上有一个开源项目,主要是本系列博客的demo代码。https://github.com/forestnlp/javabasic
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。

钢筋混凝土切割网