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

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 你知道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机制的支持,如果对大家有帮助,请留下一个赞。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
28天前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
52 1
|
1月前
|
开发框架 Java API
java反射机制的原理与简单使用
java反射机制的原理与简单使用
17 1
|
2天前
|
存储 安全 Java
[Java基础面试题] Map 接口相关
[Java基础面试题] Map 接口相关
|
2天前
|
Java
[Java 面试题] ArrayList篇
[Java 面试题] ArrayList篇
|
2天前
|
Java 数据库连接
深入理解Java异常处理机制
【4月更文挑战第24天】本文将探讨Java中的异常处理机制,包括异常的概念、分类、捕获和抛出等方面。通过深入了解异常处理机制,可以帮助我们编写更加健壮的程序,提高代码的可读性和可维护性。
|
2天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
28 1
|
3天前
|
存储 安全 Java
每日一道Java面试题:说一说Java中的泛型?
今天的每日一道Java面试题聊的是Java中的泛型,泛型在面试的时候偶尔会被提及,频率不是特别高,但在日后的开发工作中,却是是个高频词汇,因此,我们有必要去认真的学习它。
15 0
|
3天前
|
Java 编译器
每日一道Java面试题:方法重载与方法重写,这把指定让你明明白白!
每日一道Java面试题:方法重载与方法重写,这把指定让你明明白白!
14 0
|
7天前
|
XML 缓存 Java
Java大厂面试题
Java大厂面试题
18 0
|
7天前
|
存储 安全 Java
Java大厂面试题
Java大厂面试题
14 0