Spring自定义RequestMappingHandlerMapping避免PathVariable的性能低下

背景: 由于公司最近在做一个广告系统, 其中我负责的广告跟踪模块有一个记录用户点击数的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属性, 让解析的时候就生成<字符串, 处理方法>的集合

因为@RequestMappingname属性不会用于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) {
        ...
        ...
    }
}

你可能感兴趣的:(Spring自定义RequestMappingHandlerMapping避免PathVariable的性能低下)