Alibaba Sentinel RESTful 接口流控处理优化
0.前言笔者最近打算使用Sentinel替换掉之前的Hystrix作为微服务架构的熔断/断路组件。整体上,Sentinel的设计比Hystrix要易用很多。在实际使用的过程中,也存在了一些问题。本文将介绍Sentinel 在处理RESTful 风格的web项目过程中存在的问题。问题描述:在Spring Cloud架构下,如果Http请求格式是按照RESTful风格设计的,当大规模的Http...
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.enter
和entry.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
的?
如下代码时SpringMVC
的DispatchServlet
核心实现逻辑。整体流程会包含如下几步:
- 步骤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接口优化的全部内容,如果实现上有任何问题,可扫描如下个人公众号留言。
作者水平有限,欢迎留言指正吐槽~
更多推荐
所有评论(0)