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

1.Web接口异常处理

1.1 问题场景

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

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

java 复制代码
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进行处理:

java 复制代码
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 自定义异常

定义一个统一返回对象:

java 复制代码
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接口编写自定义返回内容:

java 复制代码
@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 了:

看这段源码:

java 复制代码
    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 源码解析

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

java 复制代码
@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);
    }
}
java 复制代码
@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属性:

java 复制代码
    @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,我们需要在配置文件中设置:

java 复制代码
spring.cloud.sentinel.enabled=true
java 复制代码
    @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处理运行时异常:

java 复制代码
    @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章节,我们已经写过示例:

java 复制代码
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
    @GetMapping("/product/{id}")
    Product getProductById(@PathVariable("id") Long id);
}
java 复制代码
@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方法对任意一段代码进行保护,这个方法了解即可

java 复制代码
    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;
    }
相关推荐
葫芦和十三6 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp6 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑7 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯7 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan10 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充12 小时前
1.面向对象设计思想
后端
IT_陈寒12 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro13 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗13 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端