Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理

图片[1]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课

本文导读

在前面的文章中,我们学习 Sentinel 的核心功能时,都是通过手动编写 try-catch-finally 代码块来包装受保护的资源。每次调用都需要:

  • 调用 ContextUtil#enter 进入上下文
  • 调用 SphU#entry 获取 Entry
  • 异常时调用 Tracer#trace 记录异常
  • finally 中调用 Entry#exitContextUtil#exit

如果每个接口都要手写这些代码,不仅繁琐,还容易出错。幸运的是,Sentinel 提供了对主流框架的适配支持,让我们可以”零侵入”地使用 Sentinel 的各种功能。

Sentinel 目前已经适配的框架包括:

  • Web 层:Spring MVC、WebFlux、JAX-RS
  • 服务调用:Dubbo、OpenFeign、RestTemplate
  • 网关层:Spring Cloud Gateway、Zuul
  • 消息队列:RocketMQ、Kafka

本文将深入分析两个最常用的框架适配实现:

  1. Spring MVC 适配器:基于 HandlerInterceptor 实现
  2. OpenFeign 适配器:基于 InvocationHandler 实现

理解这些适配原理后,即使 Sentinel 没有提供你使用框架的适配器,你也可以自己动手实现。


一、Sentinel 框架适配总览

1.1 框架适配的核心思想

Sentinel 框架适配的核心思想可以用一句话概括:在框架的入口点注入 Sentinel 的逻辑

具体来说,就是找到框架提供的扩展点(拦截器、过滤器、动态代理等),在这些扩展点中插入 Sentinel 的调用逻辑,从而实现对业务代码的零侵入。

图片[2]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课

不同框架的扩展点不同,适配方式也不同:

框架 扩展点 适配方式
Spring MVC HandlerInterceptor 拦截器中调用 Sentinel
WebFlux WebFilter 过滤器中调用 Sentinel
Dubbo Filter RPC 过滤器中调用 Sentinel
OpenFeign InvocationHandler 动态代理中调用 Sentinel
Spring Cloud Gateway GlobalFilter 全局过滤器中调用 Sentinel

1.2 适配的通用流程

无论哪种框架,适配的流程大体相似:

  1. 请求进入时:调用 ContextUtil.enter()SphU.entry()
  2. 业务执行:正常执行业务逻辑
  3. 异常时:调用 Tracer.trace() 记录业务异常
  4. 请求结束时:调用 entry.exit()ContextUtil.exit()

理解了这个通用流程,再看任何框架的适配都会觉得似曾相识。


二、Spring MVC 适配器深度解析

2.1 使用步骤

在分析源码之前,我们先回顾一下 Spring MVC 适配器的使用方式。

第一步:添加依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-webmvc-adapter</artifactId>
    <version>${sentinel.version}</version>
</dependency>

第二步:配置拦截器

实现 WebMvcConfigurer,在 addInterceptors 方法中注册 SentinelWebInterceptor

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();
        // 配置 BlockException 异常处理器
        config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());
        // 是否给资源名加上 HTTP 方法前缀
        config.setHttpMethodSpecify(true);
        // 注册调用来源解析器
        config.setOriginParser(request -> request.getHeader("S-user"));
        // 拦截所有请求
        registry.addInterceptor(new SentinelWebInterceptor(config))
                .addPathPatterns("/");
    }
}

三个核心配置项:

配置项 作用
setBlockExceptionHandler 配置 BlockException 异常处理器(也可以用全局异常处理器)
setOriginParser 注册调用来源(origin)解析器,比如从请求头获取调用方标识
setHttpMethodSpecify 是否给资源名加上 HTTP 方法前缀(如 GET:/hello

2.2 适配原理:HandlerInterceptor

Spring MVC 适配器是基于 HandlerInterceptor(处理器拦截器)实现的。我们先回顾一下这个接口:

public interface HandlerInterceptor {
    // 在 Controller 方法执行前调用
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                              Object handler) throws Exception {
        return true;
    }
    
    // 在 Controller 方法执行后、视图渲染前调用
    default void postHandle(HttpServletRequest request, HttpServletResponse response, 
                            Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    
    // 在请求完成后调用(无论成功或异常)
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                 Object handler, @Nullable Exception ex) throws Exception {
    }
}

这三个方法的调用时机:

  • preHandle:目标方法执行前 → 可以在这里调用 SphU.entry()
  • afterCompletion:请求完成后(无论成功或异常)→ 可以在这里调用 entry.exit() 并处理异常

这简直就是为 Sentinel 量身定做的扩展点!

2.3 SentinelWebInterceptor 类结构

SentinelWebInterceptor 是 Spring MVC 适配器的核心类,它继承自 AbstractSentinelInterceptor

AbstractSentinelInterceptor(抽象父类,实现核心逻辑)
        ↑
SentinelWebInterceptor(子类,实现资源名获取)

父类 AbstractSentinelInterceptor 实现了 preHandleafterCompletion 的核心逻辑,子类只需要实现获取资源名称的抽象方法即可。

这种设计非常经典:模板方法模式的应用,父类定义算法骨架,子类实现可变部分。

2.4 资源名称的生成

SentinelWebInterceptor 只做一件事:实现父类的 getResourceName 抽象方法,生成资源名称。

源码如下:

@Override
protected String getResourceName(HttpServletRequest request) {
    // (1) 从 request 属性中获取匹配的 URL 模板
    Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
    if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
        return null;
    }
    String resourceName = (String) resourceNameObject;
    
    // (2) 如果配置了 UrlCleaner,则调用清洗方法
    UrlCleaner urlCleaner = config.getUrlCleaner();
    if (urlCleaner != null) {
        resourceName = urlCleaner.clean(resourceName);
    }
    
    // (3) 根据配置决定是否添加 HTTP 方法前缀
    if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) {
        resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
    }
    return resourceName;
}

资源名称生成的三步曲:

第一步:获取匹配的 URL 模板

HttpServletRequest 的属性中获取 HandlerMapping 匹配的 URL 模板。

为什么不直接用 request.getRequestURI()?

因为 RESTful 接口中,URL 可能包含路径参数,比如 /hello/{name}。如果直接用 request.getRequestURI(),那么 /hello/zhangsan/hello/lisi 会被当成两个不同的资源。使用 URL 模板可以正确识别为同一个资源。

第二步:UrlCleaner 清洗

如果配置了 UrlCleaner,则调用其 clean 方法对资源名进行清洗。

UrlCleaner 的用途:将多个接口合并为一个资源,共享同一个限流规则。

例如:

  • /user/create/user/del/user/update
  • 通过 UrlCleaner 统一改成 /user/
  • 这样三个接口就共用一套限流规则

第三步:HTTP 方法前缀

根据 httpMethodSpecify 配置决定是否加上 HTTP 方法前缀。

  • 开启:GET:/helloPOST:/hello
  • 关闭:/hello

实践建议:一般不建议开启。因为如果接口使用 @RequestMapping 声明(不指定 method),需要为 GET、POST 等分别配置规则,比较麻烦。旧项目尤其需要注意。

2.5 preHandle:请求进入时的处理

AbstractSentinelInterceptor#preHandle 是请求进入时的核心逻辑:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
    try {
        // (1) 获取资源名称
        String resourceName = getResourceName(request);
        if (StringUtil.isNotEmpty(resourceName)) {
            // (2) 解析调用来源 origin
            String origin = parseOrigin(request);
            // (3) 进入 Context
            ContextUtil.enter(SENTINEL_SPRING_WEB_CONTEXT_NAME, origin);
            // (4) 获取 Entry
            Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
            // (5) 将 Entry 存入 request 属性,方便后续取出
            setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry);
        }
        return true;
    } catch (BlockException e) {
        // (6) 被限流/熔断,处理异常
        handleBlockException(request, response, e);
        return false;
    }
}

流程解读:

  1. 获取资源名:调用子类实现的 getResourceName 方法
  2. 解析调用来源:调用 OriginParser 解析 origin(调用方标识)
  3. 进入上下文ContextUtil.enter(),Context 名称固定为 sentinel_spring_web_context
  4. 获取 EntrySphU.entry(),资源类型为 COMMON_WEB,流量类型为 IN(入站)
  5. 存储 Entry:将 Entry 存入 HttpServletRequest 的属性中,方便后续取出
  6. 异常处理:捕获 BlockException,调用 handleBlockException 处理

关于 BlockException 的处理:

protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, 
                                     BlockException e) throws Exception {
    if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
        // 配置了处理器就用处理器处理
        baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e);
    } else {
        // 没配置就抛出,由全局异常处理器处理
        throw e;
    }
}

两种处理方式二选一:

  • 配置 BlockExceptionHandler:在 Sentinel 配置中指定
  • 不配置:抛出异常,由 Spring MVC 全局异常处理器统一处理

2.6 afterCompletion:请求结束时的处理

AbstractSentinelInterceptor#afterCompletion 是请求结束时的处理逻辑:

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                            Object handler, Exception ex) throws Exception {
    // (1) 从 request 中取出 Entry
    Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
    if (entry != null) {
        // (2) 记录异常并调用 exit
        traceExceptionAndExit(entry, ex);
        removeEntryInRequest(request);
    }
    // (3) 退出 Context
    ContextUtil.exit();
}

流程解读:

  1. 取出 Entry:从 HttpServletRequest 的属性中取出 preHandle 阶段存入的 Entry
  2. 记录异常并退出:调用 traceExceptionAndExit 方法
  3. 退出 ContextContextUtil.exit(),从 ThreadLocal 中移除 Context

traceExceptionAndExit 方法:

protected void traceExceptionAndExit(Entry entry, Exception ex) {
    if (entry != null) {
        if (ex != null) {
            // 有异常,记录到 Sentinel 的异常指标中
            Tracer.traceEntry(ex, entry);
        }
        entry.exit();
    }
}

关键点:如果 Controller 方法抛出了业务异常(非 BlockException),需要调用 Tracer.traceEntry() 将异常记录下来,这样熔断降级的异常比率统计才能正确工作。


三、OpenFeign 适配器深度解析

3.1 使用场景与步骤

Sentinel 整合 OpenFeign 主要用于服务消费端实现熔断降级。当下游服务不稳定、响应慢或报错时,自动触发熔断,避免级联故障。

使用步骤:

1. 引入依赖

通过 spring-cloud-starter-alibaba-sentinel 实现整合:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

2. 开启 Sentinel 支持

application.yaml 中添加配置:

feign:
  sentinel:
    enabled: true

3. 配置熔断规则

可以通过动态数据源配置,也可以硬编码调用 DegradeRuleManager.loadRules()

4. 配置 Fallback

@FeignClient 注解配置 fallback 属性,实现降级逻辑:

@FeignClient(
    name = "demo-service",
    fallback = ServiceDegradeFallback.class,
    configuration = {SentinelFeignConfig.class}
)
public interface DemoService {
    @PostMapping("/services")
    ListGenericResponse<DemoDto> getServices();
}

Fallback 类需要实现相同的接口:

public class ServiceDegradeFallback implements DemoService {
    @Override
    public ListGenericResponse<DemoDto> getServices() {
        ListGenericResponse response = new ListGenericResponse<DemoDto>();
        response.setCode(ResultCode.SERVICE_DEGRAD.getCode())
                .setMessage("服务降级");
        return response;
    }
}

最后将 Fallback 注册到 Spring 容器中:

public class SentinelFeignConfig {
    @Bean
    public ServiceDegradeFallback degradeMockYcpayService() {
        return new ServiceDegradeFallback();
    }
}

当满足熔断条件时,Sentinel 会抛出 DegradeException,然后从 Bean 工厂中获取 fallback 实例并调用对应方法。

3.2 整体调用流程

当 Sentinel 与 OpenFeign、Ribbon 整合时,一次请求的完整流程如下:

图片[3]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课

完整调用链:

  1. 调用 @FeignClient 接口方法时,由 SentinelInvocationHandler 拦截方法执行
  2. 根据接口方法上的 URL 生成资源名,调用 SphU.entry() 进行熔断检查
  3. 非熔断情况下,继续交给 OpenFeign 的 MethodHandler 处理
  4. OpenFeign 从 Ribbon 获取一个服务提供者节点(负载均衡)
  5. OpenFeign 使用 HttpClient 发起 HTTP 请求
  6. 请求成功或异常(经过重试后),调用 Entry.exit() 更新指标数据

重要细节:Sentinel 处在接口调用的最前端,因此 Sentinel 统计的指标数据不受 Ribbon 重试和 OpenFeign 重试的影响。也就是说,无论重试多少次,在 Sentinel 看来都只是一次调用。

3.3 核心原理:替换 InvocationHandler

OpenFeign 是基于 JDK 动态代理实现的,InvocationHandler 是动态代理的核心。Sentinel 正是通过替换 InvocationHandler 来实现拦截的。

图片[4]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课

那么,Sentinel 是如何将 Feign 默认的 FeignInvocationHandler 替换成 SentinelInvocationHandler 的呢?

答案是:连 Feign.Builder 一起替换掉

OpenFeign 通过 Feign.Builder 类创建接口的代理类。Sentinel 直接将 Feign.Builder 替换成了 SentinelFeign.Builder

3.4 自动配置:SentinelFeignAutoConfiguration

替换的入口在自动配置类 SentinelFeignAutoConfiguration 中:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "feign.sentinel.enabled")
    public Feign.Builder feignSentinelBuilder() {
        return SentinelFeign.builder();
    }
}

非常简洁:

  • @ConditionalOnClass:classpath 下有 Sentinel 和 Feign 才生效
  • @ConditionalOnProperty:配置了 feign.sentinel.enabled=true 才生效
  • 向 Spring 容器注入 SentinelFeign.Builder,替代默认的 Feign.Builder

3.5 SentinelFeign.Builder 的偷天换日

SentinelFeign.Builder 继承 Feign.Builder 并重写 build 方法:

public final class SentinelFeign {

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder extends Feign.Builder
            implements ApplicationContextAware {

        @Override
        public Feign build() {
            // 替换 InvocationHandlerFactory
            super.invocationHandlerFactory(new InvocationHandlerFactory() {
                @Override
                public InvocationHandler create(Target target,
                        Map<Method, MethodHandler> dispatch) {
                    // 创建 SentinelInvocationHandler
                }
            });
            // 替换 Contract(用于解析 FallbackFactory 等注解)
            super.contract(new SentinelContractHolder(contract));
            return super.build();
        }
    }
}

关键操作:

  1. 替换 InvocationHandlerFactory:把默认的工厂替换成自定义的,这样创建出来的 InvocationHandler 就是 SentinelInvocationHandler
  2. 替换 Contract:包装一层,用于支持 Sentinel 的 FallbackFactory 等注解

这就是典型的”偷天换日”:表面上还是 Feign.Builder,实际上内部已经被替换成 Sentinel 的实现了。

3.6 SentinelInvocationHandler 的创建

InvocationHandlerFactory#create 方法中,创建 SentinelInvocationHandler

// 通过反射获取 @FeignClient 注解的 fallback 属性
Class fallback = (Class) getFieldValue(feignClientFactoryBean, "fallback");
// 从 Feign 的上下文容器中获取 fallback 实例
Object fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
// 创建 SentinelInvocationHandler
return new SentinelInvocationHandler(target, dispatch,
                                new FallbackFactory.Default(fallbackInstance));

做了三件事:

  1. 通过反射从 FeignClientFactoryBean 获取 @FeignClient 注解的 fallback 属性值
  2. 根据 fallback 类型从 Feign 的上下文 Bean 工厂中取得 fallback 实例
  3. 创建 SentinelInvocationHandler,将 target、dispatch、fallback 都传进去

这样,当触发熔断时,SentinelInvocationHandler 就能拿到 fallback 实例并调用降级方法。


四、自己动手实现框架适配器

了解了这两种适配方式后,如果 Sentinel 没有提供你使用框架的适配器,你完全可以自己实现。

4.1 实现思路

选择合适的扩展点,遵循以下步骤:

  1. 找扩展点:找到框架提供的拦截器/过滤器/代理等扩展点
  2. 前置处理:在方法执行前调用 ContextUtil.enter()SphU.entry()
  3. 异常处理:捕获业务异常,调用 Tracer.trace() 记录
  4. 后置处理:在方法执行后调用 entry.exit()ContextUtil.exit()
  5. 降级处理:捕获 BlockException,执行降级逻辑

4.2 注意事项

实现自己的适配器时,需要注意:

  1. 资源名生成:合理设计资源名的生成策略,避免资源爆炸
  2. origin 解析:提供调用来源解析的扩展点
  3. 异常传递:确保业务异常被正确记录,否则熔断会失效
  4. Context 清理:确保 exit 一定会被调用,否则会有内存泄漏
  5. 线程安全:注意 ThreadLocal 的传递问题(特别是异步场景)

总结与思考

本文深入分析了 Sentinel 的两大主流框架适配:Spring MVC 和 OpenFeign。

核心要点回顾

1. Spring MVC 适配器

要点 说明
扩展点 HandlerInterceptor
入口 preHandle 中调用 ContextUtil.enter + SphU.entry
出口 afterCompletion 中调用 Tracer.trace + entry.exit + ContextUtil.exit
资源名 URL 模板 + UrlCleaner + 可选 HTTP 方法前缀
异常处理 BlockExceptionHandler 或全局异常处理器

2. OpenFeign 适配器

要点 说明
扩展点 InvocationHandler(JDK 动态代理)
实现方式 替换 Feign.Builder → 替换 InvocationHandlerFactory
入口 SentinelInvocationHandler 中调用 SphU.entry
出口 请求完成后调用 entry.exit
降级 fallback / fallbackFactory
位置 在 Ribbon 重试之前,统计不受重试影响

3. 通用适配思想

  • 找到框架的扩展点(拦截器、过滤器、代理等)
  • 前置:enter + entry
  • 后置:trace + exit + ContextUtil.exit
  • 捕获 BlockException 做降级处理

实践建议

  1. 优先使用官方适配器:官方适配器经过充分测试,稳定性有保障
  2. 合理设计资源名:避免资源数量过多导致内存占用过高
  3. 统一异常处理:建议使用全局异常处理器统一处理 BlockException
  4. 理解原理很重要:出问题时能快速定位,必要时可以自己扩展
  5. 注意异步场景:异步请求中 Context 的传递需要特殊处理

框架适配是 Sentinel 能够被广泛使用的关键,它让我们可以在几乎不修改业务代码的情况下,获得强大的服务保护能力。理解了适配原理,你就能更灵活地使用和扩展 Sentinel。

下一篇文章我们将进入集群限流的话题,看看 Sentinel 是如何实现分布式环境下的精准限流的,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞5
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!