想要年薪百萬,阿里Sentinel支援RESTful介面都搞不定?
阿新 • • 發佈:2020-05-14
最近正準備用阿里Sentinel,發現RESTful介面支援的不是很好。有些童鞋可能對Sentinel不是很瞭解,我們先簡單介紹一下。
### Sentinel簡介
![](https://img2020.cnblogs.com/blog/145687/202005/145687-20200514091403697-2066806172.png)
Sentinel是一套阿里巴巴開源的流量防衛框架,Github地址是:[https://github.com/alibaba/Sentinel](https://github.com/alibaba/Sentinel)。隨著微服務的流行,服務與服務之間的穩定性越來越重要。Sentinel以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
更多介紹可以在[Github文件](https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D)中瞭解。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
### 問題描述
在Spring MVC或者Spring Boot中的RESTful介面中,有大量的@PathVariable註解,也就是把引數放在URL裡,比如:
```java
@RestController
public class DemoController {
@GetMapping(value = "/hello/{name}")
public String helloWithName(@PathVariable String name) {
return "Hello, " + name;
}
}
```
但是在Sentinel中把每一次請求的URL作為唯一的資源名,進行匹配和流量控制的,這就造成了一個介面本應是一個資源卻被當作多個資源看待,無法達到流量控制的目的。
> 白嫖小貼士:什麼是資源?只要通過 Sentinel API 包圍起來的程式碼,就是資源,能夠被 Sentinel 保護起來。例如,由應用程式提供的服務,或由應用程式呼叫的其它應用提供的服務,甚至可以是一段程式碼。每一個資源都有自己唯一的資源名,用於標識這個資源。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
### 查詢問題原因
問題的根本原因就在於:Sentinel是如何把每一次請求URL作為唯一的資源名的?閱讀和除錯Sentinel的原始碼後,我找到`CommonFilter`的`doFilter`方法,以下是主要程式碼:
```java
//呼叫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`介面上做文章了。
歡迎關注微信公眾號:萬貓學社 ,每週一分享Java技術乾貨。
### 解決方案
#### RestfulPattern
首先我們先建立一個類,用來存放URL和對應的正則表示式:
```java
package onemore.study.sentineldemo;
import java.util.regex.Pattern;
/**
* @author 萬貓學社
*/
public class RestfulPattern implements Comparable {
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());
}
}
```
#### RestfulUrlCleaner
再寫一個實現`UrlCleaner`介面的類,在`clean`方法中寫自己的邏輯:
```java
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 patterns = new ArrayList<>();
private RestfulUrlCleaner() {
}
/**
* 根據流量控制規則建立與之匹配的RestfulUrlCleaner
* @param rules 流量控制規則
* @return RestfulUrlCleaner
*/
public static RestfulUrlCleaner create(List 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;
}
}
```
#### 單元測試
為了驗證程式碼的正確性,我們再寫一下單元測試:
```java
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 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"));
}
}
```
執行一下單元測試,發現沒有錯誤。
歡迎關注微信公眾號:萬貓學社 ,每週一分享Java技術乾貨。
#### 設定UrlCleaner
在實際開發中,流量控制規則可能配置在Redis、ZooKeeper或者 Apollo中。無論在哪裡,流量控制規則每次發生變更時都要重新設定`UrlCleaner`。我們就以硬編碼流量控制規則為例:
```java
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 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介面多資源的問題被完美解決。
微信公眾號:萬貓學社
微信掃描二維碼
獲得更多Java技術乾貨