sentinel中的扩展

为什么优秀的框架总是提供用户扩展的能力?

我想这是因为业务逻辑是千变万化的,框架中能抽象的只是部分的公共逻辑,在应用接入框架时,难免需要做一些本地化调整。框架一般通过接口(interface)的形式来提供扩展(用户通过实现这些接口来获得某些能力或完成某些逻辑),那么框架如何知道这些接口实现呢?

一般就两种方式:

  • 用户告诉框架接口实现:通过框架提供的 SDK 注册对应的接口实现
  • 框架自己来找接口实现:框架按照约定(SPI、包扫描等)方式来发现对应的接口实现

什么意思呢,我们以大名鼎鼎的 Spring 来举例:

// 通过注解 + 包扫描的方式来查找 WebMvcConfigurer 接口实现
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    private void addSpringMvcInterceptor(InterceptorRegistry registry) {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();
        // 通过 registry.addInterceptor 注册 Interceptor 接口实现
        registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
    }

}

那么 Sentinel 中的扩展中有哪些呢?其又是如何感知到这些扩展实现呢?

sentinel spi

Java 中提供了 SPI(Service Provider Interface)机制来扩展、增强我们的应用。Sentinel 中定义了自己的 SPI 实现,用于加载对应接口实现。对应的 sentinel spi 实现代码:

class SpiLoader {
    public void load() {
        // 判断是否已经加载过
        if (!loaded.compareAndSet(false, true)) {
            return;
        }

        // 读取的文件全路径(包含类的全路径名称)
        // 如:/META-INF.services/com.alibaba.csp.sentinel.init.initFunc
        String fullFileName = SPI_FILE_PREFIX + service.getName();
        // 判断使用哪种 classLoader
        ClassLoader classLoader;
        // ...
        
        // 从当前 classPath 读取 jar 包中的 spi 文件
        Enumeration<URL> urls = null;
        try {
            urls = classLoader.getResources(fullFileName);
        } catch (IOException e) {
            fail("Error locating SPI configuration file,filename=" + fullFileName + ",classloader=" + classLoader, e);
        }
        // ...

        // 解析 spi 文件中的内容,并加载为 class
        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) {
                    // 忽略空行,忽略以 # 开始的注释 容
                    // ...

                    // 根据文本创建对应的 class
                    Class<S> clazz = null;
                    try {
                        clazz = (Class<S>) Class.forName(line, false, classLoader);
                    } catch (ClassNotFoundException e) {
                        fail("class " + line + " not found", e);
                    }

                    // 判断是否重复加载,class 是否为 service 类型子类
                    // ...
                    
                    // 读取 @Spi 注解属性
                    classList.add(clazz);
                    Spi spi = clazz.getAnnotation(Spi.class);
                    String aliasName = spi == null || "".equals(spi.value()) ? clazz.getName() : spi.value();
                    // ...
                }
            } catch (IOException e) {
                fail("error reading SPI configuration file[" + url + "]", e);
            } finally {
                closeResources(in, br);
            }
        }

        sortedClassList.addAll(classList);
        // 根据 class 中定义的 @Spi 注解 order 属性进行排序
        Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {
            @Override
            public int compare(Class<? extends S> o1, Class<? extends S> o2) {
                // ...
            }
        });
    }
}

核心的扩展接口如下:

接口接口描述创建时机运行时机
InitFunc用于实现自定义的初始化逻辑sentinel 初始化时触发(sentinel core 模块在进入资源时会通过 InitExecutor.doInit 触发初始化动作)仅初始化触发一次
MetricExtension用于实现sentinel内部统计的自定义逻辑扩展sentinel 初始化时触发进入资源时触发
SlotChainBuilder用于创建自定义的slot chain为资源创建 slot chain 实例时触发仅初始化创建一次
ProcessorSlot用于实现自定义的资源进入、退出逻辑在为资源创建 slot chain 实例时触发进入、退出资源时触发
CommandCenter用于commandCenter开始前、开始、结束阶段的自定义逻辑扩展在transport模块初始化时触发仅初始化调用一次
CommandHandler用于实现自定义的请求(commend request)处理在transport模块初始化时触发发生请求(CommandRequest)时触发
CommandHandlerInterceptor用于实现自定义的请求(commend request)拦截在transport模块初始化时触发发生请求(CommandRequest)时触发
HeartbeatSender用于实现自定义的心跳发送器sentinel 初始化时触发定时触发

接口注册

上述代码中, sentinel 实现了一套自己的 SPI 机制来加载依赖的接口,以保证框架正常的运行。

另外 sentinel 还提供了一些接口方便用户在特定时机去自定义自己的逻辑,我们可以通过接口注册的方式告知 sentinel 我们的接口实现。
核心的扩展接口信息如下:

接口接口描述运行时机
ReadableDataSource用于读取外部数据源获取最新的配置1. 在定义时初始化拉取一次
2.后续更新时由外部数据源推送最新规则或定时从外部数据源拉取最新规则
WritableDataSource用于将最新配置写入外部数据源在1.8.7版本中以下动作会触发
- 网关api组更新
- 网关规则更新
- 热点规则更新时
ProcessorSlotEntryCallback进入资源时回调方法在 StatisticSlot 进入资源后触发
ProcessorSlotExitCallback退出资源时回调方法在 StatisticSlot 退出资源后触发

使用方法很简单,一般通过对应 Registry 类的静态方法进行注册,我们以 ProcessorSlotEntryCallback 接口为例演示一下基础使用方法:

class Demo {
    public static void main(String[] args) {
        StatisticSlotCallbackRegistry.addEntryCallback("entryCb1", new ProcessorSlotEntryCallback<DefaultNode>() {
            @Override
            public void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode param, int count, Object... args) throws Exception {
                System.out.println("entry resource success : " + resourceWrapper.getName());
            }
            @Override
            public void onBlocked(BlockException ex, Context context, ResourceWrapper resourceWrapper, DefaultNode param, int count, Object... args) {
                System.out.println("entry resource blocked : " + resourceWrapper.getName());
            }
        });
    }
}

总结

在本章我们了解到在 sentinel 中注册扩展主要有两种方式:

  1. 实现对应接口,在类上使用 @Spi 注解从而被 sentinel 扫描并注册
  2. 实现对应接口,通过对应的 API 方法手动注册到 sentinel 中

文章编写不易,如有帮助,欢迎 star:github 地址gitee 地址

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值