前言

之前写过一篇文章聊聊因不失当应用alibaba sentinel而踩到的坑。其实这外面有些坑是因为在sentinel在mvc我的项目统计时,是基于mvc的拦截器来实现。这种形式会导致比方热点参数规定,比拟难获取到参数,因而要在我的项目中额定配置@SentinelResource注解才会失效。明天咱们就来聊下如何通过自定义注解把springmvc申请的性能和sentinel性能给整合起来

实现思路

外围思路通过一个注解把springmvc的@RequestMapping具备的性能 + @SentinelResource具备的性能给聚合起来

实现步骤

1、自定义注解

@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Mappingpublic @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替换为咱们自定义的注解即可

外围代码块

@Aspectpublic 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