Java 的 SPI 机制

java,spi,机制 · 浏览次数 : 62

小编点评

**SPI机制**是一种服务发现机制,它允许应用程序在运行时动态加载其他应用程序的实现类,从而实现可插拔的功能。 **服务发现机制**是一种技术,它允许应用程序在运行时动态找到和加载其他应用程序的实现类。这有助于减少应用程序的启动时间,并使应用程序更容易维护。 **SPI 的关键概念:** * **接口:**定义了一个操作的 contract,指定如何实现该操作。 * **实现类:**实现接口的类,实现操作。 * **服务提供者:**提供接口的类,并实现接口的方法。 * **服务消费者:**使用服务提供者的类,调用其提供的方法。 **SPI 的优点:** * **可插拔:**开发人员可以添加或删除插件,而无需修改应用程序代码。 * **解耦:**服务提供者和服务消费者之间不存在依赖关系。 * **可维护性:**代码更容易维护,因为开发人员可以添加或删除插件而无需修改其他代码。 **SPI 的应用:** SPI 在 Java 中非常广泛使用,因为它提供了一种简单易用的机制来实现可插拔的功能。一些使用 SPI 的框架包括: * **Eclipse 和 IntelliJ IDEA:**通过 SPI 接口实现插件安装和卸载。 * **Spring Boot:**自动配置服务提供者和服务消费者,并使用 SPI 接口实现可插拔。 **SPI 和 API 的区别:** * SPI 是一个服务发现机制,它允许应用程序动态加载其他应用程序的实现类。 * API 是一个面向对象的接口,它定义了服务提供者和服务消费者之间的操作。

正文

什么是SPI机制?

SPI机制( Service Provider Interface)是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制.
SPI机制在Java中有很广泛的运用,比如:eclipse和idea里的插件使用就是通过SPI机制实现的。开发工具提供一个扩展接口,具体的实现由插件开发者实现,开发工具提供一种服务发现机制来找到具体插件的实现,这就达到了插件的安装效果。从而可以使用插件服务。如果不需要某一插件,只需要删除某一插件的实现类,开发工具找不到具体的插件实现,这就达到了插件的卸载效果。不管是安装还是卸载都不会影响其他代码,其他服务。非常方便的实现了可插拔的效果。

JDBC中数据库连接驱动也使用了SPI机制,来达到适配不同DB数据库的效果。

SPI机制除了在jdk里有运用,在springboot中也用到了。springboot自动装配中"查找spring.factories 文件步骤"就是基于SPI的部分设计思想实现的。

SPI 有如下的好处:

不需要改动源码就可以实现扩展,解耦。
实现扩展对原来的代码几乎没有侵入性。
只需要添加配置就可以实现扩展,符合开闭原则。

API 和 SPI 区别

API:大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用。
SPI :是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。
image

SPI实现服务接口与服务实现的解耦:

  • 服务提供者(如 springboot starter)提供出 SPI 接口,让客户端去自定义实现。
  • 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔。
    image

简单实现

定义接口

package com.test.service;

public interface ISpi {
    void say();
}

第一个实现类:

package com.test.service.impl;

import com.test.service.ISpi;

public class FirstSpiImpl implements ISpi {

    @Override
    public void say() {
        System.out.println("我是第一个SPI实现类");
    }
}

第二个实现类:

package com.test.service.impl;

import com.test.service.ISpi;

public class SecondSpiImpl implements ISpi {

    @Override
    public void say() {
        System.out.println("我是第二个SPI实现类");
    }
}

在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名,并写上需要动态加载的实现类的全路径名。

#com.test.service.impl.FirstSpiImpl
com.test.service.impl.SecondSpiImpl

ServiceLoader

ServiceLoader是JDK提供的专门用于实现SPI机制的类。位于java.util.ServiceLoader
ServiceLoader类的构造函数被私有化了。所以构建ServiceLoader对象只能通过ServiceLoader.load()方法。该方法有两个重载
使用ServiceLoader时可选择是否用自定义类加载器来加载目标类。也可默认使用应用程序类加载器加载。

jdk通过ServiceLoader类去ClassPath下的 “META-INF/services/”(此路径约定成俗) 路径里查找相应的接口实现类。ServiceLoader类核心功能就两个点,都在ServiceLoader的内部类LazyIterator中:

  • 查找相应接口对应实现类:hasNextService()
  • 加载相应接口实现类到虚拟机内:nextService()
public final class ServiceLoader<S> implements Iterable<S> {


    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;

    // 上下文对象
    private final AccessControlContext acc;

    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类 扫描目录前缀(META-INF/services/)+ 相应接口全限定名
                    String fullName = PREFIX + service.getName();
                    //该loader是构造ServiceLoader类时设置。可传入自定义类加载器,如未传入,则默认应用程序类加载器  
                    if (loader == null)
                        //在系统中查找资源,注意查找资源的加载器是从当前线程上下文中获取。也就是默认的应用程序类加载器。所以能加载到第三方jar包下的classpath路径。
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

应用程序通过迭代器接口获取对象实例,这里首先会判断 providers 对象中是否有实例对象:

  • 有实例,那么就返回
  • 没有,执行类的装载步骤,具体类装载实现如下:

LazyIterator#hasNextService 读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析
LazyIterator#nextService 负责实例化 hasNextService() 读到的实现类,并将实例化后的对象存放到 providers 集合中缓存
image
image

应用案例

Java定义了一套JDBC的接口,但并未提供具体实现类,而是在不同厂商提供的数据库实现包。

一般要根据自己使用的数据库驱动jar包,比如我们最常用的MySQL,其mysql-jdbc-connector.jar 里面就有:
image

小结

JDK中的SPI实现,是由ServiceLoader类根据自定义传入类加载器或者应用程序类加载器在约定好的固定路径下(ClassPath:META-INF/services/)去查找和加载第三方接口实现类。

注意:要使用JDK中的SPI机制有几个前提条件

  • 服务提供方必须实现目标接口
  • 服务提供方必须在自身ClassPath:META-INF/services/路径下建立文件,文件名为目标接口全限定名。文件内容为实现目标接口的具体实现类全限定名

与Java 的 SPI 机制相似的内容:

Java 的 SPI 机制

什么是SPI机制? SPI机制( Service Provider Interface)是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制.

Spring的Factories机制介绍

Java 的 SPI 机制 Java SpringBoot 加载 yml 配置文件中字典项 Spring的Factories就是Spring版本的Java Spi。 Spring Factories的最重要的功能就是:可以通过配置文件指定Spring容器加载一些特定的组件。 Spring Facto

深入浅出:SPI机制在JDK与Spring Boot中的应用

本文深入探讨了Spring Boot中与Java的标准SPI相似的机制,揭示了它的工作原理、应用场景及与标准SPI的异同。

深入理解java和dubbo的SPI机制

1 SPI简介 1.1 SPI(Service Provider Interface) 本质:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。 java SPI:用来设计给服务提供商做插件使用的。基于策略模式来实现动态加载的机制。我

SPI在Java中的实现与应用 | 京东物流技术团队

1 SPI的概念 API API在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实现类的实例。 简单形容就是这样的: 图1:API 如上图

源码级深度理解 Java SPI

本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。

OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的案例

背景 前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。 但这个不生效的前置条件有点多: JDK 的版本得在 18+ Spri

OpenTelemetry agent 对 Spring Boot 应用的影响:一次 SPI 失效的

背景 前段时间公司领导让我排查一个关于在 JDK21 环境中使用 Spring Boot 配合一个 JDK18 新增的一个 SPI(java.net.spi.InetAddressResolverProvider) 不生效的问题。 但这个不生效的前置条件有点多: JDK 的版本得在 18+ Spri

Java的深浅拷贝认识

目录浅拷贝深拷贝分辨代码里的深浅拷贝 在Java中,深拷贝和浅拷贝是对象复制的两种方式,主要区别在于对对象内部的引用类型的处理上。 浅拷贝 定义: 浅拷贝是指创建一个新的对象,但这个新对象的属性(包括引用类型的属性)仍然指向原来对象的属性。换言之,如果原对象中的属性是一个引用类型,那么浅拷贝只会复制

Java火焰图简单学习

前言 立的flags倒了太多次 自己还是太菜了.. 课题太大, 自己简单总结一下. 要是自己总结错了. 就不收费, 错就错了 !-_-! 第一步准备环境 一定要设置对java的HOME以及PATH路径. 如果 which java 不在 自己定义的PATH里面,建议改名. 避免程序出奇奇怪怪的问题.