5

Java SPI 和 API,傻傻分不清? - 阿丸

 2 years ago
source link: https://www.cnblogs.com/awan-note/p/16227498.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近新写了一个中间件「运行时动态日志等级开关」,其中使用Java SPI机制实现了自定义配置中心,保证良好的扩展性。

项目地址,走过路过可以点个star :)

https://github.com/saigu/LogLevelSwitch

在使用过程中,突然发现SPI其实和日常写API接口,然后进行implements实现非常相似,那SPI到底和普通API实现有啥区别呢?

Java SPI 和 API,傻傻分不清?

带着这个问题,我们一起来梳理下SPI机制吧。

本文预计阅读时间10分钟,将围绕以下几点展开:

  • 什么是 SPI 机制?
  • SPI 实践案例
  • SPI 和 API 有啥区别?

1、什么是SPI机制?

SPI(Service Provider Interface) 字面意思是服务提供者接口,本质上是一种「服务扩展机制」。

为什么需要这样一种「服务扩展机制」呢?

因为系统里抽象的各个模块,比如日志模块、xml解析模块、jdbc模块等,往往有很多不同的实现方案。

为了满足可拔插的原则,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。这就需要一种「服务扩展机制」,然后就有了SPI。

SPI 机制为我们的程序提供拓展功能。而不必将框架的一些实现类写死在代码里面。我们在相应配置文件中定义好某个接口的实现类全限定名,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

最常见的就是Java的SPI机制,另外,还有Dubbo和SpringBoot自定义的SPI机制。

2、SPI实践案例

2.1 业界SPI实践案例

简单了解了SPI的概念,我们看看业界有哪些SPI实践案例,如何利用SPI实现灵活扩展的。

  • JDBC驱动加载

最常见的SPI机制实践案例就是JDBC的驱动加载。利用Java的SPI机制,我们可以根据不同的数据库厂商来引入不同的JDBC驱动包。

  • SpringBoot的SPI机制

用过SpringBoot的同学应该都知道,我们可以在spring.factories中加上我们自定义的自动配置类,这个特性尤其在xxx-starter中应用广泛。

  • Dubbo的SPI机制

Dubbo基本上自身的每个功能点都提供了扩展点,把SPI机制应用的淋漓尽致,比如提供了集群扩展、路由扩展和负载均衡扩展等差不多接近30个扩展点。
如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现替换掉Dubbo的实现即可。

2.2 在实际项目中如何使用

以上三个例子是业界最常见的SPI机制的实现。下面,来看看我在实际项目中如何利用Java SPI机制实现了自定义配置中心,保证良好的扩展性。

项目地址,走过路过可以点个star :)

https://github.com/saigu/LogLevelSwitch

需求很简单,中间件「运行时动态日志等级开关」需要在应用运行时获取开关状态,然后动态改变应用日志等级。

如何获取开关状态呢?我们一般需要配置中心来进行处理。作为一个开源中间件,使用它的应用可能有自己的不同的配置中心(比如Nacos、Apollo、spring cloud config、自研配置中心等),因此,必须支持自定义配置中心接入。

这时候就需要SPI机制来实现了!

1)定义接口interface

package io.github.saigu.log.level.sw.listener;

public interface ConfigListener<T> {
    /**
     * 获取初始开关状态
     * @return initial context of switch
     */
    SwitchContext getInitSwitch();

    /**
     * 获取变化的配置
     * @param changedConfig changed config context
     */
    void listenChangedConfig(T changedConfig);
}

2)SPI加载

本项目通过Java SPI实现,不需要依赖额外的组件,通过ServiceLoader来动态加载

public class ChangeListenerFactory {
    public static ConfigListener getListener() {
        final ServiceLoader<ConfigListener> loader = ServiceLoader
                .load(ConfigListener.class);
        for (ConfigListener configListener : loader) {
            return configListener;
        }
        throw new IllegalArgumentException("please choose valid listener");
    }
}

3)应用自定义配置中心接入

使用这个中间件的应用,只需要三步即可接入自定义配置中心。

  • STEP 1: 应用中pom引入依赖
<dependency>
  <groupId>io.github.saigu</groupId>
  <artifactId>log-switch-core</artifactId>
  <version>1.0.0-beta</version>
</dependency>
  • STEP 2: 构建config Bean
@Configuration
public class LogLevelSwitchConfig {
    @Bean
    LogLevelSwitch logLevelSwitch() {
        return new LogLevelSwitch();
    }
}
  • STEP 3: 接入配置中心

声明配置中心的SPI实现。

在resource路径下新建 META-INF/services,创建文件名为
io.github.saigu.log.level.sw.listener.ConfigListener的文件,并写入自定义配置中心的「实现类名」。

3、SPI和API有啥区别?

我们已经介绍了什么是SPI,怎么使用SPI机制,现在,回头来看看一开始提出的问题,SPI和API有啥区别呢?

它们都需要定义接口interface,然后自定义实现类implements,看起来基本一致呀。

区别在哪?各自的使用场景是啥?

别急,我们从头梳理一下。

Java SPI 和 API,傻傻分不清?

从「面向接口编程」的思想来看,「调用方」应该通过调用「接口」而不是「具体实现」来处理逻辑。那么,对于「接口」的定义,应该在「调用方」还是「实现方」呢?

理论上来说,会有三种选择:

  • 「接口」定义在「实现方」
  • 「接口」定义在「调用方」
  • 「接口」定义在 独立的包中

1)「接口」定义在「实现方」

先来看看「接口」定义在「实现方」的情况。这个很容易理解,实现方同时提供了「接口」和「实现类」,「调用方」可以引用接口来达到调用某实现类的功能,这就是我们日常使用的API。API的最显著特征就是:

实现和接口在一个包中。自己定义接口,自己实现类。

2)「接口」定义在「调用方」

再来看看「接口」属于「调用方」的情况。这个其实就是SPI机制。以JDBC驱动为例,「调用方」(用户或者说JDK)定义了java.sql.Driver接口,这个接口位于「调用方」JDK的包中,各个数据库厂商实现了这个接口,比如mysql驱动com.mysql.jdbc.Driver。因此,SPI最显著的特征就是:

「接口」在「调用方」的包,「调用方」定义规则,而自定义实现类在「实现方」的包,然后把实现类加载到「调用方」中。

3)「接口」定义在独立的包

最后一种情况,如果一个「接口」在一个上下文是API,在另一个上下文是SPI,那么就可以把「接口」定义在独立的包中。

本文介绍了是SPI机制,然后结合业界案例与项目实践来说明SPI的使用场景,最后对Java SPI和API的区别进行了分析。

本文不对SPI原理进行深入解析,下一篇文章会详细分析下Java SPI的实现《大名鼎鼎的Java SPI机制,究竟有没有破坏双亲委派呢?》,应该会挺有意思,欢迎关注。

都看到最后了,原创不易,点个关注,点个赞吧~
文章持续更新,可以微信搜索「阿丸笔记 」第一时间阅读,回复【笔记】获取Canal、MySQL、HBase、JAVA实战笔记,回复【资料】获取一线大厂面试资料。
知识碎片重新梳理,构建Java知识图谱:github.com/saigu/JavaK…(历史文章查阅非常方便)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK