- 网上回答:SPI是什么?
- 思考:类加载器与SPI
2.1 类加载器知识
2.2 SPI与线程上下文加载器
2.3 需要SPI进行类加载吗? - 为什么要引入SPI
3.1 案例实现 - 业务项目有机会使用SPI吗
4.1 业务项目常用—spring方式
1. 网上回答:SPI是什么?
一般是先说定义:SPI ,全称为 Service Provider Interface
,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
然后讲JDBC:毕竟JDBC是每一个JAVA程序员的hello world
。
但是,并没有深入思考,SPI使用的场景!!!
2. 思考:类加载器与SPI
SPI引入会打破JVM类加载器的双亲委派模型,JVM会引入线程上下文加载器
Thread Context ClassLoader
。这个类加载器可以通过java.lang.Thread
类的setContextClassLoader()
方法进行设置。
2.1 类加载器知识
类加载阶段,通过类加载器,将class文件读取到内存中,并在内存中生成class对象。
而常见的类加载器:
- 启动类加载器(BootStrap):加载的是<JAVA_HOME>/lib中的class文件,也就是JDK的依赖。
- 拓展类加载器(Extension):加载的是<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。
- 应用程序加载器(AppClassLoader):程序默认加载器,加载用户类路径上指定的类库。
2.2 SPI与线程上下文加载器
SPI接口一般有两种情况,一种是JDK中声明的SPI接口,一种是框架使用的SPI例如dubbo。这两种情况使用的加载器是不同的。
JDK声明的SPI接口:SPI的实现类一般是由应用加载器Application ClassLoader
加载的,而JDK提供的SPI接口是Bootstrap ClassLoader
加载:也就导致SPI接口无法找到对应的实现类。根本原因:并不是同一个类加载器进行加载的。为了解决这种情况,JVM设计出线程上下文加载器,来打破双亲委派模型。即父类使用子类的类加载器来进行类加载。从而保证父子类由一个类加载器进行加载,
框架的SPI接口:无论是父类还是子类,均使用的是Application ClassLoader
加载,不会打破双亲委派模型,子类将委托父类的类加载器完成类的加载,从而保证了父子类由一个类加载器进行加载。
2.3 需要SPI进行类加载吗?
针对JDK的SPI接口,需要由线程上下文类加载器来完成父子类的加载,保证SPI接口和实现类由一个类加载器完成加载。
3. 为什么要引入SPI
场景:如何寻找一个接口的所有实现类。
- Spring环境:可以依赖注入一个集合,那么会扫描Spring容器中该接口的所有时间类。
- 侵入代码:可以维护一个枚举类或者一个map来存储接口所有的实现类。
但是对于框架来说,即不能和Spring强耦合,也无法侵入代码未卜先知声明所有的子类。所以就有了SPI:Service Provider Interface
,是一种服务发现机制。
SPI目的:在框架中找到某个接口的实现所有实现类,存储到List中。或者更新颖的玩法,在子类上声明注解,通过SPI找到所有实现类,然后得到实现类上的注解参数,将其组合为一个Map。当不同的请求到达时,可以动态的选择不同的子类来进行处理。
由上可知,SPI的目的是通过读取规定配置信息,通过反射的方式创建接口实现类。
3.1 案例实现
源码位置—限流组件:sentinel1.8.1源码
读取META-INF/services/
的配置信息,即SPI实现类的全类名。通过类加载器完成类加载过程,通过反射完成对象的创建:
源码位置:com.alibaba.csp.sentinel.spi.SpiLoader
public final class SpiLoader<S> {
// Default path for the folder of Provider configuration file
private static final String SPI_FILE_PREFIX = "META-INF/services/";
// Cache the SpiLoader instances, key: classname of Service, value: SpiLoader instance
private static final ConcurrentHashMap<String, SpiLoader> SPI_LOADER_MAP = new ConcurrentHashMap<>();
// Cache the classes of Provider
private final List<Class<? extends S>> classList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());
// Cache the sorted classes of Provider
private final List<Class<? extends S>> sortedClassList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());
/**
* Cache the classes of Provider, key: aliasName, value: class of Provider.
* Note: aliasName is the value of {@link Spi} when the Provider class has {@link Spi} annotation and value is not empty,
* otherwise use classname of the Provider.
*/
private final ConcurrentHashMap<String, Class<? extends S>> classMap = new ConcurrentHashMap<>();
// Cache the singleton instance of Provider, key: classname of Provider, value: Provider instance
private final ConcurrentHashMap<String, S> singletonMap = new ConcurrentHashMap<>();
// Whether this SpiLoader has been loaded, that is, loaded the Provider configuration file
private final AtomicBoolean loaded = new AtomicBoolean(false);
// Default provider class
private Class<? extends S> defaultClass = null;
// The Service class, must be interface or abstract class
private Class<S> service;
/**
* Create SpiLoader instance via Service class
* Cached by className, and load from cache first
* 创建SpiLoader实例通过SPI接口
*/
public static <T> SpiLoader<T> of(Class<T> service) {
AssertUtil.notNull(service, "SPI class cannot be null");
AssertUtil.isTrue(service.isInterface() || Modifier.isAbstract(service.getModifiers()),
"SPI class[" + service.getName() + "] must be interface or abstract class");
String className = service.getName();
SpiLoader<T> spiLoader = SPI_LOADER_MAP.get(className);
if (spiLoader == null) {
synchronized (SpiLoader.class) {
spiLoader = SPI_LOADER_MAP.get(className);
if (spiLoader == null) {
SPI_LOADER_MAP.putIfAbsent(className, new SpiLoader<>(service));
spiLoader = SPI_LOADER_MAP.get(className);
}
}
}
return spiLoader;
}
/**
*
* 加载配置文件中提供的类
*/
public void load() {
if (!loaded.compareAndSet(false, true)) {
return;
}
// 组装配置文件地址
String fullFileName = SPI_FILE_PREFIX + service.getName();
ClassLoader classLoader;
if (SentinelConfig.shouldUseContextClassloader()) {
classLoader = Thread.currentThread().getContextClassLoader();
} else {
//获取到父类的类加载器
classLoader = service.getClassLoader();
}
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
Enumeration<URL> urls = null;
try {
//类加载器读取资源信息
urls = classLoader.getResources(fullFileName);
} catch (IOException e) {
fail("Error locating SPI configuration file, filename=" + fullFileName + ", classloader=" + classLoader, e);
}
if (urls == null || !urls.hasMoreElements()) {
RecordLog.warn("No SPI configuration file, filename=" + fullFileName + ", classloader=" + classLoader);
return;
}
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
InputStream in = null;
BufferedReader br = null;
try {
in = url.openStream();
br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line;
while ((line = br.readLine()) != null) {
if (StringUtil.isBlank(line)) {
// Skip blank line
continue;
}
line = line.trim();
int commentIndex = line.indexOf("#");
if (commentIndex == 0) {
// Skip comment line
continue;
}
if (commentIndex > 0) {
line = line.substring(0, commentIndex);
}
line = line.trim();
Class<S> clazz = null;
try {
//通过反射创建子类对象
clazz = (Class<S>) Class.forName(line, false, classLoader);
} catch (ClassNotFoundException e) {
fail("class " + line + " not found", e);
}
if (!service.isAssignableFrom(clazz)) {
fail("class " + clazz.getName() + "is not subtype of " + service.getName() + ",SPI configuration file=" + fullFileName);
}
classList.add(clazz);
//读取子类上的配置
Spi spi = clazz.getAnnotation(Spi.class);
String aliasName = spi == null || "".equals(spi.value()) ? clazz.getName() : spi.value();
if (classMap.containsKey(aliasName)) {
Class<? extends S> existClass = classMap.get(aliasName);
fail("Found repeat alias name for " + clazz.getName() + " and "
+ existClass.getName() + ",SPI configuration file=" + fullFileName);
}
classMap.put(aliasName, clazz);
if (spi != null && spi.isDefault()) {
if (defaultClass != null) {
fail("Found more than one default Provider, SPI configuration file=" + fullFileName);
}
defaultClass = clazz;
}
RecordLog.info("[SpiLoader] Found SPI implementation for SPI {}, provider={}, aliasName={}"
+ ", isSingleton={}, isDefault={}, order={}",
service.getName(), line, aliasName
, spi == null ? true : spi.isSingleton()
, spi == null ? false : spi.isDefault()
, spi == null ? 0 : spi.order());
}
} catch (IOException e) {
fail("error reading SPI configuration file", e);
} finally {
closeResources(in, br);
}
}
//子类排序
sortedClassList.addAll(classList);
Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {
@Override
public int compare(Class<? extends S> o1, Class<? extends S> o2) {
Spi spi1 = o1.getAnnotation(Spi.class);
int order1 = spi1 == null ? 0 : spi1.order();
Spi spi2 = o2.getAnnotation(Spi.class);
int order2 = spi2 == null ? 0 : spi2.order();
return Integer.compare(order1, order2);
}
});
}
4. 业务项目有机会使用SPI吗
因为现在Spring一统天下,只针对业务系统来说,Spring的bean单例池+依赖注入集合对象的方式可以找到某个接口的在Spring容器中的所有实现子类。故SPI这种服务发现的方式其实用到的机会不是很多。
但是脱离Spring的一些框架,使用SPI方法找到接口对应的所有子类的方式是比较常见的。例如dubbo、sentinel、seata等框架。了解SPI的对阅读源码或者自己造轮子是非常有帮助的。
4.1 业务项目常用—spring方式
项目常用的实现方式:实现服务的发现。
@Service
@Slf4j
public class CommonInfoServiceImpl implements InfoService{
private Map<TypeEnum, ContentItemService> serviceMap = new HashMap<>();
/**
* 构造器注入,注入Spring容器中ContentItemService接口所有的子类Bean。并将其放入到Map缓存中。
* key:{@link TypeEnum},value:子类Bean。
*/
@Autowired
public CommonOcrItemInfoServiceImpl(List<ContentItemService> contentItemList) {
for (ContentItemService contentItemService : contentItemList) {
List<TypeEnum> typeEnums = contentItemService.getTypeEnum();
for (TypeEnum typeEnum : typeEnums) {
serviceMap.put(sourceTypeEnum, contentItemService);
}
}
}
}
接口类:
public interface ContentItemService {
//业务逻辑的实现类
List<ItemInfoResp> listItemId(List<ItemInfoRequestVO> vo);
//子类对应的枚举对象
List<TypeEnum> getTypeEnum();
}
项目启动后,会加载Spring容器中的子类对象,组合成Map,根据参数的TypeEnum值的不同,选择合适的子类完成业务逻辑。