先拋出問題。以下兩個方法聲明有毛區別:
@RequestMapping(value = "/rg") public void rg(@PathVariable Long pageId, @PathVariable Long moduleId) { Map<String, Object> result = new HashMap<String, Object>(); result.put("what", "haha"); sendJsonpResultJson(result); } @RequestMapping(value = "/rg") public void rg(HttpServletResponse response,@PathVariable Long pageId, @PathVariable Long moduleId) { Map<String, Object> result = new HashMap<String, Object>(); result.put("what", "haha"); sendJsonpResultJson(result); }
這是在一個Controller里面的接口方法聲明,這兩個方法,一個聲明了
HttpServletResponse response,
另一個沒有,他們看似沒有區別,但是spring mvc的套路里面,他們在特殊場景下的區別大的你想哭。
先描述問題的源頭:
某天刮風,飄來了一個接口需要處理
http://localhost:8088/1/2/rg.html
這個接口沒有什么特殊,GET請求,返回JSON數據,由於習慣使用g.html而不是g.json,再為了兼容jsonp。然而,依賴@ResponseBody注解的方式,對jsonp支持不夠完美。
因此,方法g里面,直接操作response,具體處理的地方是另一個地方,利用Filter+ThreadLocal實現的,因此,在g方法中無需聲明HttpServletResponse就能達到目的
HttpServletResponse response = this.getResponse(); if (StringUtils.isNotBlank(contentType)) { response.setContentType(contentType); } else { response.setContentType("application/json"); } response.setCharacterEncoding(SystemConstant.ENCODING_UTF_8); response.getWriter().print(obj);
這樣處理一下,接口也的確返回了數據,但是reponse.status一直是500。問題描述完畢。
怎么了,你累了,說好的200呢?
首先找到500的原因.
spring mvc里面,我配置了根據客戶端的不同的請求決定不同的view進行響應的視圖解析器
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="order" value="0"/> <property name="defaultViews"> <list> <bean id="mappingJackson2JsonView" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/> <!-- for application/json --> <!--<ref bean="mappingJackson2JsonView"/>--> </list> </property> </bean>
實驗證明,如果我不配置這個解析器,頁面會直接404,其實這是同一個問題,因此我去掉這個解析器,頁面立即顯示了閃花眼鏡的tomcat404
看到錯誤信息,加上404,聯想到/1/2/1/2/rg這個資源沒有找到,然而,我們本也沒打算給它配資源,get請求回去的,是json數據。
在DispaterSerlvet的方法doDispatch中,ModelAndView被賦值,利用這個入口,可以找到原因。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv);
循序打入斷點,到了解析方法參數的地方。
如果mv(即ModuleAndView)為null,則不會去尋找資源,因此開始尋找這個void請求,為毛還返回不為null的mv。
往下走,
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod中,調用getModelAndView
mavContainer.isRequestHandled()
為真,那么我們就達到目的了。(我為什么會這么推測了,因此Controller參數含response和不含response的情況下,我分別跟蹤了代碼。為什么想到需要response參數呢,因為別人家的接口都正確返回了200狀態,我家的為什么不按套路,只有對比了。)
return getModelAndView(mavContainer, modelFactory, webRequest); private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest, mavContainer); if (mavContainer.isRequestHandled()) { return null; } ....
往下走
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle中的判斷條件
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled(true); return; } } ....
這是請求Controller層接口的實現方法,由於我們的rg方法是void返回類型,因此,這里的returnValue是null,如果滿足
if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled())
就好,而帶response參數的請求
mavContainer.isRequestHandled()
為true。
往下走
最終
HandlerMethodArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
隱藏了我們想要的真相。
對於參數response,他對應的方法參數解析器是
org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver
@Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { if (mavContainer != null) { mavContainer.setRequestHandled(true); }
mavContainer是ModelAndView的容器,里面存放了很多信息,這里不深究了。
這里
mavContainer.setRequestHandled(true);
設置了請求已經被處理的標識,這樣
mavContainer.isRequestHandled()
就為真了。
最后在方法上加上了response參數后,最終返回的ModelAndView為null,也就不會去找資源
/1/2/1/2/rg
也就不會出現404了。
總結
HttpServletResponse httpServletResponse參數神奇的原因是
ServletResponseMethodArgumentResolver
它做了特殊處理,帶上這個參數的接口,都會被認為請求已經被處理了。
在springmvc里面,一個參數聲明與否,並不是等價的,即使你沒有用到,他也有存在的意義