什么是SPI

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

SPI示例

定义标准接口

  1. package com.yq1ng.spi;
  2. /**
  3. * spiService
  4. *
  5. * @author yq1ng
  6. * @date 2022/2/17 22:06
  7. * @since 1.0.0
  8. */
  9. public interface say {
  10. public void say(String name);
  11. }

实现类

  1. package com.yq1ng.spi;
  2. /**
  3. * sayHello
  4. *
  5. * @author yq1ng
  6. * @date 2022/2/17 22:09
  7. * @since 1.0.0
  8. */
  9. public class sayHello implements say {
  10. @Override
  11. public void say(String name) {
  12. System.out.println("hello " + name);
  13. }
  14. }
package com.yq1ng.spi;

/**
 * sayBye
 *
 * @author yq1ng
 * @date 2022/2/17 22:12
 * @since 1.0.0
 */
public class sayBye implements say {
    @Override
    public void say(String name) {
        System.out.println("bye " + name);
    }
}

接着在resource创建META-INF/services/目录,创建以spi接口全限定名为文件名的文件,内容写上实现类的全限定名。例如这里创建com.yq1ng.spi.say
image.png
来个测试类

import com.yq1ng.spi.say;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * speak
 *
 * @author yq1ng
 * @date 2022/2/17 22:15
 * @since 1.0.0
 */
public class speak {
    public static void main(String[] args) {
        ServiceLoader<say> says = ServiceLoader.load(say.class);
        Iterator<say> iterator = says.iterator();
        while (iterator.hasNext()){
            say s = iterator.next();
            s.say("yq1ng");
        }
    }
}

image.png

源码解析

先看看类都有那些属性
image.png
注意到providersLinkedHashMap也就是会按照配置文件里面的顺序去找spi实现类。然后看load()
image.png
最终是new了内部类LazyIterator
image.png
后面hasNext()next()都是LazyIterator完成的
image.png
直接Class.forName(),这也知道了为啥文件里面需要写全限定名了

缺点&安全问题

缺点

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

    安全问题

    存在跨目录文件上传的话,如果限制jsp后缀可以试试spi机制。先上传一个配置文件,里面写上恶意类的全限定名,然后上传一个恶意class文件让他去加载。不举例了,就是配置文件多一行com.yq1ng.spi.evil
    然后根据这个特性有了下面这个反序列化(yml就相当于spi的配置)
    SnakeYaml