Let's talk about how to integrate spring MVC and sentinel through custom annotations

preface

I wrote an article before Talk about the pit you stepped on due to the improper use of alibaba sentinel . In fact, there are some pitfalls because sentinel is based on mvc interceptors during mvc project statistics. This method will make it difficult to obtain parameters, such as hotspot parameter rules. Therefore, the @ SentinelResource annotation will not take effect until it is additionally configured in the project. Today, let's talk about how to integrate the spring mvc request function and sentinel function through custom annotations

Realization idea

The core idea is to aggregate the functions of @ RequestMapping of springmvc and @ SentinelResource through an annotation

Implementation steps

1. Custom annotation

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

In fact, this annotation integrates the @ RequestMapping and @ SentinelResource parameters

2. Implement @ RequestMapping function

1. Override 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: the core point of this rewrite is to be compatible with the existing functions of spring MVC

2. Replace the spring MVC default RequestMappingHandlerMapping with our own RequestMappingHandlerMapping

public class CircuitBreakerMappingWebMvcRegistrations implements WebMvcRegistrations {

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

3. Implement @ SentinelResource function

Because @ SentinelResource is implemented based on aop, we only need to replace @ SentinelResource used by aop with our customized annotation

Core code block

@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();
        }
    }
}

Integration effect demonstration

1. Write test controller

@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. Configure sentinel dashbord er address in YML

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080

3. Testing

3.1 flow control effect

a. Flow control effect not configured:


b. Configure flow control effect


3.2 degradation effect

a. Degradation effect not configured:


b. Configure degradation effect



3.3. Hot spot parameter flow control effect

a. Hot spot parameter flow control effect is not configured:

b. Configure hotspot parameter flow control effect


3.3 authorized flow control effect

a. Authorization flow control effect is not configured:


b. Configure authorization flow control effect

summary

Generally speaking, the idea is not very difficult. When implementing, pay attention to be compatible with the original functions. If you can't realize a function, you lose the original functions. Secondly, when implementing, it is important to pay attention to the version on which the implementation is based. For different versions, it may abolish some APIs or add some APIs. It may even be that the api has not changed, but the package name has changed

demo link

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

Keywords: Spring MVC sentinel

Added by kevincompton on Thu, 06 Jan 2022 05:27:48 +0200