背景: 由于公司最近在做一个广告系统, 其中我负责的广告跟踪模块有一个记录用户点击数的Api接口, 接口的url /api/event/click/{userId}
, 接口中使用了路径变量, 因此在处理的方法中就要用@PathVariable
进行处理. 之前一直没有关注路径变量和参数变量(@RequestParam
)在性能上的区别, 但是这个参与的广告系统对请求的响应要求很高. 因此在代码review的时候, 架构师发了一篇达达科技的文章, 里面提到了路径变量和参数变量在性能上的区别, 同时提出了相应的解决思路. 因此就按照达达科技的思路对原本的广告系统的实现进行了改造. 具体的达达科技提到的思路请看这里
1. 解决路径参数带来的性能问题的步骤
思路: 因为使用路径参数需要进行复杂的匹配流程以及正则匹配, 因此性能会比较低. 所以解决的思路就是跳过复杂的匹配流程以及正则匹配. 因为复杂的匹配流程和正则匹配的目的就是为了找到处理当前url的方法是哪一个, 因为这是一个内部系统, 因此调用端完全可以知道处理当前的url的方法是哪一个,可以通过url传递过来使用哪个方法进行处理, 因此就可以跳过复杂的匹配流程.
1.1 自定义查找url对应的处理方法
RequestMappingHandlerMapping
中查找url对应的处理方法是由lookupHandlerMethod
这个函数实现的, 在这个函数中会优先查找参数变量其次是路径变量url, 在查找到路径变量url后, 再进行正则的替换. 因此我们要做的就是如果url是路径变量就跳过这个方法, 而使用我们自己的查找方式
代码如下:
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final static Map HANDLER_METHOD_REQUEST_MAPPING_INFO_MAP = Maps.newHashMap();
// 用于保存处理方法和RequestMappingInfo的映射关系(这个方法在解析@RequestMapping时就会被调用, 达达科技中这个地方可能写的有问题, 文中提到覆写AbstractHandlerMethodMapping#registerMapping方法, 但是经过实验之后覆写这个方法不能生效)
@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 {
// 判断请求参数中是否带了event字段
String event = request.getParameter("event");
// 如果没有带则说明这次的请求不带路径参数, 则使用默认的处理
if(StringUtils.isEmpty(event)) {
return super.lookupHandlerMethod(lookupPath, request);
}
// 如果带了, 则从Map(这个Map中的entry在后面介绍)中获取处理当前url的方法
List handlerMethods = super.getHandlerMethodsForMappingName(event);
if(CollectionUtils.isEmpty(handlerMethods)) throw new ServiceException("没有找到指定的方法");
if(handlerMethods.size() > 1) throw new ServiceException("存在多个匹配的方法");
HandlerMethod handlerMethod = handlerMethods.get(0);
// 根据处理方法查找RequestMappingInfo, 用于解析路径url中的参数
RequestMappingInfo requestMappingInfo = HANDLER_METHOD_REQUEST_MAPPING_INFO_MAP.get(handlerMethod);
if(requestMappingInfo == null) throw new ServiceException("没有对应的匹配方法");
super.handleMatch(requestMappingInfo, lookupPath, request);
return handlerMethod;
}
}
1.2 注入自定义的RequestMappingHandlerMapping
因为我们的广告系统使用的是Spring boot, 因此可以通过继承WebMvcRegistrationsAdapter
, 并且覆写其中的getRequestMappingHandlerMapping
方法注入自己的RequestMappingHandlerMapping
.最后在继承WebMvcRegistrationsAdapter
的类上加上@Configuration
注解
这里有一个需要注意的地方: 如果使用的spring boot的版本低于1.4.1的话是没有WebMvcRegistrationsAdapter
, 这个时候如果直接继承WebMvcConfigurationSupport
来实现自定义的RequestMappingHandlerMapping
的话就会导致WebMvcAutoConfiguration
失效, 造成的结果就是DefaultViewResolver
, WelcomePageHandlerMapping
等的一些配置失效, 这个时候应该怎么办呢?可以参考Stack Overflow上的这两个issue: issue1, issue2 这里就不再赘述了. 代码如下
@Configuration
public class WebMvcConfig extends WebMvcRegistrationsAdapter {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new CustomRequestMappingHandlerMapping();
}
}
1.3 在@RequestMapping
中加上name
属性, 让解析的时候就生成<字符串, 处理方法>的集合
因为@RequestMapping
中name
属性不会用于url
的匹配, spring
会解析name
属性, 并将name
属性的值和处理方法进行关联, 这就正好可以满足我们的需求. 因此url
中传的event
的值就对应着name
属性的值, 这样就可以找到对应的处理方法了, 而不需要我们再维护一个集合
代码如下:
@Controller
@Api(tags = "2-User Event", description = "用户事件API")
@Validated
public class UserEventController extends BaseNasdaqController {
/**
* 点击计数
*
* @param userId
* @param hash
* @return
*/
@RequestMapping(name="click", value = EVENT_CLICK, method = GET)
@ApiOperation(value = "点击计数", notes = "点击计数+1")
public String click(@PathVariable Integer userId, @NotNull String hash) {
...
...
}
}