Java SPI - ServiceLoader 使用简介

问题引入

以前一直想指定一套标准,让别人按照这个标准来实现,并编写好对应的容器。
然后我在代码中动态获取这些实现,让代码运行起来。
类似于写个 windows 环境,让开发者自己开发对应的软件。

困难

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

  • 初步方案
    和同事讨论,是通过扫描包的 class 的方式。然后判断是否为定制标准的子类。

  • 缺点
    觉得很别扭,需要限定死实现类的包名称,而且性能也较差。

SPI 的解决方式

今天在阅读 hibernate-validator 源码时受到了启发。
可以通过 SPI 的方式,更加自然的解决这个问题。

SPI

SPI 是 Service Provider Interfaces 的缩写。

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

简单实现

文件目录

.
├── 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
/**
 * <p> 接口 </p>
 *
 * <pre> Created: 2018/5/27 上午10:36  </pre>
 * <pre> Project: tech-validation  </pre>
 *
 * @author houbinbin
 * @version 1.0
 * @since JDK 1.7
 */
public interface Say {

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

}
  • DefaultSay.java
package com.github.houbb.forname.impl;

import com.github.houbb.forname.Say;

/**
 * <p> </p>
 *
 * <pre> Created: 2018/5/27 上午10:37  </pre>
 * <pre> Project: tech-validation  </pre>
 *
 * @author houbinbin
 * @version 1.0
 * @since JDK 1.7
 */
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 版本

jar 的引入

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

接口和定义

  • Sing.java
/**
 * <p> 接口 </p>
 *
 * <pre> Created: 2018/5/27 上午10:36  </pre>
 * <pre> Project: tech-validation  </pre>
 *
 * @author houbinbin
 * @version 1.0
 * @since JDK 1.7
 */
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 的编译时注解,生成对应的文件即可。

项目源码

github spi

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 103,789评论 13 125
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 25,939评论 18 394
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 3,873评论 1 62
  • 一首民谣,一把吉他,一段旅程,一个故事。 夜静的只剩下民谣和自己,这几天练琴练的手疼,却依旧不舍。 也许越老越开始...
    筱雨天阅读 65评论 0 2
  • 图解HTTP 状态码的类别 2XX成功 2xx的响应结果表示请求被正常处理了 3XX重定向 3XX的响应结果表示浏...
    rxdxxxx阅读 306评论 0 0