news 2026/5/16 18:00:05

SpringCloud快速入门(11)---- Sentinel(异常处理)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringCloud快速入门(11)---- Sentinel(异常处理)

1.Web接口异常处理

1.1 问题场景

当我们对web接口进行了保护,例如流量控制时,访问量过多时sentinel会直接把错误信息返回:

这是因为sentinel默认是使用一个拦截器来实现的:

public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String resourceName = ""; try { resourceName = this.getResourceName(request); if (StringUtil.isEmpty(resourceName)) { return true; } else if (this.increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) { return true; } else { String origin = this.parseOrigin(request); String contextName = this.getContextName(request); ContextUtil.enter(contextName, origin); //资源保护流程 Entry entry = SphU.entry(resourceName, 1, EntryType.IN); request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry); //没有违反规则返回true,违法规则抛出BlockException异常 return true; } } catch (BlockException var12) { BlockException e = var12; try { //调用这个方法处理 this.handleBlockException(request, response, resourceName, e); } finally { ContextUtil.exit(); } return false; } } }

handleBlockException()最后会调用下面这个handle进行处理:

public class DefaultBlockExceptionHandler implements BlockExceptionHandler { public DefaultBlockExceptionHandler() { } public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException ex) throws Exception { response.setStatus(429); PrintWriter out = response.getWriter(); out.print("Blocked by Sentinel (flow limiting)"); out.flush(); out.close(); } }

也就输出了页面里的内容。这样的方式不适合前后端分离项目,我们需要自定义异常处理器统一返回 JSON。

1.2 自定义异常

定义一个统一返回对象:

package com.ting.common; import lombok.Data; @Data public class R { private Integer code; private String msg; private Object data; public static R ok() { R r = new R(); r.setCode(200); return r; } public static R ok(String msg, Object data) { R r = new R(); r.setCode(200); r.setMsg(msg); r.setData(data); return r; } public static R error() { R r = new R(); r.setCode(500); return r; } public static R error(Integer code, String msg) { R r = new R(); r.setCode(code); r.setMsg(msg); return r; } }

实现BlockExceptionHandler接口编写自定义返回内容:

@Component public class MyBlockExceptionHandler implements BlockExceptionHandler { private ObjectMapper objectMapper = new ObjectMapper(); @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception { PrintWriter writer = httpServletResponse.getWriter(); R error = R.error(500, s + "被Sentinel限制了,原因:" + e.getMessage()); writer.write(objectMapper.writeValueAsString(error)); } }

再次触发保护时就会返回我们设定好的内容

为什么我们实现了BlockExceptionHandler 就不会走DefaultBlockExceptionHandler 了:

看这段源码:

public SentinelWebMvcConfig sentinelWebMvcConfig() { SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); sentinelWebMvcConfig.setHttpMethodSpecify(this.properties.getHttpMethodSpecify()); sentinelWebMvcConfig.setWebContextUnify(this.properties.getWebContextUnify()); Optional var10000; //Optional<BlockExceptionHandler> blockExceptionHandlerOptional; //isPresent()表示判断Spring容器是否有BlockExceptionHandler的bean if (this.blockExceptionHandlerOptional.isPresent()) { //有,直接用 var10000 = this.blockExceptionHandlerOptional; Objects.requireNonNull(sentinelWebMvcConfig); var10000.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); } else if (StringUtils.hasText(this.properties.getBlockPage())) { //如果配置了自定义的限流跳转页面,则使用跳转方式处理异常 sentinelWebMvcConfig.setBlockExceptionHandler((request, response, resourceName, e) -> { response.sendRedirect(this.properties.getBlockPage()); }); } else { //使用默认的DefaultBlockExceptionHandler sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); } var10000 = this.urlCleanerOptional; Objects.requireNonNull(sentinelWebMvcConfig); var10000.ifPresent(sentinelWebMvcConfig::setUrlCleaner); var10000 = this.requestOriginParserOptional; Objects.requireNonNull(sentinelWebMvcConfig); var10000.ifPresent(sentinelWebMvcConfig::setOriginParser); return sentinelWebMvcConfig; }

注意:只有会被自动识别的资源(SpringMVC 接口,OpenFeign 远程调用接口,Gateway 网关路由接口)才会使用BlockExceptionHandler处理,@SentinelResource 定义的资源不会走BlockExceptionHandler

2. @SentinelResource添加的资源

2.1 源码解析

这里我们修改一下项目代码;

@RestController public class OrderController { @Autowired OrderService orderService; @GetMapping("/order") public Order createOrder( @RequestParam("userId") Long userId, @RequestParam("productId") Long productId) { return orderService.createOrder(userId, productId); } }
@Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired ProductFeignClient productFeignClient; @SentinelResource(value = "createOrder") @Override public Order createOrder(Long userId, Long productId) { log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)"); Product product = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(productId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum()))); order.setUserId(userId); order.setNickName("Ting"); order.setAddress("北京"); order.setProductList(Arrays.asList(product)); return order; } }

添加Service层,把业务逻辑移动到service层,并且把createOrder方法标注为createOrder资源,调用一次后我们就可以在sentinel控制台看见这个资源:

我们对其进行流量控制,快速点击触发保护:

可以发现并没有走BlockExceptionHandler进行处理,这是因为BlockExceptionHandler是基于web拦截器进行实现的,只能对于SpringMVC 接口,OpenFeign 远程调用接口,Gateway 网关路由接口,这种涉及到请求的资源生效。@SentinelResource 手动资源保护是基于SpringAOP实现的:

在SentinelResurceAspect这个类里我们可以看到到,定义了一个切点即@SentinelResource注解,添加了这个注解的方法就会使用下面的invokeResourceWithSentinel方法进行增强,可以看到在执行pjp.proceed()之前调用了SphU.entry()即检查是否违法了规则,如果正常则继续执行,异常则会抛出BlockException异常被下面的catch捕获进而调用handleBlockException()方法进行处理。

handleBlockException()方法中我们可以看到,检查了@SentinelResource注解中是否设置了blockHandler,如果设置了由invoke()方法处理,没有则由handleFallback()方法处理。在我们刚才的情况中我们没有设置任何东西,所有代码在这里会进入handleFallback()方法:

这里可以看到handleFallback()方法调用了他自己的一个重载方法,其中传入了两个关键参数:

annotation.fallback()和annotation.defaultFallback()

在下面方法中,首先通过fallback参数尝试获取了fallback方法,如果有则通过这个方法处理,但是我们并没有设置,所以这里获取的结果是null,会直接进入else中,调用handleDefaultFallback()方法通过默认的fallback(annotation.defaultFallback())进行处理。

在handleDefaultFallback()方法中先通过DefaultFallback参数尝试获取了默认fallback方法,但是我们什么都没有在@SentinelResource注解中设置,所以获取到的同样是null,这里直接进入了else也就是直接把异常抛出。

2.2 blockHandle

通过这个属性指定一个兜底回调,方法必须和原方法参数、返回值完全一致可以额外添加BlockException属性:

@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback") @Override public Order createOrder(Long userId, Long productId) { log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)"); Product product = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(productId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum()))); order.setUserId(userId); order.setNickName("Ting"); order.setAddress("北京"); order.setProductList(Arrays.asList(product)); return order; } //兜底回调 public Order createOrderFallback(Long userId, Long productId, BlockException e) { Order order = new Order(); order.setId(0L); order.setTotalAmount(new BigDecimal("0")); order.setUserId(0L); order.setNickName("出现异常:" + e.getClass()); order.setAddress(""); return order; }

当再次触发限流时就会触发我们的兜底回调:

注意:blockHandler只处理限流 / 熔断(BlockException)导致的异常

2.3 fallback

fallback 只处理业务异常(运行时异常),不会处理BlockException异常,方法必须和原方法参数、返回值完全一致,可以额外加 Throwable 参数。

注意:Sentinel 默认不会捕获业务异常,运行时异常会直接抛出去,不走 fallback,我们需要在配置文件中设置:

spring.cloud.sentinel.enabled=true
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback", fallback = "createOrderRuntimeExceptionFallback") @Override public Order createOrder(Long userId, Long productId) { log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)"); Order test = null; test.getAddress(); Product product = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(productId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum()))); order.setUserId(userId); order.setNickName("Ting"); order.setAddress("北京"); order.setProductList(Arrays.asList(product)); return order; } //兜底回调 public Order createOrderRuntimeExceptionFallback(Long userId, Long productId, Throwable e) { Order order = new Order(); order.setId(0L); order.setTotalAmount(new BigDecimal("0")); order.setUserId(0L); order.setNickName("出现运行时异常异常:" + e.getClass()); order.setAddress(""); return order; }

这我模拟了一个空指针的场景:

2.4 defaultFallback

和fallback类似,只处理业务异常(运行时异常),不会处理BlockException异常,通常用于对当前类多个业务方法做兜底返回,返回值必须和业务方法一致,支持无参或Throwable参数,当未指定fallback或者指定了未实现时会使用defaultFallback处理运行时异常:

@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback", defaultFallback = "OrderRuntimeExceptionDefaultFallback" ) @Override public Order createOrder(Long userId, Long productId) { log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)"); Order test = null; test.getAddress(); Product product = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(productId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum()))); order.setUserId(userId); order.setNickName("Ting"); order.setAddress("北京"); order.setProductList(Arrays.asList(product)); return order; } public Order OrderRuntimeExceptionDefaultFallback(Throwable e) { Order order = new Order(); order.setId(0L); order.setTotalAmount(new BigDecimal("0")); order.setUserId(0L); order.setNickName("出现运行时异常异常:" + e.getClass()); order.setAddress("OrderDefaultFallback"); return order; }

3. Feign远程调用资源

3.1 使用示例

在前面OpenFeign章节,我们已经写过示例:

@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class) public interface ProductFeignClient { @GetMapping("/product/{id}") Product getProductById(@PathVariable("id") Long id); }
@Component public class ProductFeignClientFallback implements ProductFeignClient { @Override public Product getProductById(Long id) { Product product = new Product(); product.setId(666L); product.setPrice(new BigDecimal("636")); product.setProductName("xiaomi666"); product.setNum(777); return product; } }

编写好fallback,在@FeignClient注解中指定fallback方法所在类即可

3.2 源码解析

在SentinelFeignAutoConfiguration,这个Sentinel和OpenFeign整合配置类中,这里可以看到注册了Feign.Builder到spring容器中,这里面就包含了所有的Feign客户端:

在这个类中内部构建方法可以看到,这里获取并判断了fallback是否存在,最后整合进了SentinelInvocationHandler

在这个类的invoke方法中就可以看到我们熟悉的逻辑,先判断是否违法规则,如果违法抛出异常,再判断是否有fallback,没有直接把异常抛出

4.SphU硬编码控制

我们可以通过SphU的entry方法对任意一段代码进行保护,这个方法了解即可

public Order createOrder(Long userId, Long productId) { log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)"); // Order test = null; // test.getAddress(); Product product = productFeignClient.getProductById(productId); Order order = new Order(); try { SphU.entry("resourceName"); order.setId(productId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum()))); order.setUserId(userId); order.setNickName("Ting"); order.setAddress("北京"); order.setProductList(Arrays.asList(product)); } catch (BlockException e) { //编码处理 } return order; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 17:51:04

Neovim AI模型集成框架model.nvim:统一配置与高级应用指南

1. 项目概述&#xff1a;一个为Neovim量身定制的模型集成框架如果你和我一样&#xff0c;长期在Neovim这个编辑器里摸爬滚打&#xff0c;从写配置、折腾插件到构建自己的开发工作流&#xff0c;那么你肯定对“效率”和“智能化”有着近乎偏执的追求。我们早已不满足于简单的语法…

作者头像 李华
网站建设 2026/5/16 17:47:06

开源机器人控制平台:从ROS架构到模块化任务控制实践

1. 项目概述&#xff1a;从“任务控制中心”到开源机器人控制平台如果你玩过机器人&#xff0c;或者对自动化设备控制有过一些了解&#xff0c;你肯定遇到过这样的困境&#xff1a;硬件组装好了&#xff0c;代码也写了几行&#xff0c;但如何让这些冰冷的零件“活”起来&#x…

作者头像 李华
网站建设 2026/5/16 17:42:56

基于MCP协议构建多链签名服务:架构、安全与实战指南

1. 项目概述&#xff1a;一个为MCP协议量身定制的加密签名工具如果你正在开发一个需要与区块链节点交互的应用&#xff0c;或者正在构建一个多链的钱包服务&#xff0c;那么“签名”这个环节你一定绕不过去。签名是区块链世界里的“数字指纹”&#xff0c;它证明了某笔交易或某…

作者头像 李华
网站建设 2026/5/16 17:42:07

AI开发平台TAI:PD分离加持,让大模型推理“快且稳”

在大型语言模型&#xff08;LLM&#xff09;推理系统中&#xff0c;效率与延迟的优化一直是工程团队面临的核心挑战。当你的AI服务需要同时满足"快速响应"和"稳定吞吐"这两个看似矛盾的需求时&#xff0c;传统的部署方式往往显得力不从心。今天&#xff0c…

作者头像 李华
网站建设 2026/5/16 17:41:08

LabelImg标注的YOLO格式txt坐标转换保姆级教程(附Python代码)

LabelImg标注的YOLO格式坐标转换实战指南&#xff1a;从原理到Python实现 在计算机视觉项目中&#xff0c;数据标注是模型训练前的关键步骤。LabelImg作为一款开源的图像标注工具&#xff0c;支持生成YOLO格式的标注文件。然而&#xff0c;许多开发者在实际应用中发现&#xff…

作者头像 李华
网站建设 2026/5/16 17:40:06

GitHub系统提示词库:提升大模型交互效率的工程实践指南

1. 项目概述&#xff1a;一个系统提示词的宝库如果你深度使用过ChatGPT、Claude或者DeepSeek这类大语言模型&#xff0c;那你一定对“系统提示词”这个概念不陌生。简单来说&#xff0c;它就是你发给模型的“第一条指令”&#xff0c;用来设定它的身份、行为准则和对话风格。比…

作者头像 李华