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; }