什么是SPI
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
SPI示例
定义标准接口
package com.yq1ng.spi;
/**
* spiService
*
* @author yq1ng
* @date 2022/2/17 22:06
* @since 1.0.0
*/
public interface say {
public void say(String name);
}
实现类
package com.yq1ng.spi;
/**
* sayHello
*
* @author yq1ng
* @date 2022/2/17 22:09
* @since 1.0.0
*/
public class sayHello implements say {
@Override
public void say(String name) {
System.out.println("hello " + name);
}
}
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
来个测试类
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");
}
}
}
源码解析
先看看类都有那些属性
注意到providers
是LinkedHashMap
也就是会按照配置文件里面的顺序去找spi实现类。然后看load()
最终是new了内部类LazyIterator
后面hasNext()
和next()
都是LazyIterator完成的
直接Class.forName()
,这也知道了为啥文件里面需要写全限定名了
缺点&安全问题
缺点
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
安全问题
存在跨目录文件上传的话,如果限制jsp后缀可以试试spi机制。先上传一个配置文件,里面写上恶意类的全限定名,然后上传一个恶意class文件让他去加载。不举例了,就是配置文件多一行com.yq1ng.spi.evil
然后根据这个特性有了下面这个反序列化(yml就相当于spi的配置)
SnakeYaml