sentinel学习笔记4-SPI 在 Sentinel 中的应用
本文属于sentinel学习笔记系列。网上看到吴就业老师的专栏,写的好值得推荐,我整理的有所删减,推荐看原文。
https://blog.csdn.net/baidu_28523317/category_10400605.html
java SPI
SPI机制是Java平台提供的一种用于服务发现和服务提供者查找的机制。它允许在运行时动态地加载和实例化实现特定接口的类,从而达到扩展性和灵活性的目的,SPI 的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类,实现在运行时动态替换接口的实现类。
适应场景:
数据库驱动管理、日志框架选择、框架扩展和插件机制、服务发现:SPI机制可以帮助系统在运行时发现可用的服务提供者,从而动态地选择和使用服务。
demo
找了之前的storage工程做个测试,新加了1个接口两个实现类
public interface TestService {
void hello(String para);
}
@Service
public class AAATestServiceImpl implements TestService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void hello(String para) {
logger.info("AAA.hello para="+para);
}
}
@Service
public class BBBTestServiceImpl implements TestService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void hello(String para) {
logger.info("BBB.hello para="+para);
}
}
测试:
@SpringBootTest
class TlmallStorageApplicationTests {
@Test
void contextLoads() {
ServiceLoader<TestService> serviceLoader = ServiceLoader.load(TestService.class);
for(TestService testService:serviceLoader){
testService.hello("world");
}
}
}
当我们想通过修改配置文件的方式而不修改代码实现权限验证框架的切换,就可以使用 Java 的 SPI。通过运行时从配置文件中读取实现类,加载使用配置的实现类。
需要在 resources 目录下新建一个目录 META-INF,并在 META-INF 目录下创建 services 目录,用来存放接口配置文件。然后在目录中创建一个文件,名称必须是Testservice定义的接口的全类路径名称。然后在文件中写上Testservice接口的实现类的全类路径名称。
当配置为:
org.tuling.tlmallstorage.service.impl.AAATestServiceImpl
测试输出
2024-12-22T11:50:14.265+08:00 INFO 5260 --- [tlmall-storage] [ main] o.t.t.service.impl.AAATestServiceImpl : AAA.hello para=world
当配置为: org.tuling.tlmallstorage.service.impl.BBBTestServiceImpl
测试输出
配置多个就输出多个:
看下 java.util.ServiceLoader#load(java.lang.Class<S>)
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的上下文类加载器。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//使用获取到的类加载器、服务接口以及调用者的类信息,创建并返回一个新的 ServiceLoader 实例。
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
调用 load 方法传入接口名就能获取到一个 ServiceLoader 实例,此时配置文件中注册的实现类是还没有加载到 JVM 的,只有后面的在遍历迭代器的时候 ServiceLoader 才通过调用 Class#forName 方法加载类并且通过反射创建实例。
Java SPI 在 Sentinel 中的应用
在 sentinel-core 模块的resources下面,META-INF/services 目录,该目录下有三个以接口全名命名的文件
其中 com.alibaba.csp.sentinel.slotchain.SlotChainBuilder 文件用于配置 SlotChainBuilder 接口的实现类,com.alibaba.csp.sentinel.init.InitFunc 文件用于配置 InitFunc 接口的实现类,
com.alibaba.csp.sentinel.slotchain.ProcessorSlot 用于配置默认责任链。
以SlotChainBuilder为例。
以常见例子SphU.entry("XX") 为切入口,整个限流的核心操作在这里实现,代码在
com.alibaba.csp.sentinel.CtSph#entryWithPriority()
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
//构建责任链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
/*
* Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
* so no rule checking will be done.
*/
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
Entry e = new CtEntry(resourceWrapper, chain, context);
try {// 驱动责任链上的第一个处理器,进而由处理器自驱动执行下一个处理器
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
核心逻辑就是构建责任链lookProcessChain与执行chain.entry。
其中看下构建责任链
com.alibaba.csp.sentinel.CtSph#lookProcessChain
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.限制6000
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
//真正构建责任链
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
从chainMap
里面获取slot功能链, 没有的话,就构建一个newSlotChain() 。
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// Resolve the slot chain builder SPI. 通过SPI构建,返回第一个类型不等于 defaultClass 的实例
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
if (slotChainBuilder == null) {
// Should not go through here. spi获取不到则使用默认的兜底
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
return slotChainBuilder.build();
}
注意,这里Sentinel 在加载 SlotChainBuilder 时,只会通过loadFirstInstanceOrDefault获取第一个非默认(非 DefaultSlotChainBuilder)实现类的实例。
Sentinel 将 ProcessorSlot
作为 SPI 接口进行扩展(1.7.2 版本以前 SlotChainBuilder
作为 SPI),使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。
下面的官网的自定义slotdemo,实现请求 pass 后记录当前的 context 和资源信息
@Spi(order = -1500)
public class DemoSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
System.out.println("------Entering for entry on DemoSlot------");
System.out.println("Current context: " + context.getName());
System.out.println("Current entry resource: " + context.getCurEntry().getResourceWrapper().getName());
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
System.out.println("------Exiting for entry on DemoSlot------");
System.out.println("Current context: " + context.getName());
System.out.println("Current entry resource: " + context.getCurEntry().getResourceWrapper().getName());
fireExit(context, resourceWrapper, count, args);
}
}
配置spi:
public static void main(String[] args) {
Entry entry = null;
try {
entry = SphU.entry("abc");
} catch (BlockException ex) {
ex.printStackTrace();
} finally {
if (entry != null) {
entry.exit();
}
}
}
输出:
INFO: Sentinel log output type is: file
INFO: Sentinel log charset is: utf-8
INFO: Sentinel log base directory is: C:\Users\bohu8\logs\csp\
INFO: Sentinel log name use pid is: false
INFO: Sentinel log level is: INFO
------Entering for entry on DemoSlot------
Current context: sentinel_default_context
Current entry resource: abc
------Exiting for entry on DemoSlot------
Current context: sentinel_default_context
Current entry resource: abc
Process finished with exit code 0