【Sentinel入门】04 springmvc 集成Sentinel & springboot集成Sentinel & 全局异常处理类统一处理异常(BlockExceptionHandler接口)


系列文章:
【Sentinel入门】01 最简单的例子 helloworld
【Sentinel入门】02 @SentinelResource语法(blockHandler、fallback)
【Sentinel入门】03 规则类型(BlockException、FlowException、ParamFlowException、DegradeException)
【Sentinel入门】04 springmvc 集成Sentinel & 全局异常处理类统一处理异常(BlockExceptionHandler接口)
【Sentinel入门】05 springmvc 集成Sentinel & springboot集成Sentinel & 链路模式失效 & WebContextUnify & CommonFilter

相关文章:
【springboot集成sentinel】 sentinel-spring-webmvc-adapter
【springmvc框架】详解WebMvcConfigurer接口(Interceptor拦截器、ResourceHandler、Formatter、CorsMapping)

前言

官网也有示例,对应sentinel-demo-spring-webmvc

通过sentinel-spring-webmvc-adapter依赖,可以实现所有的springmvc定义的url自动作为sentinel的资源,并且支持全局异常处理类统一处理异常

注意本篇示例是基于springcloud的,而非集成springcloudAlibaba

WebCallbackManager是sentinel-web-servlet包中的接口,入参是UrlBlockHandler子类,用法:

WebCallbackManager.setUrlBlockHandler(xxxx)

BlockExceptionHandler接口 是sentinel-spring-webmvc-adapter包中的:

 SentinelWebMvcConfig config = new SentinelWebMvcConfig();
 config.setBlockExceptionHandler(new MySentinelExceptionHandler());

1. springmvc 集成Sentinel

经过前面文章的介绍,我们知道了如何使用sentinel的一些基础用法,以及常见的异常处理方式,每个url的资源或方法都可以通过@SentinelResource语法定义为一个资源,那么对于springmvc的url,每个url虽然可以作为资源。

常规声明语法:参见 Demo1Controller.java

@RestController
public class Demo1Controller {

    @Autowired
    private TestService service;
    //每个url入口都要声明 @SentinelResource
    @GetMapping("/foo1")
    @SentinelResource(value = "foo1", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public String apiFoo1(@RequestParam(required = false) Long t) throws Exception {
        if (t == null) {
            t = System.currentTimeMillis();
        }
        service.test();
        return service.hello(t);
    }

现在有个新的问题,如果有100个url,则需要100个声明,有没有统一的处理方式呢? 即我们定义一个全局的声明,默认100个url都使用该声明?

答案是有!

引入sentinel-spring-webmvc-adapter后,就可以通过自带的拦截器配置,默认会把所有的url资源纳入sentinel管理,完整的工程参见官网的 sentinel-demo-spring-webmvc

2. 测试

先启动Sentinel控制台

启动应用的项目,通过在vm参数设置 -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-demo-spring-webmvc

或在application.properties中配置:

csp.sentinel.dashboard.server=127.0.0.1:8080

然后访问:http://127.0.0.1:8088/hello 端口改为实际使用的端口,我的10000冲突了,改为8088
在这里插入图片描述

此时控制台已经把url纳入了管理,在簇点链路中能看到/hello资源:
在这里插入图片描述

我们增加一个流控规则试试:

在这里插入图片描述
快速多访问几次http://127.0.0.1:8088/hello:

在这里插入图片描述
说明流控生效了!

提示信息“Blocked by Sentinel (flow limiting)”来自DefaultBlockExceptionHandler

2.1 有什么特别地方吗

对比【Sentinel入门】02 @SentinelResource语法(blockHandler、fallback)里面的项目sentinel-demo-annotation-spring-aop工程,我们这里的工程没有在controller中添加@SentinelResource标签:

@Controller
public class WebMvcTestController {
   //注意:这里没有@SentinelResource标签
    @GetMapping("/hello")
    @ResponseBody
    public String apiHello() {
        doBusiness();
        return "Hello!";
    }

3. 全局统一异常处理

此时,我们知道省略了 @SentinelResource语法,流控规则仍能生效,问题来了,出现流控异常后,怎么给处理结果呢?即 blockHandler和blockHandlerClass对应的怎么实现?

常规用法,每个url对应的方法需要增加blockHandler :

    @Override
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }

而且以前的用法中,每个方法的入口都要额外用@SentinelResource声明,如果有100个url,则需要100个声明,现在,springmvc 集成Sentinel后,有统一的处理方式,即我们定义一个全局的声明,默认100个url都会使用该声明。

我们看下InterceptorConfig类,使用内置的一个BlockExceptionHandler接口的实现:

 //使用内置的一个BlockExceptionHandler接口的实现
 config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());

DefaultBlockExceptionHandler是一个全局异常处理类:

public class DefaultBlockExceptionHandler implements BlockExceptionHandler {
    public DefaultBlockExceptionHandler() {
    }

    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        response.setStatus(429);
        StringBuffer url = request.getRequestURL();
        if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) {
            url.append("?").append(request.getQueryString());
        }

        PrintWriter out = response.getWriter();
        //对应前面流控异常,浏览器展示的内容
        out.print("Blocked by Sentinel (flow limiting)");
        out.flush();
        out.close();
    }

3.1 自定义全局统一限流降级处理类

当然,我们也可以自定义一个我们自己的处理类,例如SentinelExceptionHandler 类,实现BlockExceptionHandler ,并将其声明为@Component注入到spring容器中:

package com.alibaba.csp.sentinel.demo.spring.webmvc.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MySentinelExceptionHandler implements BlockExceptionHandler {

        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception {

            String msg = null;

            if (ex instanceof FlowException) {
                msg = "访问频繁,请稍候再试";

            } else if (ex instanceof DegradeException) {
                msg = "系统降级";
//ParamFlowException异常需要额外的依赖包
//         <dependency>
//            <groupId>com.alibaba.csp</groupId>
//            <artifactId>sentinel-parameter-flow-control</artifactId>
//        </dependency>

//            } else if (ex instanceof ParamFlowException) {
//                msg = "热点参数限流";

            } else if (ex instanceof SystemBlockException) {
                msg = "系统规则限流或降级";

            } else if (ex instanceof AuthorityException) {
                msg = "授权规则不通过";

            } else {
                msg = "未知限流降级";
            }
            // http状态码
            response.setStatus(500);
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            response.setContentType("application/json;charset=utf-8");

            response.getWriter().write(msg);
        }

    }

然后指定并使用MySentinelExceptionHandler :

  config.setBlockExceptionHandler(new MySentinelExceptionHandler());

在这里插入图片描述

4. springboot集成Sentinel原理

前面提到的springmvc集成Sentinel就是 基于springboot集成Sentinel,区别于spring cloud alibaba集成Sentinel ,当然springboot集成Sentinel是spring cloud alibaba集成Sentinel的基础,学会了前者的原理,有助于学习后者的原理

springboot集成Sentinel的核心是拦截器,我们定义了一个InterceptorConfig 类,他是springmvc框架下的WebMvcConfigurer 接口的实现:

WebMvcConfigurer 接口参见 【springmvc框架】详解WebMvcConfigurer接口(Interceptor拦截器、ResourceHandler、Formatter、CorsMapping)

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 
   //重写父类方法
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Add Sentinel interceptor
        addSpringMvcInterceptor(registry);
    }

    private void addSpringMvcInterceptor(InterceptorRegistry registry) {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();

        // Depending on your situation, you can choose to process the BlockException via
        // the BlockExceptionHandler or throw it directly, then handle it
        // in Spring web global exception handler.

        // config.setBlockExceptionHandler((request, response, e) -> { throw e; });

        // Use the default handler.
        config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());

        // Custom configuration if necessary
        config.setHttpMethodSpecify(true);
        // By default web context is true, means that unify web context(i.e. use the default context name),
        // in most scenarios that's enough, and it could reduce the memory footprint.
        // If set it to false, entrance contexts will be separated by different URLs,
        // which is useful to support "chain" relation flow strategy.
        // We can change it and view different result in `Resource Chain` menu of dashboard.
        config.setWebContextUnify(true);
        config.setOriginParser(request -> request.getHeader("S-user"));

        // 注册一个 sentinel interceptor(拦截器)
        registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
    }

registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");注入了一个SentinelWebInterceptor拦截器,是系统内置的一个拦截器,拦截所有的url:

//继承了AbstractSentinelInterceptor 类
public class SentinelWebInterceptor extends AbstractSentinelInterceptor {

继承了AbstractSentinelInterceptor 类,拦截器的核心方法定义在此父类中:

public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        try {
 				....
            // Parse the request origin using registered origin parser.
            String origin = parseOrigin(request);
            String contextName = getContextName(request);
            ContextUtil.enter(contextName, origin);
            //核心代码, SphU.entry 硬编码添加sentinel资源
            Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
            request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
            return true;

SphU.entry硬编码添加sentinel资源。也就是说通过拦截器拦截所有的url资源,并且硬编码,实现所有的url注册为sentinel资源

5. SentinelSpringMvcBlockHandlerConfig全局异常类的作用

@ControllerAdvice
@Order(0)
public class SentinelSpringMvcBlockHandlerConfig {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @ExceptionHandler(BlockException.class)
    @ResponseBody
    public ResultWrapper sentinelBlockHandler(BlockException e) {
        logger.warn("Blocked by Sentinel: {}", e.getRule());
        // Return the customized result.
        return ResultWrapper.blocked();
    }
}

SentinelSpringMvcBlockHandlerConfig的作用是什么?首先我们知道SentinelSpringMvcBlockHandlerConfig的作用是全局异常类,但是已经定义了MySentinelExceptionHandler处理 springmvc的sentinel异常,这里又多定义一个,为啥?

作用是兜MySentinelExceptionHandler的底,如果MySentinelExceptionHandler里面不小心抛出了BlockException异常,那么就会被catch住!

参见 注释:

 * Spring configuration for global exception handler.
 * This will be activated when the {@code BlockExceptionHandler}
 * throws {@link BlockException directly}.

参考

Sentinel 限流降级,Sentinel持久化 参见 9、Sentinel全局限流降级结果返回


版权声明:本文为m0_45406092原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。