java SPI 01-SPI 是什么?spi 使用入门教程 ServiceLoader 使用简介

系列目录

spi 01-spi 是什么?入门使用html

spi 02-spi 的实战解决 slf4j 包冲突问题java

spi 03-spi jdk 实现源码解析git

spi 04-spi dubbo 实现源码解析github

spi 05-dubbo adaptive extension 自适应拓展sql

spi 06-本身从零手写实现 SPI 框架apache

spi 07-自动生成 SPI 配置文件实现方式编程

问题引入

之前一直想指定一套标准,让别人按照这个标准来实现,并编写好对应的容器。oracle

而后我在代码中动态获取这些实现,让代码运行起来。框架

困难

如何获取某个接口的实现?maven

初步方案

和同事讨论,是经过扫描包的 class 的方式。而后判断是否为定制标准的子类。

  • 缺点

以为很别扭,须要限定死实现类的包名称,并且性能也较差。

SPI 的解决方式

今天在阅读 hibernate-validator 源码时受到了启发。

能够经过 SPI 的方式,更加天然的解决这个问题。

SPI 是 Service Provider Interfaces 的缩写。

本文简单介绍下如何使用,具体原理,暂时不作深究。

SPI 是什么

SPI 是 Java 提供的一种服务加载方式,全名为 Service Provider Interface。

根据 Java 的 SPI 规范,咱们能够定义一个服务接口,具体的实现由对应的实现者去提供,即服务提供者。

而后在使用的时候再根据 SPI 的规范去获取对应的服务提供者的服务实现。

经过 SPI 服务加载机制进行服务的注册和发现,能够有效的避免在代码中将具体的服务提供者写死。从而能够基于接口编程,实现模块间的解耦。

SPI 机制的约定

  1. 在 META-INF/services/ 目录中建立以接口全限定名命名的文件,该文件内容为API具体实现类的全限定名

  2. 使用 ServiceLoader 类动态加载 META-INF 中的实现类

  3. 如 SPI 的实现类为 Jar 则须要放在主程序 ClassPath 中

  4. API 具体实现类必须有一个不带参数的构造方法

应用场景举例

SPI 应用场景举例

  • JDBC

jdbc4.0之前, 开发人员还须要基于Class.forName("xxx")的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,能够经过METAINF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.

  • COMMON-LOGGING

apache最先提供的日志的门面接口。只有接口,没有实现。

具体方案由各提供商实现,发现日志提供商是经过扫描METAINF/services/org.apache.commons.logging.LogFactory配置文件,经过读取该文件的内容找到日志提工商实现类。

只要咱们的日志实现里包含了这个文件,并在文件里制定 LogFactory 工厂接口的实现类便可。

简单实现

文件目录

.
├── java
│   └── com
│       └── github
│           └── houbb
│               └── forname
│                   ├── Say.java
│                   ├── Sing.java
│                   └── impl
│                       ├── DefaultSay.java
│                       └── DefaultSing.java
└── resources
    └── META-INF
        └── services
            └── com.github.houbb.forname.Say

定义接口和实现

  • Say.java
public interface Say {

    /**
     * 说
     */
    void say();

}
  • DefaultSay.java
import com.github.houbb.forname.Say;

public class DefaultSay implements Say {

    @Override
    public void say() {
        System.out.println("Default say");
    }

}

编写 services 实现指定

在 resources 目录下,建立 META-INF/services 文件夹,以接口全路径名 com.github.houbb.forname.Say 为文件名称,内容为对应的实现类全路径。

若是是多个,就直接换行隔开。

  • com.github.houbb.forname.Say
com.github.houbb.forname.impl.DefaultSay

测试

  • SayTest.java
public class SayTest {

    @Test
    public void spiTest() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ServiceLoader<Say> loader = ServiceLoader.load(Say.class, classLoader);

        for (Say say : loader) {
            say.say();
        }
    }

}
  • 测试结果
Default say

简单总结

Java 中,能够经过 ServiceLoader 类比较方便的找到该类的全部子类实现。

META-INF/services 下的实现指定和实现子类实现彻底能够和接口定义彻底分开。

麻烦的地方

每次都要手动建立实现指定文件,比较繁琐。

Auto 就为解决这个问题而生。

Auto 版本演示

maven 引入

<dependencies>
    <dependency>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>1.0-rc4</version>
        <optional>true</optional>
    </dependency>
</dependencies>

接口和定义

  • Sing.java
public interface Sing {

    /**
     * 唱歌
     */
    void sing();

}
  • DefaultSing.java
@AutoService(Sing.class)
public class DefaultSing implements Sing {

    @Override
    public void sing() {
        System.out.println("Sing a song...");
    }

}

测试

  • SingTest.java
public class SingTest {

    @Test
    public void spiTest() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ServiceLoader<Sing> loader = ServiceLoader.load(Sing.class, classLoader);

        for (Sing sing : loader) {
            sing.sing();
        }
    }

}
  • 结果
Sing a song...

简单总结

经过 google 的 auto,能够在编译时自动为咱们生成对应的接口实现指定文件。

在 target 对应的文件下能够看到。

实现原理,也相对简单。经过 java 的编译时注解,生成对应的文件便可。

实际上诸如 dubbo 等框架,会利用 SPI 机制来提高项目总体的灵活性。

java 自带的 SPI 有不少不足的地方,本系列就是要学习使用,而且实现本身加强的 SPI 框架。

源码地址

SPI 源码

参考资料

Oracle SPI-intro

google auto

详解 JAVA SPI 机制和使用方法