Alibaba Sentinel RESTful 接口流控处理优化

0.前言

笔者最近打算使用Sentinel替换掉之前的Hystrix作为微服务架构的熔断/断路组件。整体上,Sentinel的设计比Hystrix要易用很多。在实际使用的过程中,也存在了一些问题。本文将介绍Sentinel 在处理RESTful 风格的web项目过程中存在的问题。

问题描述
在Spring Cloud架构下,如果Http请求格式是按照RESTful风格设计的,当大规模的Http请求访问系统集群,Sentinel Dashboard实时监控簇点链路的记录数非常多;看到的资源名已经飙到了几千条之多!另外虽然资源名数量庞大,但是监控的TPS和并发数却非常低,甚至很多资源名仅仅被访问过一次。想从这么多资源名中找到动态调控TPS或者并发数资源名,是非常困难的。另外由于统计量级的问题,也会导致 sentinel控制机器往Sentinel Dashboard传输的统计数据也非常大,整体请求下来,会导致整体服务的质量变得非常差。

1.RESTful接口问题重现:

1.1 在服务端定义一个RESTful风格的URL接口:
@Controller
public class TestController {
    @GetMapping("/api/v1/{tenantId}/order/{orderId}/basic")
    public String pay(@PathVariable("tenantId")String tenantId,@PathVariable("orderId")String orderId){
        return tenantId+":"+orderId;
    }
}
1.2 项目中引入spring-cloud-starter-alibaba-sentinel依赖
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
			<version>0.2.2.RELEASE</version>
		</dependency>
1.3 模拟客户端,请求Http服务,随机生成请求访问
        RestTemplate restTemplate = new RestTemplate();
        Random random = new Random(100000000);

        int poolSize = 100;
        ThreadPoolExecutor
            pool = new ThreadPoolExecutor(poolSize,poolSize,60, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1000000));
        for (int i = 0; i < poolSize; i++) {
            for (int j = 0; j < 9000; j++) {
                pool.submit(()->{
                    String tenantId = ""+random.nextInt(100000000);
                    String orderId =""+ random.nextInt(100000000);
                    //随机生成请求连接
                    String url = "http://<server-host>:<server-port>/api/v1/"+tenantId+"/order/"+orderId+"/basic";
                    String result = restTemplate.getForEntity(url,String.class).getBody();
                    System.out.println(result);
                });
            }
        }
1.4 在 Sentinel Dashboard 上的簇点链路实时跟踪已经飚到上千个资源名

在这里插入图片描述
在这里插入图片描述

问题分析
通过上述的例子中,可以看到,Sentinel 将每一个Http 请求的URL当成了一个唯一的资源名,用来做流控限制,这显然是非常不合适的。当大并发过来时,已经影响到了流控组件的性能消耗。

接下来我们将分析sentinel的工作机制,通过分析,来找到恰当的解决方案。

2. 当前Sentinel对于Web请求的处理原理

如果Spring Cloud 项目中,引入了spring-cloud-starter-alibaba-sentinel,那么该组件将自动创建一个拦截器,拦截所有的Http请求。当每一个请求进入系统后,该拦截器会获取到当前Http请求的URL路径,并将该URL路径 作为资源名,用来做sentinel 基于资源名的流控操作。整体请求的的行为如下图所示:
在这里插入图片描述

工作流程大概如下:

  • Http请求经过sentinel自定义的拦截器Sentinel CommonFilter,该拦截器从请求中提取URL, 然后将此当做流控资源名;
  • 提取流控资源名之后,进入Sentinel 核心的代码段entry.enterentry.exit包裹处理;
  • 包裹后,开始执行Spring MVC自身的HandlerMapping映射处理机制,从上下文中挑选合适的HandlerMethod,即某一个合适的Controller的@RequestMapping方法上
  • 执行Controller方法,返回结果给Sentinel CommonFilter,根据结果执行entry.exit,完成单次资源访问控制逻辑

其拦截器的实现也非常简单,如下所示:

public class CommonFilter implements Filter {

    private final static String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY";
    private final static String COLON = ":";
    private boolean httpMethodSpecify = false;

    @Override
    public void init(FilterConfig filterConfig) {
        httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest sRequest = (HttpServletRequest) request;
        Entry entry = null;

        Entry methodEntry = null;

        try {
            //提取当前HTTP请求的URL作为资源名
            String target = FilterUtil.filterTarget(sRequest);
            // 如果设置了URL清空机制,则使用URL清空器 清空
            // Clean and unify the URL.
            // For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or
            // the amount of context and resources will exceed the threshold.
            UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
            if (urlCleaner != null) {
                target = urlCleaner.clean(target);
            }

            // Parse the request origin using registered origin parser.
            String origin = parseOrigin(sRequest);

            ContextUtil.enter(target, origin);
            entry = SphU.entry(target, EntryType.IN);


            // Add method specification if necessary
            if (httpMethodSpecify) {
                methodEntry = SphU.entry(sRequest.getMethod().toUpperCase() + COLON + target,
                        EntryType.IN);
            }

            chain.doFilter(request, response);
        } catch (BlockException e) {
            HttpServletResponse sResponse = (HttpServletResponse) response;
            // Return the block page, or redirect to another URL.
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e);
        } catch (IOException e2) {
            Tracer.trace(e2);
            throw e2;
        } catch (ServletException e3) {
            Tracer.trace(e3);
            throw e3;
        } catch (RuntimeException e4) {
            Tracer.trace(e4);
            throw e4;
        } finally {
            if (methodEntry != null) {
                methodEntry.exit();
            }
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }
    }

    private String parseOrigin(HttpServletRequest request) {
        RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser();
        String origin = EMPTY_ORIGIN;
        if (originParser != null) {
            origin = originParser.parseOrigin(request);
            if (StringUtil.isEmpty(origin)) {
                return EMPTY_ORIGIN;
            }
        }
        return origin;
    }

    @Override
    public void destroy() {

    }

    private static final String EMPTY_ORIGIN = "";
}

通过上面的流程来看,问题就出现在Sentinel CommonFilter上。那么,既然Request URL不适合做 资源名,那什么适合做资源名控制呢?

3. RESTful 风格的请求,应当怎么定义Sentinel的资源名?

首先应当明确的是:资源名的选取,要具备实际的可控制的意义,按照上面所示的RESTful接口而言:

GET /api/v1/{tenantId}/order/{orderId}/basic

顾名思义,上述的URL表意是:获取某一个租户的某一个订单的基本信息,请求中存在两个PathVariable变量;
基于上述的定义,我们可以有如下几种方式挑选:

  • 选取方式1:上述的这个RequestMapping实际上是和对应的Controller的某一个方法是一一映射的,如果我们将资源名的定义界定为对某一个类的某一个方法的调用时,我们就可以选取/api/v1/{tenantId}/order/{orderId}/basic 作为资源名。这种维度是比较粗的,从定义上来看,我们限制的是当前系统内,对某一个Controller类的某一个方法的调用。
  • 选取方式2根据自己的需要,决定PathVariable的值是否可以作为资源创建的参数。实际上,如果定义成了方式1的资源名,通过系统的角度上而言,我可以控制系统内对这个资源的访问;而不能细化到某一个特定租户的请求访问;基于租户做流控限制这种需求很常见,假设在一个多租户的系统内,当某一个租户的请求猛增时,如果不加限制,可能会影响到其他租户的正常使用。所以从这个角度上,我希望的资源定义可以根据住户编号的不同,分别创建资源名,如下所示:
资源名解释
/api/v1/12345678/order/{orderId}/basic租户12345678的订单查询流控限制
/api/v1/88888888/order/{orderId}/basic租户88888888的订单查询流控限制
/api/v1/99999999/order/{orderId}/basic租户99999999的订单查询流控限制
  • 选取方式3完全使用Pathvariable的真实值来构造资源名,这种方式的结果就和问题举例一样,会导致sentinel性能极差。

上述三种方案,从产生的资源名数量来看 : 方式1 < 方式2 < 方式3,所以我们的策略应该尽可能往方式1方式2上靠。

另外,定义资源名时,应道考虑其可流控性,像/api/v1/99999999/order/3344455666/basic 这种查询某一个特定订单基本信息的请求,在实际的系统中,其并发访问实际上时非常低的,这样的就不具备可流控性,而/api/v1/99999999/order/{orderId}/basic请求,可以限定某一个的租户的所有查询订单请求,这个就具备可流控性

4.如何在过滤器(Filter)层获取到当前请求对应的HandlerMethod?

为了实现上述的方式1方式2,则需要有一个能力:需要在过滤器层就能知道当前请求应当被哪一个Controller的哪一个方法执行,即能够找到对应的HandlerMethod。而在目前的SpringMVC框架模式下,请求的调用关系是先经过拦截器,然后才能通过DispatchServlet的机制找到对应HandlerMethod,并处理。
在这里插入图片描述
那问题来了,怎么在Filter中提前感知到对应请求的HandlerMethod呢?
问题的答案在SpringMVC上,Spring MVC是如何为一个请求找对应的HandlerMethod的?

4.1 Spring MVC是如何为一个请求找对应的HandlerMethod的?

如下代码时SpringMVCDispatchServlet核心实现逻辑。整体流程会包含如下几步:

  • 步骤1:根据当前请求,查找HandlerExecutionChain,该对象内部包含HandlerMethod
  • 步骤2:根据当前HandlerExecutionChain 找到合适的HandlerAdaptor,用于处理请求
  • 步骤3:调用HandlerAdaptor,处理请求
  • 步骤4:返回ModelAndView
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// 步骤1:根据当前请求,查找HandlerExecutionChain,该对象内部包含HandlerMethod                         
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// 步骤2:根据当前HandlerExecutionChain 找到合适的HandlerAdaptor,用于处理请求
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// 步骤3:调用HandlerAdaptor,处理请求
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

                                 // 步骤4:返回ModelAndView
				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
               // 省略部分代码
	}

很明显,在步骤1的时候,我们已经可以获取到HandlerExecutionChain,该对象就包含了HandlerMethod。我们再来看这一步骤是怎么实现的:

	/**
	 * Return the HandlerExecutionChain for this request.
	 * <p>Tries all handler mappings in order.
	 * @param request current HTTP request
	 * @return the HandlerExecutionChain, or {@code null} if no handler could be found
	 */
	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

到这个代码处,我们已经能够获取到合适的HandlerMethod了,我们可以在拦截器内,执行上面的这段核心逻辑代码,这样我们就能狗在拦截器初期获取到HandlerMethod了。

4.2 修改Filter,使其支持在拦截器内部可以获取到HandlerMethod

对默认的Sentinel CommonFilter进行拓展,仿照DispatchServlet的实现逻辑,获取HandlerMethod,然后根据反射机制,获取到对应的Controller 方法调用引用上声明的@RequestMapping注解,然后将注解内容组合成资源名,并返回。

@Service
@Slf4j
public class SpringCommonFilter implements Filter {
    public final static String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY";
    public final static String EXCLUDE_URLS = "EXCLUDE_URLS";
    private final static String COLON = ":";
    private final static String ROOT_PATH = "/";
    private boolean httpMethodSpecify = false;
    private List<String> excludeUrls = new ArrayList<>();

    private Map<HandlerMethod,String> handlerMethodUrlMap = new ConcurrentHashMap<>(32);

    @Autowired
    private DispatcherServlet dispatcherServlet;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;

        String originalTarget = FilterUtil.filterTarget(httpServletRequest);
        if(excludeUrls.stream().anyMatch(url-> antPathMatcher.match(url,originalTarget))){
            chain.doFilter(request,response);
            return;
        }
        Entry entry = null;
        Entry methodEntry = null;
        try {
            String target = this.resolveTarget(httpServletRequest);
            // Parse the request origin using registered origin parser.
            String origin = parseOrigin(httpServletRequest);

            ContextUtil.enter(target, origin);
            entry = SphU.entry(target, EntryType.IN);

            // Add method specification if necessary
            if (httpMethodSpecify) {
                methodEntry = SphU.entry(httpServletRequest.getMethod().toUpperCase() + COLON + target, EntryType.IN);
            }

            chain.doFilter(request, response);
        } catch (BlockException e) {
            // Return the block page, or redirect to another URL.
            WebCallbackManager.getUrlBlockHandler().blocked(httpServletRequest, httpServletResponse, e);
        } catch (IOException e2) {
            Tracer.trace(e2);
            throw e2;
        } catch (ServletException e3) {
            Tracer.trace(e3);
            throw e3;
        } catch (RuntimeException e4) {
            Tracer.trace(e4);
            throw e4;
        } finally {
            if (methodEntry != null) {
                methodEntry.exit();
            }
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }
    }

    /**
     * Use Spring Mvc principle that searching the best matching HandlerMethod(aka. Controller Method)
     *  使用SpringMVC的查询Handler机制,查找合适的`HandlerMethod`
     * @param request servlet http request
     * @return
     */
    protected String resolveTarget(HttpServletRequest request) {
        String target = FilterUtil.filterTarget(request);

        String pattern = "";
        for (HandlerMapping mapping : dispatcherServlet.getHandlerMappings()) {
            HandlerExecutionChain handler = null;
            try {
                handler = mapping.getHandler(request);
                // handler hit, then resolve resource name from Controller and it's Controller method
                if (handler != null) {
                    Object handlerObject = handler.getHandler();
                    if (handlerObject instanceof HandlerMethod) {
                        HandlerMethod handlerMethod = (HandlerMethod)handlerObject;
                        //use it as cache
                        pattern = handlerMethodUrlMap.getOrDefault(handlerMethod,"");
                        if(StringUtils.isEmpty(pattern)){
                            //提取Controller方法上的注解值,拼装成Pattern
                            pattern = resolveResourceNameHandlerMethod(handlerMethod);
                            handlerMethodUrlMap.put(handlerMethod,pattern);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // Clean and unify the URL.
        // For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or
        // the amount of context and resources will exceed the threshold.
        UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
        if (urlCleaner != null) {
            if(StringUtils.isNotEmpty(pattern) && urlCleaner instanceof RestfulUrlCleaner){
                RestfulUrlCleaner restfulUrlCleaner = (RestfulUrlCleaner)urlCleaner;
                target = restfulUrlCleaner.clean(target,pattern);
            }else{
                target = urlCleaner.clean(target);
            }
        }
        return target;

    }

    /**
     * A HandlerMethod object usually represents  a controller's method which is annotated with
     * 
     * @param handlerMethod An object that represents an Controller's Method
     * @return the resource name
     * @RequestMapping ,@GetMapping , @PostMapping, @DeleteMapping and so on,
     * so the resource can be represented with the controller's methods correspondingly.
     * Although Controller's methods is not good option to represent Resources.
     * As a result , the Annotations on Controllers and their methods can be introspected according to
     * Spring original mechanisms.
     * <p>
     * The Resource should use following patterns:
     * <Http_method>:<Controller-class-level-url-annotation><Controller-method-level-url-annotation>
     */
    private String resolveResourceNameHandlerMethod(HandlerMethod handlerMethod) {
        String target;
        String typeMapping = "";
        RequestMapping typeRequestMapping =
            AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
        if (typeRequestMapping!=null && typeRequestMapping.value().length > 0) {
            typeMapping = typeRequestMapping.value()[0];
        }
        RequestMapping methodRequestMapping =
            AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), RequestMapping.class);
        String methodMapping = methodRequestMapping.value()[0];
        if (typeMapping.length() > 1 && typeMapping.endsWith(ROOT_PATH)) {
            typeMapping = typeMapping.substring(0, typeMapping.length() - 1);
        }
        target = typeMapping + methodMapping;
        return target;
    }

    @Override
    public void init(FilterConfig filterConfig) {
        httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY));
        String excludeUrlsString = filterConfig.getInitParameter(EXCLUDE_URLS);
        if(!StringUtils.isEmpty(excludeUrlsString)){
            excludeUrls = Arrays.asList(excludeUrlsString.split(","));
        }

    }

    private String parseOrigin(HttpServletRequest request) {
        RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser();
        String origin = EMPTY_ORIGIN;
        if (originParser != null) {
            origin = originParser.parseOrigin(request);
            if (StringUtil.isEmpty(origin)) {
                return EMPTY_ORIGIN;
            }
        }
        return origin;
    }

    @Override
    public void destroy() {

    }

    private static final String EMPTY_ORIGIN = "";
}
4.3. 覆盖掉spring-cloud-starter-alibaba-sentinel 的默认实现

想要覆盖掉默认的spring-cloud-starter-alibaba-sentinel实现,需要版本 >= 0.2.1.RELEASE,低版本不能通过配置的方式实现覆盖。

## 关闭默认实现
spring.cloud.sentinel.filter.enabled = false

自定义Configuration:

@Configuration
@EnableConfigurationProperties(SentinelProperties.class)
public class SentinelAutoConfiguration {
    private static final Logger log = LoggerFactory.getLogger(SentinelWebAutoConfiguration.class);

    private static final String SKIP_LIST_KEY = "spring.cloud.sentinel.pathvariable.skip.list";

    private static final String EXCLUDE_URLS = "spring.cloud.sentinel.excludeUrls";

    @Bean
    @ConditionalOnProperty(name = {"spring.cloud.sentinel.filter.enabled"}, havingValue = "false")
    @ConditionalOnBean(SentinelProperties.class)
    public FilterRegistrationBean sentinelFilter(SentinelProperties properties, SpringCommonFilter springCommonFilter,ApplicationContext applicationContext) {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean();
        org.springframework.cloud.alibaba.sentinel.SentinelProperties.Filter filterConfig = properties.getFilter();
        if (filterConfig.getUrlPatterns() == null || filterConfig.getUrlPatterns().isEmpty()) {
            List<String> defaultPatterns = new ArrayList();
            defaultPatterns.add("/*");
            filterConfig.setUrlPatterns(defaultPatterns);
        }
        registration.addUrlPatterns((String[])filterConfig.getUrlPatterns().toArray(new String[0]));
        registration.setFilter(springCommonFilter);
        Map<String,String> parameters = new HashMap<>();
        parameters.put(HTTP_METHOD_SPECIFY,"false");
        parameters.put(EXCLUDE_URLS,applicationContext.getEnvironment().getProperty(EXCLUDE_URLS, ""));
        registration.setInitParameters(parameters);
        registration.setOrder(filterConfig.getOrder());
        log.info("[Sentinel Starter] register Sentinel with urlPatterns: {}.", filterConfig.getUrlPatterns());
        return registration;
    }

    @Bean
    public SpringCommonFilter springCommonFilter(ApplicationContext applicationContext) {

        return new SpringCommonFilter();
    }

    @Bean
    @ConditionalOnMissingBean
    public UrlCleaner urlCleaner(ApplicationContext applicationContext) {
        String[] skipList =
            applicationContext.getEnvironment().getProperty(SKIP_LIST_KEY, "")
                .split(",");
        Set<String> skipSet = new HashSet<>();
        skipSet.addAll(Arrays.asList(skipList));
        return new RestfulUrlCleaner(skipSet);
    }

}

上述的变更,能够满足我们可以获取到Controller 方法上的注解表示作为资源名了。

4.4 优化结果展示

如下图所示,优化过的结果可以看到 大批量的URL请求已经成为 静态的资源名表示,和Controller注解上的@ReuqestMapping表示完全相同: /api/v1/{tenantId}/order/{orderId}/basic
在这里插入图片描述
在这里插入图片描述

5. RESTful参数化进一步优化

上面的流程上,可以看到,我们已经将RESTful的PathVariable问题解决了。而实际上,我们可能是希望将部分的PathVariable替换掉,那我们应该怎么做?
在这里插入图片描述
如下图所示,如果希望可以根据特定的擦除规则,我们可以拓展一下UrlCleaner来实现这方面的定制,来完成对特定PathVariable的擦除:

public class RestfulUrlCleaner implements UrlCleaner {

    public static final String ROOT_PATH = "/";

    private Set<String> skipSet = new HashSet<>();

    private static final Pattern pathVariable = Pattern.compile("\\{(\\w+)\\}");

    public RestfulUrlCleaner() {
    }

    public RestfulUrlCleaner(Set<String> skipSet) {
        this.skipSet = skipSet;
    }

    /***
     * <p>Process the url. Some path variables should be handled and unified.</p>
     * <p>e.g. collect_item_relation--10200012121-.html will be converted to collect_item_relation.html</p>
     *
     * @param originUrl original url
     * @return processed url
     */
    @Override
    public String clean(String originUrl) {
        return originUrl;
    }

    /***
     *  根据restful接口类型,进行清空
     *  url的格式: /api/{tenantId}/name/{transactionId}
     *  提取变量信息,然后确定哪些应该被替换,当前采用的默认策略是租户编号被替换,而营销订单ID不被替换
     * @param originUrl original url
     * @param pattern 匹配的pattern
     * @return processed url
     */
    public String clean(String originUrl, String pattern) {
        if (originUrl.startsWith(ROOT_PATH)) {
            originUrl = originUrl.substring(1);
        }
        if (pattern.startsWith(ROOT_PATH)) {
            pattern = pattern.substring(1);
        }
        String[] original = originUrl.split(ROOT_PATH);
        String[] patternArray = pattern.split(ROOT_PATH);
        if (original.length != patternArray.length) {
            return originUrl;
        }
        Matcher matcher;
        StringBuilder replacedUrl = new StringBuilder();
        for (int i = 0; i < patternArray.length; i++) {
            replacedUrl.append(ROOT_PATH);
            matcher = pathVariable.matcher(patternArray[i]);
            if (matcher.matches() && skipSet.contains(matcher.group(1))) {
                replacedUrl.append(matcher.group(0));
            } else {
                replacedUrl.append(original[i]);
            }
        }
        return replacedUrl.toString();
    }

    public static void main(String[] args) {

        HashSet<String> set = new HashSet<>();
        //        set.add("tenantId");
        set.add("transactionId");
        //        set.add("activityId");

        RestfulUrlCleaner restfulUrlCleaner = new RestfulUrlCleaner(set);

        String result = restfulUrlCleaner
            .clean("/api/00001234/A1224455/44433344566", "/api/{tenantId}/{activityId}/{transactionId}");

        System.out.println(result);

    }
}

通过这种改造方式,最终的效果如下图所示:
在这里插入图片描述

在这里插入图片描述

6. 结语:以上是针对sentinel RESTful接口优化的全部内容,如果实现上有任何问题,可扫描如下个人公众号留言。

作者水平有限,欢迎留言指正吐槽~


在这里插入图片描述

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值