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; //OptionalBlockExceptionHandler 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 定义的资源不会走BlockExceptionHandler2. 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()方法通过默认的fallbackannotation.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 fallbackfallback 只处理业务异常运行时异常不会处理BlockException异常方法必须和原方法参数、返回值完全一致可以额外加 Throwable 参数。注意Sentinel 默认不会捕获业务异常运行时异常会直接抛出去不走 fallback我们需要在配置文件中设置spring.cloud.sentinel.enabledtrueSentinelResource(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; }