springmvc restful优化

SpringMVC RESTful 性能优化

使用RESTful风格的接口有如下优势:

  • 语言无关(这点对于我们Python+Java的后台系统很关键)
  • 开发效率高、调试方便
  • 接口的语义明确然而缺点也显而易见:基于HTTP的RPC在效率上不如传统的RPC。
    在ModelService中,我们使用SpringMVC框架来实现RESTful接口。但是,在最近一次对ModelService的更新中我们发现SpringMVC的RESTful接口性能存在问题。

RESTful:

@RequestMapping(path = "/list/cityId/{cityId}", method = RequestMethod.GET)
@ResponseBody
public String getJsonByCityId(@PathVariable Integer cityId)

非RESTful:

@RequestMapping(path = "/list/cityId", method = RequestMethod.GET)
@ResponseBody
public String getJsonByCityId(@RequestParam Integer cityId)

 

我们使用Apache JMeter对SpringMVC RESTful接口与非RESTful接口进行了性能测试:

RESTful接口:
测试结果

非RESTful接口:
测试结果

*并发量为200
*测试在同一台机器上进行,执行业务逻辑相同,仅接口不同。
*为了证明的确是SpringMVC造成的问题,我们使用了最简单的业务逻辑,直接返回字符串。

由结果可见,非RESTful接口的性能是RESTful接口的两倍,且请求的最大响应时间是35毫秒,有99%的请求在20毫秒内完成。相比之下,RESTful接口的最大响应时间是436毫秒。

由于ModelService是一个对并发性能要求极高的系统,且被多个上层业务系统所依赖,所有请求需在50ms内返回,若超时则会引起上层系统的read timeout,进而导致502。所以需要对这一情况进行优化。

方案一:将所有的url修改为非RESTful风格(不使用@PathVariable)

这是最直接的方式,也是最能保证效果的方式。但是这么做需要修改的是ModelService中已有的全部100+个接口,同时也要修改客户端相应的调用。修改量太大,而且极有可能由于写错URL导致404。更令人不爽的是这种修改会导致接口没有了RESTful风格。故该方案只能作为备选。

方案二:对SpringMVC进行改造

根据实际现象以及测试的结果,几乎可以确定的是问题出在SpringMVC的RESTful路径查找中。所以我们对SpringMVC中的相关代码进行了调查。

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
(spring-webmvc-4.2.3.RELEASE) 

路径匹配的过程中有如下代码:

List<Match> matches = new ArrayList<Match>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
   addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
   // No choice but to go through all mappings...
   addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}

        SpringMVC首先对HTTP请求中的path与已注册的RequestMappingInfo(经解析的@RequestMapping)中的path进行一个完全匹配来查找对应的HandlerMethod,即处理该请求的方法,这个匹配就是一个Map#get方法。若找不到则会遍历所有的RequestMappingInfo进行查找。

        这个查找是不会提前停止的,直到遍历完全部的RequestMappingInfo。在遍历过程中,SpringMVC首先会根据@RequestMapping中的headers, params, produces, consumes, methods与实际的HttpServletRequest中的信息对比,剔除掉一些明显不合格的RequestMapping。如果以上信息都能够匹配上,那么SpringMVC会对RequestMapping中的path进行正则匹配,剔除不合格的。

        接下来会对所有留下来的候选@RequestMapping进行评分并排序。最后选择分数最高的那个作为结果。所以使用非RESTful风格的URL时,SpringMVC可以立刻找到对应的HandlerMethod来处理请求。但是当在URL中存在变量时,即使用了@PathVariable时,SpringMVC就会进行上述的复杂流程。

        从结果可见,这段匹配逻辑对性能的影响很大,URL数量越多,SpringMVC的性能越差,初步验证了我们从源码中得出的结论。在最近一次ModelService的更新中,接口数量翻了一倍,导致性能下降了一半,这也符合我们的结论。考虑到未来ModelService的接口必定会持续增加,我们肯定不能容忍在请求压力不断增加的情况下ModelService的性能反而不断下降的情况。所以现在我们要做的就是防止SpringMVC执行这种复杂的匹配逻辑,找到一种方式可以绕过它。

通过继承

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

我们可以实现自己的匹配逻辑。由于ModelService已经服务化,所以每个接口都有一个服务名,通过这个服务名即可直接找到对应的方法,并不需要通过@RequestMapping匹配的方式。而在服务消费端,由于服务消费端是通过服务名进行的方法调用,所以在服务消费端可以很直接地获取到服务名,把服务名加到HTTP请求的header中并不需要对代码进行大量的修改。

最终方案:

服务端:

  1. 在每个@RequestMapping中添加接口对应服务名的信息。
  2. 实现自己定义的HandlerMethod查询逻辑,在HandlerMethod注册时记录与之对应的服务名,在查询时通过HTTP请求头中的服务名查表获得HandlerMethod。

客户端:

  1. 调用服务时将服务名加入到HTTP请求头中

分析:

  • 这样的查询时间复杂度是O(1)的,典型的空间换时间。理论上使用这样的查找逻辑的效率和非RESTful接口的效率是一样的。
  • 由于HandlerMethod的注册是在服务启动阶段完成的,且在运行时不会发生改变,所以不用考虑注册的效率以及并发问题。
  • SpringMVC提供了一系列的方法可以让我们替换它的组件,所以该方案的可行性很高。

实现细节:

我们要建立一个HandlerMethod与服务名的映射,保存在一个Map中。注意到在@RequestMapping中有一个name属性,这个属性并没有被SpringMVC用在匹配逻辑中。该属性是用来在JSP中直接生成接口对应的URL的,但是在AbstractHandlerMethodMapping.MappingRegistry中已经提供了一个name与Handler Method的映射,直接拿来用即可。所以我们只需要在每个接口的@RequestMapping中添加name属性,值为接口的服务名。在SpringMVC启动时会自动帮我们建立起一个服务名与Handler Method的映射。我们只要在匹配时从HTTP请求头中获取请求的服务名,然后从该Map中查询到对应的HandlerMethod返回。如果没有查询到则调用父类中的原匹配逻辑,这样可以保证不会对现有的系统造成问题。

*小细节:

因为RESTful接口存在@PathVariable,我们还需要调用handleMatch方法来将HTTP请求的path解析成参数。然而这个方法需要的参数是RequestMappingInfo,并不是HandlerMethod,SpringMVC也没有提供任何映射,所以我们还是要自己实现一个HandlerMethod => RequestMappingInfo的反向查询表。重写AbstractHandlerMethodMapping#registerMapping方法即可在@RequestMapping的注册阶段完成映射的建立。

1:自定义的MappingHandlerMapping

public class actMappingHandlerMapping extends RequestMappingHandlerMapping {
    private static Map<String, HandlerMethod> NAME_HANDLER_MAP = new HashMap<String, HandlerMethod>();
    private static Map<HandlerMethod, RequestMappingInfo> MAPPING_HANDLER_MAP = new HashMap<HandlerMethod, RequestMappingInfo>();

    @Override
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        RequestMapping rMapping = AnnotationUtils.getAnnotation(method, RequestMapping.class);
        NAME_HANDLER_MAP.put(rMapping.name(), handlerMethod);
        MAPPING_HANDLER_MAP.put(handlerMethod, mapping);
        System.out.println("======================name=" + rMapping.name() + "=handlerMethod="
                + handlerMethod.toString());
        super.registerHandlerMethod(handler, method, mapping);
    }

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        String api = request.getHeader("api");
        HandlerMethod handlerMethod = NAME_HANDLER_MAP.get(api);
        if (StringUtils.isNotBlank(api) && handlerMethod != null) {
            handleMatch(MAPPING_HANDLER_MAP.get(handlerMethod), lookupPath, request);
            return handlerMethod;
        }
        return super.lookupHandlerMethod(lookupPath, request);
    }

}
 2:Spring-servlet.xml配置添加
<bean name="handlerAdapter"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
	<property name="webBindingInitializer">
		<bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
	                <property name="conversionService" ref="conversionService" />
		</bean>
	</property>
	<property name="messageConverters">
		<list>
			<ref bean="stringHttpMessageConverter" />
			<ref bean="fastJsonHttpMessageConverter" />
		</list>
	</property>
</bean>
<bean name="conversionService"
class="org.springframework.format.support.DefaultFormattingConversionService" />
<bean name="handlerMapping" class="com.mact.flter.actMappingHandlerMapping" />
 
 3:对ajax支持可能需要这个拦截器
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,
            ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        httpResponse.setHeader("Access-Control-Allow-Headers", "api");
        if ("OPTIONS".equals(httpRequest.getMethod())) {
            httpResponse.setStatus(204);
            httpResponse.setHeader("Cache-Control", "no-cache");
        }
        filterChain.doFilter(request, response);
    }
 

感谢达达技术分析

猜你喜欢

转载自finishx.iteye.com/blog/2346648