最近正准备用阿里Sentinel,发现RESTful接口支持的不是很好。有些童鞋可能对Sentinel不是很了解,我们先简单介绍一下。
Sentinel是一套阿里巴巴开源的流量防卫框架,Github地址是:https://github.com/alibaba/Sentinel。随着微服务的流行,服务与服务之间的稳定性越来越重要。Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
更多介绍可以在Github文档中了解。
在Spring MVC或者Spring Boot中的RESTful接口中,有大量的@PathVariable注解,也就是把参数放在URL里,比如:
@RestController public class DemoController { @GetMapping(value = "/hello/{name}") public String helloWithName(@PathVariable String name) { return "Hello, " + name; } }
但是在Sentinel中把每一次请求的URL作为唯一的资源名,进行匹配和流量控制的,这就造成了一个接口本应是一个资源却被当作多个资源看待,无法达到流量控制的目的。
白嫖小贴士:什么是资源?只要通过 Sentinel API 包围起来的代码,就是资源,能够被 Sentinel 保护起来。例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。每一个资源都有自己唯一的资源名,用于标识这个资源。
问题的根本原因就在于:Sentinel是如何把每一次请求URL作为唯一的资源名的?阅读和调试Sentinel的源码后,我找到CommonFilter
的doFilter
方法,以下是主要代码:
//调用filterTarget方法获取当前请求的URL String target = FilterUtil.filterTarget(sRequest); UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner(); if (urlCleaner != null) { target = urlCleaner.clean(target); } if (!StringUtil.isEmpty(target)) { String origin = parseOrigin(sRequest); String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target; ContextUtil.enter(contextName, origin); if (httpMethodSpecify) { //如果配置加HTTP方法名做前缀,URL前加HTTP方法名后作为资源名。 String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target; urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN); } else { //如果不加HTTP方法名做前缀,就直接使用URL作为资源名。 urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN); } }
在上面的代码中,我们看见了请求URL作为资源名的整个过程,同时也发现有一个UrlCleaner
接口,请求URL会经过它的clean
方法进行处理。我们就在这个UrlCleaner
接口上做文章了。
首先我们先创建一个类,用来存放URL和对应的正则表达式:
package onemore.study.sentineldemo; import java.util.regex.Pattern; /** * @author 万猫学社 */ public class RestfulPattern implements Comparable<RestfulPattern> { private Pattern pattern; private String realResource; public RestfulPattern(Pattern pattern, String realResource) { this.pattern = pattern; this.realResource = realResource; } public Pattern getPattern() { return pattern; } public String getRealResource() { return realResource; } @Override public int compareTo(RestfulPattern o) { return o.getPattern().pattern().compareTo(this.getPattern().pattern()); } }
再写一个实现UrlCleaner
接口的类,在clean
方法中写自己的逻辑:
package onemore.study.sentineldemo; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author 万猫学社 */ public class RestfulUrlCleaner implements UrlCleaner { private List<RestfulPattern> patterns = new ArrayList<>(); private RestfulUrlCleaner() { } /** * 根据流量控制规则创建与之匹配的RestfulUrlCleaner * @param rules 流量控制规则 * @return RestfulUrlCleaner */ public static RestfulUrlCleaner create(List<FlowRule> rules) { RestfulUrlCleaner cleaner = new RestfulUrlCleaner(); if (rules == null || rules.size() == 0) { return cleaner; } Pattern p = Pattern.compile("\\{[^\\}]+\\}"); for (FlowRule rule : rules) { Matcher m = p.matcher(rule.getResource()); //如果发现类似{xxx}的结构,断定其为RESTful接口 if (m.find()) { cleaner.patterns.add( new RestfulPattern(Pattern.compile(m.replaceAll("\\\\S+?")), rule.getResource())); } } //根据正则表达式重新排序 Collections.sort(cleaner.patterns); return cleaner; } @Override public String clean(String originUrl) { for (RestfulPattern pattern : patterns) { if (pattern.getPattern().matcher(originUrl).matches()) { return pattern.getRealResource(); } } return originUrl; } }
为了验证代码的正确性,我们再写一下单元测试:
package onemore.study.sentineldemo; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; import java.util.List; /** * @author 万猫学社 */ public class RestfulUrlCleanerTest { @Test public void test(){ List<FlowRule> rules = new ArrayList<>(); rules.add(new FlowRule("/hello")); rules.add(new FlowRule("/hello/{name}")); rules.add(new FlowRule("/hello/{firstName}/{lastName}")); rules.add(new FlowRule("/hello/{firstName}/and/{lastName}")); RestfulUrlCleaner cleaner = RestfulUrlCleaner.create(rules); Assert.assertEquals("/hello", cleaner.clean("/hello")); Assert.assertEquals("/hello/{name}", cleaner.clean("/hello/onemore")); Assert.assertEquals("/hello/{firstName}/{lastName}", cleaner.clean("/hello/onemore/study")); Assert.assertEquals("/hello/{firstName}/and/{lastName}", cleaner.clean("/hello/onemore/and/study")); } }
运行一下单元测试,发现没有错误。
在实际开发中,流量控制规则可能配置在Redis、ZooKeeper或者 Apollo中。无论在哪里,流量控制规则每次发生变更时都要重新设置UrlCleaner
。我们就以硬编码流量控制规则为例:
package onemore.study.sentineldemo; import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; @Configuration public class DemoConfiguration { @PostConstruct public void initRules() { List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("/hello/{name}"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); //设置QPS限流阈值为1 rule.setCount(1); rules.add(rule); WebCallbackManager.setUrlCleaner(RestfulUrlCleaner.create(rules)); FlowRuleManager.loadRules(rules); } }
至此,RESTful接口多资源的问题被完美解决。