聊聊如何通過自定義註解實現springmvc和sentinel整合

linyb極客之路發表於2022-01-06

前言

之前寫過一篇文章聊聊因不恰當使用alibaba sentinel而踩到的坑。其實這裡面有些坑是因為在sentinel在mvc專案統計時,是基於mvc的攔截器來實現。這種方式會導致比如熱點引數規則,比較難獲取到引數,因此要在專案中額外配置@SentinelResource註解才會生效。今天我們就來聊下如何通過自定義註解把springmvc請求的功能和sentinel功能給整合起來

實現思路

核心思路通過一個註解把springmvc的@RequestMapping具備的功能 + @SentinelResource具備的功能給聚合起來

實現步驟

1、自定義註解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface CircuitBreakerMapping {

    //----------------RequestMapping-------------------------------
    /**
     * Assign a name to this mapping.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used on both levels, a combined name is derived by concatenation
     * with "#" as separator.
     * @see org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder
     * @see org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
     */
    String name() default "";

    /**
     * The primary mapping expressed by this annotation.
     * <p>This is an alias for {@link #path}. For example
     * {@code @RequestMapping("/foo")} is equivalent to
     * {@code @RequestMapping(path="/foo")}.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this primary mapping, narrowing it for a specific handler method.
     */
    @AliasFor("path")
    String[] value() default {};

    /**
     * The path mapping URIs (e.g. "/myPath.do").
     * Ant-style path patterns are also supported (e.g. "/myPath/*.do").
     * At the method level, relative paths (e.g. "edit.do") are supported
     * within the primary mapping expressed at the type level.
     * Path mapping URIs may contain placeholders (e.g. "/${connect}").
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this primary mapping, narrowing it for a specific handler method.
     * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
     * @since 4.2
     */
    @AliasFor("value")
    String[] path() default {};

    /**
     * The HTTP request methods to map to, narrowing the primary mapping:
     * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this HTTP method restriction (i.e. the type-level restriction
     * gets checked before the handler method is even resolved).
     */
    RequestMethod[] method() default {};

    /**
     * The parameters of the mapped request, narrowing the primary mapping.
     * <p>Same format for any environment: a sequence of "myParam=myValue" style
     * expressions, with a request only mapped if each such parameter is found
     * to have the given value. Expressions can be negated by using the "!=" operator,
     * as in "myParam!=myValue". "myParam" style expressions are also supported,
     * with such parameters having to be present in the request (allowed to have
     * any value). Finally, "!myParam" style expressions indicate that the
     * specified parameter is <i>not</i> supposed to be present in the request.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this parameter restriction (i.e. the type-level restriction
     * gets checked before the handler method is even resolved).
     * <p>Parameter mappings are considered as restrictions that are enforced at
     * the type level. The primary path mapping (i.e. the specified URI value)
     * still has to uniquely identify the target handler, with parameter mappings
     * simply expressing preconditions for invoking the handler.
     */
    String[] params() default {};

    /**
     * The headers of the mapped request, narrowing the primary mapping.
     * <p>Same format for any environment: a sequence of "My-Header=myValue" style
     * expressions, with a request only mapped if each such header is found
     * to have the given value. Expressions can be negated by using the "!=" operator,
     * as in "My-Header!=myValue". "My-Header" style expressions are also supported,
     * with such headers having to be present in the request (allowed to have
     * any value). Finally, "!My-Header" style expressions indicate that the
     * specified header is <i>not</i> supposed to be present in the request.
     * <p>Also supports media type wildcards (*), for headers such as Accept
     * and Content-Type. For instance,
     * <pre class="code">
     * &#064;RequestMapping(value = "/something", headers = "content-type=text/*")
     * </pre>
     * will match requests with a Content-Type of "text/html", "text/plain", etc.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this header restriction (i.e. the type-level restriction
     * gets checked before the handler method is even resolved).
     * @see org.springframework.http.MediaType
     */
    String[] headers() default {};

    /**
     * The consumable media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Content-Type} matches one of these media types.
     * Examples:
     * <pre class="code">
     * consumes = "text/plain"
     * consumes = {"text/plain", "application/*"}
     * </pre>
     * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Content-Type} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this consumes restriction.
     * @see org.springframework.http.MediaType
     * @see javax.servlet.http.HttpServletRequest#getContentType()
     */
    String[] consumes() default {};

    /**
     * The producible media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Accept} matches one of these media types.
     * Examples:
     * <pre class="code">
     * produces = "text/plain"
     * produces = {"text/plain", "application/*"}
     * produces = MediaType.APPLICATION_JSON_UTF8_VALUE
     * </pre>
     * <p>It affects the actual content type written, for example to produce a JSON response
     * with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
     * <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Accept} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this produces restriction.
     * @see org.springframework.http.MediaType
     */
    String[] produces() default {};



    //------------------------CircuitBreaker-------------------------------------

    EntryType entryType() default EntryType.OUT;

    int resourceType() default COMMON_WEB;

    String blockHandler() default "";

    Class<?>[] blockHandlerClass() default {};

    String fallback() default "";

    String defaultFallback() default "";

    Class<?>[] fallbackClass() default {};

    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

其實這個註解就是把@RequestMapping和@SentinelResource引數給整合一塊

2、實現@RequestMapping功能

1、重寫RequestMappingHandlerMapping
public class CircuitBreakerMappingHandlerMapping extends RequestMappingHandlerMapping {


    private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();

    private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();

    @Nullable
    private StringValueResolver embeddedValueResolver;



    @Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, CircuitBreakerMapping.class)
           );
    }

    @Nullable
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = this.createRequestMappingInfo(method);
        if (info != null) {
            RequestMappingInfo typeInfo = this.createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }

            String prefix = this.getPathPrefix(handlerType);
            if (prefix != null) {
                info = RequestMappingInfo.paths(new String[]{prefix}).build().combine(info);
            }
        }

        return info;
    }

    @Nullable
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        CircuitBreakerMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, CircuitBreakerMapping.class);
        RequestCondition<?> condition = element instanceof Class ? this.getCustomTypeCondition((Class)element) : this.getCustomMethodCondition((Method)element);
        return requestMapping != null ? this.createRequestMappingInfo(requestMapping, condition) : null;
    }


    protected RequestMappingInfo createRequestMappingInfo(
            CircuitBreakerMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

        RequestMappingInfo.Builder builder = RequestMappingInfo
                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name());
        if (customCondition != null) {
            builder.customCondition(customCondition);
        }
        return builder.options(this.config).build();
    }

    @Nullable
    String getPathPrefix(Class<?> handlerType) {
        for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
            if (entry.getValue().test(handlerType)) {
                String prefix = entry.getKey();
                if (this.embeddedValueResolver != null) {
                    prefix = this.embeddedValueResolver.resolveStringValue(prefix);
                }
                return prefix;
            }
        }
        return null;
    }
}

ps: 該重寫核心點是要相容springmvc已有的功能

2、將springmvc預設的RequestMappingHandlerMapping替換為我們自己實現的RequestMappingHandlerMapping
public class CircuitBreakerMappingWebMvcRegistrations implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new CircuitBreakerMappingHandlerMapping();
    }
}

3、實現@SentinelResource功能

因為@SentinelResource是基於aop進行實現,所以只需將aop使用@SentinelResource替換為我們自定義的註解即可

核心程式碼塊

@Aspect
public class CircuitBreakerAspect extends AbstractCircuitBreakerAspectSupport {



    @Around("@annotation(circuitBreakerMapping)")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp, CircuitBreakerMapping circuitBreakerMapping) throws Throwable {
        Method originMethod = resolveMethod(pjp);
        CircuitBreakerMapping controllerCircuitBreakerMapping = AnnotationUtils.findAnnotation(pjp.getTarget().getClass(),CircuitBreakerMapping.class);
        String baseResouceName = "lybgeek:";
        if(circuitBreakerMapping != null){
            baseResouceName = baseResouceName + controllerCircuitBreakerMapping.value()[0];
        }

        baseResouceName = baseResouceName + circuitBreakerMapping.value()[0];

        String resourceName = getResourceName(baseResouceName, originMethod);
        EntryType entryType = circuitBreakerMapping.entryType();
        int resourceType = circuitBreakerMapping.resourceType();
        Entry entry = null;
        try {
            String contextName = "lybgeek_circuitbreaker_context";
            RequestOriginParser parser = SpringUtil.getBean(RequestOriginParser.class);
            ContextUtil.enter(contextName,parser.parseOrigin(getRequest()));
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            Object result = pjp.proceed();
            return result;
        } catch (BlockException ex) {
            return handleBlockException(pjp, circuitBreakerMapping, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = circuitBreakerMapping.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, circuitBreakerMapping.exceptionsToTrace())) {
                traceException(ex, circuitBreakerMapping);
                return handleFallback(pjp, circuitBreakerMapping, ex);
            }

            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
            ContextUtil.exit();
        }
    }
}

整合效果演示

1、編寫測試控制器
@RestController
@CircuitBreakerMapping(value = "/test")
public class TestController {

    @CircuitBreakerMapping(value = "/flow/{username}")
    public String flow(@PathVariable("username") String username){

        return "flow circuit breaker mapping : " + username;
    }

    @CircuitBreakerMapping(value = "/degrade/{username}")
    public String degrade(@PathVariable("username") String username){

        if("zhangsan".equals(username)){
            throw new BizException(400,String.format("illgel username --> %s",username));
        }

        return "degrade circuit breaker mapping : " + username;
    }

    @CircuitBreakerMapping(value = "/paramFlow/{username}")
    public String paramFlow(@PathVariable("username") String username){

        return "paramFlow circuit breaker mapping : " + username;
    }


    @CircuitBreakerMapping(value = "/authority/{username}",fallback = "fallback")
    public String authority(@PathVariable("username") String username,String origin){
        System.out.println("origin:-->" + origin);
        return "authority circuit breaker mapping : " + username;
    }

    @CircuitBreakerMapping(value = "/{username}",fallback = "fallback")
    public String username(@PathVariable("username") String username){

        return " circuit breaker mapping : " + username;
    }

    public String fallback(String username){

        return "fallback circuit breaker mapping : " + username;
    }
}
2、application.yml中配置sentinel dashbord地址
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
3、測試

3.1、流控效果

a、 未配置流控效果:


b、 配置流控效果


3.2、降級效果

a、 未配置降級效果:


b、 配置降級效果



3.3、熱點引數流控效果

a、 未配置熱點引數流控效果:

b、 配置熱點引數流控效果


3.3、授權流控效果

a、 未配置授權流控效果:


b、 配置授權流控效果

總結

總體來說思路不是很難,實現的時候注意要相容原本的功能,不能實現一個功能,把原來具備的功能也弄沒了。其次實現的時候,注意一下是基於哪個版本進行實現,這個很重要,因為不同版本,它可能廢除一些api也可能新增一些api,甚至可能api沒變,但是包名變了

demo連結

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-circuit-breaker

相關文章