這是堅持技術寫作計劃(含翻譯)的第8篇,定個小目標999,每週最少2篇。
前段時間的文章多是運維方面的,最近放出一波後端相關的。
背景
最近開始使用Sentinel進行流量保護,但是預設的web servlet filter是攔截全部http請求。在傳統的專案中問題不大。但是如果專案中用了Spring MVC,並且用了@PathVariable就尷尬了。
比如 uri pattern是 /foo/{id}
,而從Sentinel監控看 /foo/1
和 /foo/2
就是兩個資源了,並且Sentinel最大支援6000個資源,再多就不生效了。
解決辦法
官方給的方案是:UrlCleaner
WebCallbackManager.setUrlCleaner(new UrlCleaner() {
@Override
public String clean(String originUrl) {
if (originUrl.startsWith(fooPrefix)) {
return "/foo/*";
}
return originUrl;
}
});
複製程式碼
但是想想就吐, /v1/{foo}/{bar}/qux/{baz}
這種的來個20來個,截一個我看看。
AOP
換種思路,uri pattern難搞,用笨辦法 aop總行吧?答案是可以的。
@Aspect
public class SentinelResourceAspect {
@Pointcut("within(com.anjia.*.web.rest..*)")
public void sentinelResourcePackagePointcut() {
// Method is empty as this is just a Pointcut, the implementations are
// in the advices.
}
@Around("sentinelResourcePackagePointcut()")
public Object sentinelResourceAround(ProceedingJoinPoint joinPoint) throws Throwable {
Entry entry = null;
// 務必保證finally會被執行
try {
// 資源名可使用任意有業務語義的字串
// 注意此處只是類名#方法名,方法過載是合併的,如果需要進行區分,
// 可以獲取引數型別加入到資源名稱上
entry = SphU.entry(joinPoint.getSignature().getDeclaringTypeName()+
"#"+joinPoint.getSignature().getName());
// 被保護的業務邏輯
// do something...
} catch (BlockException ex) {
// 資源訪問阻止,被限流或被降級
// 進行相應的處理操作
} finally {
if (entry != null) {
entry.exit();
}
}
return result;
}
}
複製程式碼
攔截器
溫習一下 Spring mvc的執行流程 doFilter -> doService -> dispatcher -> preHandle -> controller -> postHandle -> afterCompletion -> filterAfter
核心的是 String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
但是是在dispatcher階段才賦值的,所以在CommFilter是取不到的,所以導致使用官方的Filter是不行的。只能用攔截器
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner;
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager;
import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String origin = parseOrigin(request);
String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String uriTarget = StringUtils.defaultString(pattern,FilterUtil.filterTarget(request));
try {
// 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) {
uriTarget = urlCleaner.clean(uriTarget);
}
RecordLog.info(String.format("[Sentinel Pre Filter] Origin: %s enter Uri Path: %s", origin, uriTarget));
SphU.entry(uriTarget, EntryType.IN);
return true;
} catch (BlockException ex) {
RecordLog.warn(String.format("[Sentinel Pre Filter] Block Exception when Origin: %s enter fall back uri: %s", origin, uriTarget), ex);
WebCallbackManager.getUrlBlockHandler().blocked(request, response, ex);
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
while (ContextUtil.getContext() != null && ContextUtil.getContext().getCurEntry() != null) {
ContextUtil.getContext().getCurEntry().exit();
}
ContextUtil.exit();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
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;
}
private static final String EMPTY_ORIGIN = "";
}
複製程式碼
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Inject
SentinelHandlerInterceptor sentinelHandlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sentinelHandlerInterceptor);
}
}
複製程式碼
UrlBlockHandler和UrlCleaner和WebServletConfig.setBlockPage(blockPage)
上面說過,UrlCleaner是為了歸併請求,清洗url用的。而UrlBlockHandler是在被攔截後的預設處理器。但是clean和handler都不是鏈式的,所以如果有多種處理,需要自己在一個方法裡,進行邏輯判斷。
UrlCleaner
WebCallbackManager.setUrlCleaner(new UrlCleaner() {
@Override
public String clean(String originUrl) {
if (originUrl.startsWith(fooPrefix)) {
return "/foo/*";
}
return originUrl;
}
});
複製程式碼
UrlBlockHandler
如果通用一點的,可以自己根據request的 content-type進行自適應返回內容(PLAN_TEXT和JSON)
WebCallbackManager.setUrlBlockHandler((request, response, ex) -> {
response.addHeader("Content-Type","application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("{\"code\"":429,\"msg\":\"系統繁忙,請稍後重試\""}");
out.flush();
out.close();
});
複製程式碼
WebServletConfig.setBlockPage(blockPage)
WebServletConfig.setBlockPage("http://www.baidu.com")
複製程式碼
注意,三個方法都不是不支援呼叫鏈,比如我寫兩個UrlBlockHandler,只認最後一個。
參考資料
招聘小廣告
山東濟南的小夥伴歡迎投簡歷啊 加入我們 , 一起搞事情。
長期招聘,Java程式設計師,大資料工程師,運維工程師,前端工程師。