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;
    }
相关推荐
_童年的回忆_1 小时前
【Linux】安装Jenkins并且打包发布springboot项目
linux·spring boot·jenkins
邪修king1 小时前
UE5 TA 核心修炼:材质与纹理艺术全解 —— 从 PBR 理论到工业级材质实战
c++·后端·游戏·ue5·材质
彭于晏Yan1 小时前
Maven 资源插件:非过滤文件后缀配置及风险规避
java·spring boot·maven
benpaodeDD1 小时前
idea里创建maven的web项目
java
青衫码上行1 小时前
如何接入AI大模型
java·人工智能·ai·langchain·ai编程
摇滚侠1 小时前
并发编程 Java 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·开发语言
兰令水1 小时前
topcode【随机算法题】【2026.5.15打卡-java版本】
java·算法·leetcode
uzong1 小时前
这套AI技术栈可将你的人工智能成本削减80%
人工智能·后端·架构
Ting-yu1 小时前
SpringCloud快速入门(10)---- Sentinel(应用场景&控制台安装)
spring·spring cloud·sentinel