你知道JAVA中的SPI机制吗,被面试官问到了

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 你知道JAVA中的SPI机制吗,被面试官问到了

前言


最近面试的过程被问到SPI机制,一脸懵逼,虽然听说过,但是也仅限于听说,无法用自己的话将SPI这个东西讲清楚,本文就重新讲讲SPI是怎么一回事,大家都知道吗?


SPI是个什么鬼


简介


SPI全称叫做Service Provider Interface,服务提供接口。它是是Java内置的一种服务提供发现机制,可以用来提高框架的扩展性。

怎么理解呢?简单理解就是在上层模块或者核心接口层中定义一个标准的服务接口,它没有实现,那谁来实现呢?有下层模块或者各个不同的服务提供方去实现这样的标准接口,如下图所示:

1671196802649.jpg

区别于API的概念:

API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。


应用案例

如果还是没不理解的话,我这边举个非常常见的应用例子帮助大家理解。

我们都有用过jdbc连接数据吧,连接数据库需要驱动包,不同的数据库厂商,比如mysql或者oracle是不一样的。我们总不能在jdk中定义囊括所有的厂商实现吧,如配置是mysql,调用mysql的驱动,是oracle,调用oracle驱动,还要把各个厂商的驱动都内置进去,是不是非常ugly, 这时候我们的SPI机制派上用场了。

  1. 定义内置服务接口

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

  1. 不同产商各自实现内置服务接口
  • mysql厂商实现

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

  • postgresql厂商实现

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

等等,其他厂商类似。

  1. 使用实现

调用下面的方法,就会将对应的实现驱动通过SPI机制加载进来。

Connection conn = DriverManager.getConnection(url,username,password);

它是如何做到的呢?我们看下DriverManager的源码:

public class DriverManager {
    ......
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    .....
  private static void loadInitialDrivers() {
        String drivers;
        .....
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
          //使用SPI的ServiceLoader来加载接口的实现
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    }
}
  • DriverManager类加载会先调用静态代码块中的loadInitialDrivers()方法。
  • loadInitialDrivers()方法中的ServiceLoader.load()方法就是jdk中对spi的实现,具体源码解读查看下一个章节。

小结:

此外还有一些其他使用SPI的例子,如下:

  • 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等


核心思想


通过上面的介绍和例子的讲解,我们可以知道SPI核心的思想就是解耦。SPI机制将服务的具体实现转移到了程序外,为框架的扩展和解耦提供了极大的便利。


JDK中SPI实现


使用


要使用jdk的SPI需要,需要遵循如下约定:

  1. 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的jar包放在主程序的classpath中;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  4. SPI的实现类必须携带一个不带参数的构造方法;

直接上例子:

  1. 定义一个服务接口
public interface Phone {
    String getSystemInfo();
}
  1. 定义服务接口的实现
public class Huawei implements Phone {
    @Override
    public String getSystemInfo() {
        return "Hong Meng";
    }
}
public class IPhone implements Phone {
    @Override
    public String getSystemInfo() {
        return  "iOS";
    }
}
  1. 添加配置
  • 目录名必须是 META-INF/services
  • 文件名是接口的全路径

1671196832866.jpg

  1. 使用
  • 通过ServiceLoader 加载实现类并调用服务
@Test
public void test1() {
    // 调用ServiceLoader的load方法
    ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class);
    Iterator<Phone> iterator = phoneServiceLoader.iterator();
    while (iterator.hasNext()) {
        Phone phone = iterator.next();
        System.out.println("phone ....");
        if(phone != null) {
            String systemInfo = phone.getSystemInfo();
            System.out.println(systemInfo);
        }
    }
}

1671196842199.jpg


源码实现


jdk中的SPI机制是通过ServiceLoader类实现的,通过调用load()方法实现对服务提供接口的查找,最后遍历来逐个访问服务提供接口的实现类。

public final class ServiceLoader<S> implements Iterable<S>{
    // 服务提供接口对应文件放置目录
    private static final String PREFIX = "META-INF/services/";
    // The class or interface representing the service being loaded
    private final Class<S> service;
    // 类加载器
    private final ClassLoader loader;
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
    // 按照初始化顺序缓存服务提供接口实例
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 内部类,实现了Iterator接口
    private LazyIterator lookupIterator;
private class LazyIterator
        implements Iterator<S>
    {
        ... 
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    // 获取全路径地址
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        // 获取文件的内容
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
        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
        } 
        .... 
    }
}
  • ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNextnext方法。这里主要都是调用的lookupIterator的相应hasNextnext方法。
  • LazyIterator中的hasNext方法,静态变量PREFIX就是“META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。
  • 最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,然后返回实例对象。


小结


JDK中的SPI机制思想非常好,很好的实现了解耦,但是也存在一些缺点:

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是线程不安全的。
  • 实现类必须要提供无参的构造方法。


Spring中SPI实现


因为jdk中的SPI实现存在着一些不完美的地方,所有Spring借鉴了jdk的思想,自己实现了一套SPI机制。


使用


  1. 创建一个服务接口
public interface Phone {
    String getSystemInfo();
}
  1. 创建两个服务实现类
public class Huawei implements Phone {
    @Override
    public String getSystemInfo() {
        return "Hong Meng";
    }
}
public class IPhone implements Phone {
    @Override
    public String getSystemInfo() {
        return  "iOS";
    }
}
  1. 添加配置
  • 在项目META-INF/目录下创建spring.factories文件
  • 文件内容key是接口的全限定名,value是实现类

1671196870508.jpg

#key是接口的全限定名,value是接口的实现类
com.alvin.error.spi.Phone=com.alvin.error.spi.Huawei,com.alvin.error.spi.IPhone
  1. 使用
  • 调用SpringFactoriesLoader.loadFactories方法加载
@Test
public void test() {
    // 调用SpringFactoriesLoader.loadFactories方法加载Phone接口所有实现类的实例
    List<Phone> spis = SpringFactoriesLoader.loadFactories(Phone.class,
            Thread.currentThread().getContextClassLoader());
    // 遍历Phone接口实现类实例
    for (Phone spi : spis) {
        System.out.println(spi.getSystemInfo());;
    }
}

1671196882669.jpg


源码实现


Spring中使用SpringFactoriesLoader类实现了对SPI机制的支持。

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得资源文件的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 遍历所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 组装数据,并返回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}
  • 从classpath下的每个Jar包中搜寻所有META-INF/spring.factories配置文件
  • 然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在classpath下的jar包中
  • 最后将扫描得到的类通过反射实例化


总结


SPI技术将服务接口与服务实现分离以达到解耦,极大的提升程序的可扩展性。本文主要带大家理解了什么是SPI,然后分析了JDK和Spring中对SPI机制的支持,如果对大家有帮助,请留下一个赞。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
23天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
60 2
|
27天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
11天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
35 14
|
28天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
29天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
52 4
|
26天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
86 4
|
1月前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
60 2
|
1月前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
29 3