![图片[1]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/cover-2112.png)
本文导读
在前面的文章中,我们学习 Sentinel 的核心功能时,都是通过手动编写 try-catch-finally 代码块来包装受保护的资源。每次调用都需要:
- 调用
ContextUtil#enter进入上下文 - 调用
SphU#entry获取 Entry - 异常时调用
Tracer#trace记录异常 - finally 中调用
Entry#exit和ContextUtil#exit
如果每个接口都要手写这些代码,不仅繁琐,还容易出错。幸运的是,Sentinel 提供了对主流框架的适配支持,让我们可以”零侵入”地使用 Sentinel 的各种功能。
Sentinel 目前已经适配的框架包括:
- Web 层:Spring MVC、WebFlux、JAX-RS
- 服务调用:Dubbo、OpenFeign、RestTemplate
- 网关层:Spring Cloud Gateway、Zuul
- 消息队列:RocketMQ、Kafka
本文将深入分析两个最常用的框架适配实现:
- Spring MVC 适配器:基于 HandlerInterceptor 实现
- OpenFeign 适配器:基于 InvocationHandler 实现
理解这些适配原理后,即使 Sentinel 没有提供你使用框架的适配器,你也可以自己动手实现。
一、Sentinel 框架适配总览
1.1 框架适配的核心思想
Sentinel 框架适配的核心思想可以用一句话概括:在框架的入口点注入 Sentinel 的逻辑。
具体来说,就是找到框架提供的扩展点(拦截器、过滤器、动态代理等),在这些扩展点中插入 Sentinel 的调用逻辑,从而实现对业务代码的零侵入。
![图片[2]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/ae715de0-ed3b-11ea-be9c-f7616f01fc23.png)
不同框架的扩展点不同,适配方式也不同:
| 框架 | 扩展点 | 适配方式 |
| Spring MVC | HandlerInterceptor | 拦截器中调用 Sentinel |
| WebFlux | WebFilter | 过滤器中调用 Sentinel |
| Dubbo | Filter | RPC 过滤器中调用 Sentinel |
| OpenFeign | InvocationHandler | 动态代理中调用 Sentinel |
| Spring Cloud Gateway | GlobalFilter | 全局过滤器中调用 Sentinel |
1.2 适配的通用流程
无论哪种框架,适配的流程大体相似:
- 请求进入时:调用
ContextUtil.enter()和SphU.entry() - 业务执行:正常执行业务逻辑
- 异常时:调用
Tracer.trace()记录业务异常 - 请求结束时:调用
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 实现了 preHandle 和 afterCompletion 的核心逻辑,子类只需要实现获取资源名称的抽象方法即可。
这种设计非常经典:模板方法模式的应用,父类定义算法骨架,子类实现可变部分。
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:/hello、POST:/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;
}
}
流程解读:
- 获取资源名:调用子类实现的
getResourceName方法 - 解析调用来源:调用
OriginParser解析 origin(调用方标识) - 进入上下文:
ContextUtil.enter(),Context 名称固定为sentinel_spring_web_context - 获取 Entry:
SphU.entry(),资源类型为COMMON_WEB,流量类型为IN(入站) - 存储 Entry:将 Entry 存入
HttpServletRequest的属性中,方便后续取出 - 异常处理:捕获
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();
}
流程解读:
- 取出 Entry:从
HttpServletRequest的属性中取出 preHandle 阶段存入的 Entry - 记录异常并退出:调用
traceExceptionAndExit方法 - 退出 Context:
ContextUtil.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 集成原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/7b545840-ed3b-11ea-9210-e5c7b119b96e.png)
完整调用链:
- 调用
@FeignClient接口方法时,由SentinelInvocationHandler拦截方法执行 - 根据接口方法上的 URL 生成资源名,调用
SphU.entry()进行熔断检查 - 非熔断情况下,继续交给 OpenFeign 的
MethodHandler处理 - OpenFeign 从 Ribbon 获取一个服务提供者节点(负载均衡)
- OpenFeign 使用 HttpClient 发起 HTTP 请求
- 请求成功或异常(经过重试后),调用
Entry.exit()更新指标数据
重要细节:Sentinel 处在接口调用的最前端,因此 Sentinel 统计的指标数据不受 Ribbon 重试和 OpenFeign 重试的影响。也就是说,无论重试多少次,在 Sentinel 看来都只是一次调用。
3.3 核心原理:替换 InvocationHandler
OpenFeign 是基于 JDK 动态代理实现的,InvocationHandler 是动态代理的核心。Sentinel 正是通过替换 InvocationHandler 来实现拦截的。
![图片[4]-Sentinel 框架适配深度解析:Spring MVC 与 OpenFeign 集成原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/d7b506c0-ed3b-11ea-816b-87f82de6664d.png)
那么,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();
}
}
}
关键操作:
- 替换 InvocationHandlerFactory:把默认的工厂替换成自定义的,这样创建出来的
InvocationHandler就是SentinelInvocationHandler - 替换 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));
做了三件事:
- 通过反射从
FeignClientFactoryBean获取@FeignClient注解的fallback属性值 - 根据 fallback 类型从 Feign 的上下文 Bean 工厂中取得 fallback 实例
- 创建
SentinelInvocationHandler,将 target、dispatch、fallback 都传进去
这样,当触发熔断时,SentinelInvocationHandler 就能拿到 fallback 实例并调用降级方法。
四、自己动手实现框架适配器
了解了这两种适配方式后,如果 Sentinel 没有提供你使用框架的适配器,你完全可以自己实现。
4.1 实现思路
选择合适的扩展点,遵循以下步骤:
- 找扩展点:找到框架提供的拦截器/过滤器/代理等扩展点
- 前置处理:在方法执行前调用
ContextUtil.enter()和SphU.entry() - 异常处理:捕获业务异常,调用
Tracer.trace()记录 - 后置处理:在方法执行后调用
entry.exit()和ContextUtil.exit() - 降级处理:捕获
BlockException,执行降级逻辑
4.2 注意事项
实现自己的适配器时,需要注意:
- 资源名生成:合理设计资源名的生成策略,避免资源爆炸
- origin 解析:提供调用来源解析的扩展点
- 异常传递:确保业务异常被正确记录,否则熔断会失效
- Context 清理:确保 exit 一定会被调用,否则会有内存泄漏
- 线程安全:注意 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 做降级处理
实践建议
- 优先使用官方适配器:官方适配器经过充分测试,稳定性有保障
- 合理设计资源名:避免资源数量过多导致内存占用过高
- 统一异常处理:建议使用全局异常处理器统一处理 BlockException
- 理解原理很重要:出问题时能快速定位,必要时可以自己扩展
- 注意异步场景:异步请求中 Context 的传递需要特殊处理
框架适配是 Sentinel 能够被广泛使用的关键,它让我们可以在几乎不修改业务代码的情况下,获得强大的服务保护能力。理解了适配原理,你就能更灵活地使用和扩展 Sentinel。
下一篇文章我们将进入集群限流的话题,看看 Sentinel 是如何实现分布式环境下的精准限流的,敬请期待。












请登录后查看评论内容