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

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 你知道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机制的支持,如果对大家有帮助,请留下一个赞。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
4月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
7月前
|
缓存 Java 关系型数据库
2025 年最新华为 Java 面试题及答案,全方位打造面试宝典
Java面试高频考点与实践指南(150字摘要) 本文系统梳理了Java面试核心考点,包括Java基础(数据类型、面向对象特性、常用类使用)、并发编程(线程机制、锁原理、并发容器)、JVM(内存模型、GC算法、类加载机制)、Spring框架(IoC/AOP、Bean生命周期、事务管理)、数据库(MySQL引擎、事务隔离、索引优化)及分布式(CAP理论、ID生成、Redis缓存)。同时提供华为级实战代码,涵盖Spring Cloud Alibaba微服务、Sentinel限流、Seata分布式事务,以及完整的D
407 1
|
6月前
|
缓存 Java API
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
528 0
|
6月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
298 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
4月前
|
算法 Java
50道java基础面试题
50道java基础面试题
|
6月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
289 4
|
7月前
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
214 5
|
7月前
|
安全 Java API
2025 年 Java 校招面试常见问题及详细答案汇总
本资料涵盖Java校招常见面试题,包括Java基础、并发编程、JVM、Spring框架、分布式与微服务等核心知识点,并提供详细解析与实操代码,助力2025校招备战。
358 1
|
6月前
|
缓存 Java 关系型数据库
Java 面试经验总结与最新 BAT 面试资料整理含核心考点的 Java 面试经验及最新 BAT 面试资料
本文汇总了Java面试经验与BAT等大厂常见面试考点,涵盖心态准备、简历优化、面试技巧及Java基础、多线程、JVM、数据库、框架等核心技术点,并附实际代码示例,助力高效备战Java面试。
232 0