背景: 由于公司最近在做一個(gè)廣告系統(tǒng), 其中我負(fù)責(zé)的廣告跟蹤模塊有一個(gè)記錄用戶點(diǎn)擊數(shù)的Api接口, 接口的url /api/event/click/{userId}
, 接口中使用了路徑變量, 因此在處理的方法中就要用@PathVariable
進(jìn)行處理. 之前一直沒有關(guān)注路徑變量和參數(shù)變量(@RequestParam
)在性能上的區(qū)別, 但是這個(gè)參與的廣告系統(tǒng)對請求的響應(yīng)要求很高. 因此在代碼review的時(shí)候, 架構(gòu)師發(fā)了一篇達(dá)達(dá)科技的文章, 里面提到了路徑變量和參數(shù)變量在性能上的區(qū)別, 同時(shí)提出了相應(yīng)的解決思路. 因此就按照達(dá)達(dá)科技的思路對原本的廣告系統(tǒng)的實(shí)現(xiàn)進(jìn)行了改造. 具體的達(dá)達(dá)科技提到的思路請看這里
1. 解決路徑參數(shù)帶來的性能問題的步驟
思路: 因?yàn)槭褂寐窂絽?shù)需要進(jìn)行復(fù)雜的匹配流程以及正則匹配, 因此性能會(huì)比較低. 所以解決的思路就是跳過復(fù)雜的匹配流程以及正則匹配. 因?yàn)閺?fù)雜的匹配流程和正則匹配的目的就是為了找到處理當(dāng)前url的方法是哪一個(gè), 因?yàn)檫@是一個(gè)內(nèi)部系統(tǒng), 因此調(diào)用端完全可以知道處理當(dāng)前的url的方法是哪一個(gè),可以通過url傳遞過來使用哪個(gè)方法進(jìn)行處理, 因此就可以跳過復(fù)雜的匹配流程.
1.1 自定義查找url對應(yīng)的處理方法
RequestMappingHandlerMapping
中查找url對應(yīng)的處理方法是由lookupHandlerMethod
這個(gè)函數(shù)實(shí)現(xiàn)的, 在這個(gè)函數(shù)中會(huì)優(yōu)先查找參數(shù)變量其次是路徑變量url, 在查找到路徑變量url后, 再進(jìn)行正則的替換. 因此我們要做的就是如果url是路徑變量就跳過這個(gè)方法, 而使用我們自己的查找方式
代碼如下:
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final static Map<HandlerMethod, RequestMappingInfo> HANDLER_METHOD_REQUEST_MAPPING_INFO_MAP = Maps.newHashMap();
// 用于保存處理方法和RequestMappingInfo的映射關(guān)系(這個(gè)方法在解析@RequestMapping時(shí)就會(huì)被調(diào)用, 達(dá)達(dá)科技中這個(gè)地方可能寫的有問題, 文中提到覆寫AbstractHandlerMethodMapping#registerMapping方法, 但是經(jīng)過實(shí)驗(yàn)之后覆寫這個(gè)方法不能生效)
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
HandlerMethod handlerMethod = super.createHandlerMethod(handler, method);
HANDLER_METHOD_REQUEST_MAPPING_INFO_MAP.put(handlerMethod, mapping);
super.registerHandlerMethod(handler, method, mapping);
}
@Override
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
// 判斷請求參數(shù)中是否帶了event字段
String event = request.getParameter("event");
// 如果沒有帶則說明這次的請求不帶路徑參數(shù), 則使用默認(rèn)的處理
if(StringUtils.isEmpty(event)) {
return super.lookupHandlerMethod(lookupPath, request);
}
// 如果帶了, 則從Map(這個(gè)Map中的entry在后面介紹)中獲取處理當(dāng)前url的方法
List<HandlerMethod> handlerMethods = super.getHandlerMethodsForMappingName(event);
if(CollectionUtils.isEmpty(handlerMethods)) throw new ServiceException("沒有找到指定的方法");
if(handlerMethods.size() > 1) throw new ServiceException("存在多個(gè)匹配的方法");
HandlerMethod handlerMethod = handlerMethods.get(0);
// 根據(jù)處理方法查找RequestMappingInfo, 用于解析路徑url中的參數(shù)
RequestMappingInfo requestMappingInfo = HANDLER_METHOD_REQUEST_MAPPING_INFO_MAP.get(handlerMethod);
if(requestMappingInfo == null) throw new ServiceException("沒有對應(yīng)的匹配方法");
super.handleMatch(requestMappingInfo, lookupPath, request);
return handlerMethod;
}
}
1.2 注入自定義的RequestMappingHandlerMapping
因?yàn)槲覀兊膹V告系統(tǒng)使用的是Spring boot, 因此可以通過繼承WebMvcRegistrationsAdapter
, 并且覆寫其中的getRequestMappingHandlerMapping
方法注入自己的RequestMappingHandlerMapping
.最后在繼承WebMvcRegistrationsAdapter
的類上加上@Configuration
注解
這里有一個(gè)需要注意的地方: 如果使用的spring boot的版本低于1.4.1的話是沒有WebMvcRegistrationsAdapter
, 這個(gè)時(shí)候如果直接繼承WebMvcConfigurationSupport
來實(shí)現(xiàn)自定義的RequestMappingHandlerMapping
的話就會(huì)導(dǎo)致WebMvcAutoConfiguration
失效, 造成的結(jié)果就是DefaultViewResolver
, WelcomePageHandlerMapping
等的一些配置失效, 這個(gè)時(shí)候應(yīng)該怎么辦呢?可以參考Stack Overflow上的這兩個(gè)issue: issue1, issue2 這里就不再贅述了. 代碼如下
@Configuration
public class WebMvcConfig extends WebMvcRegistrationsAdapter {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new CustomRequestMappingHandlerMapping();
}
}
1.3 在@RequestMapping
中加上name
屬性, 讓解析的時(shí)候就生成<字符串, 處理方法>的集合
因?yàn)?code>@RequestMapping中name
屬性不會(huì)用于url
的匹配, spring
會(huì)解析name
屬性, 并將name
屬性的值和處理方法進(jìn)行關(guān)聯(lián), 這就正好可以滿足我們的需求. 因此url
中傳的event
的值就對應(yīng)著name
屬性的值, 這樣就可以找到對應(yīng)的處理方法了, 而不需要我們再維護(hù)一個(gè)集合
代碼如下:
@Controller
@Api(tags = "2-User Event", description = "用戶事件API")
@Validated
public class UserEventController extends BaseNasdaqController {
/**
* 點(diǎn)擊計(jì)數(shù)
*
* @param userId
* @param hash
* @return
*/
@RequestMapping(name="click", value = EVENT_CLICK, method = GET)
@ApiOperation(value = "點(diǎn)擊計(jì)數(shù)", notes = "點(diǎn)擊計(jì)數(shù)+1")
public String click(@PathVariable Integer userId, @NotNull String hash) {
...
...
}
}