文章標題可能有點繞口。先來解釋下遇到的問題。web
我寫了一個攔截器,但願可以實現保存特定方法的請求參數到cookie中。瀏覽器
1 public class SaveParamInterceptor extends HandlerInterceptorAdapter{ 2 @Override 3 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 4 throws Exception { 5 // if(((HandlerMethod)handler).hasMethodAnnotation(SaveParam.class)){ 6 // saveParam(request, response); 7 // } 8 return super.preHandle(request, response, handler); 9 } 10 11 @Override 12 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 13 ModelAndView modelAndView) throws Exception { 14 if(((HandlerMethod)handler).hasMethodAnnotation(SaveParam.class)){ 15 saveParam(request, response); 16 } 17 super.postHandle(request, response, handler, modelAndView); 18 } 19 20 private void saveParam(HttpServletRequest request, HttpServletResponse response){ 21 Enumeration<String> enumeration = request.getParameterNames(); 22 while(enumeration.hasMoreElements()){ 23 String name = enumeration.nextElement(); 24 //過濾dataTables參數 25 if(name.startsWith("columns") || name.startsWith("search") || name.startsWith("order")){ 26 continue; 27 } 28 String value = request.getParameter(name); 29 Cookie cookie = new Cookie(name, value); 30 cookie.setMaxAge(3600); 31 cookie.setPath("/"); 32 response.addCookie(cookie); 33 System.out.println("name:" + name + " value:" + value); 34 } 35 } 36 }
一開始我將saveParam方法放在postHandle中。發現雖然請求能被正常攔截,可是頁面上取不到保存過的cookie。cookie
而後我又試了下將saveParam移到preHandle中,結果就正常了。app
並且這種狀況只有在被@ResponseBody註釋的方法上纔會發生。ide
因爲給response添加cookie的本質應該就是在reponse的header裏寫入一些信息。因此應該是某個流程後,再往response裏寫信息就無效了(以前看servlet的API裏也有相似的狀況,當response被提交事後,再對其進行一些操做會拋出異常)。post
因而我猜測,這跟SpringMVC處理請求的流程有關。想起前些天Spring綁定請求參數的流程中,handler被invoke以後,有一個設置response的status的動做。this
先隨便找一個控制器試試:spa
1 @RequestMapping("test") 2 @ResponseBody 3 @SaveParam 4 public JSONObject test(HttpServletResponse res) { 5 res.addCookie(new Cookie("befroe", "1")); 6 res.setStatus(200); 7 res.addCookie(new Cookie("after", "1")); 8 9 JSONObject object = new JSONObject; 10 return object; 11 }
從瀏覽器中查看結果:debug
發現兩個cookie都是正常的。看來真想並無這麼簡單。code
因而只好從Spring的流程在查一遍:
直接從ServletInvocableHandlerMethod的invokeAndHandle找起。
1 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, 2 Object... providedArgs) throws Exception { 3 4 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); 5 setResponseStatus(webRequest); 6 7 if (returnValue == null) { 8 if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) { 9 mavContainer.setRequestHandled(true); 10 return; 11 } 12 } 13 else if (StringUtils.hasText(this.responseReason)) { 14 mavContainer.setRequestHandled(true); 15 return; 16 } 17 18 mavContainer.setRequestHandled(false); 19 try { 20 this.returnValueHandlers.handleReturnValue( 21 returnValue, getReturnValueType(returnValue), mavContainer, webRequest); 22 } 23 catch (Exception ex) { 24 if (logger.isTraceEnabled()) { 25 logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); 26 } 27 throw ex; 28 } 29 }
進到handleReturnValue這個方法裏:
1 public void handleReturnValue(Object returnValue, MethodParameter returnType, 2 ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { 3 4 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); 5 if (handler == null) { 6 throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); 7 } 8 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); 9 }
這是選擇對應的處理器來處理返回值,繼續往下:
由於是被@ResponseBoby註釋的方法,因此咱們進到了RequestResponseBodyMethodProcessor的實現裏:
1 @Override 2 public void handleReturnValue(Object returnValue, MethodParameter returnType, 3 ModelAndViewContainer mavContainer, NativeWebRequest webRequest) 4 throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { 5 6 mavContainer.setRequestHandled(true); 7 ServletServerHttpRequest inputMessage = createInputMessage(webRequest); 8 ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); 9 10 // Try even with null return value. ResponseBodyAdvice could get involved. 11 writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); 12 }
前兩步幾部是設置了狀態,並將原生的request和response封裝一下在返回。咱們看writeWithMessageConverters裏作了啥,
1 protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, 2 ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) 3 throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { 4 5 Object outputValue; 6 Class<?> valueType; 7 Type declaredType; 8 9 if (value instanceof CharSequence) { 10 outputValue = value.toString(); 11 valueType = String.class; 12 declaredType = String.class; 13 } 14 else { 15 outputValue = value; 16 valueType = getReturnValueType(outputValue, returnType); 17 declaredType = getGenericType(returnType); 18 } 19 20 HttpServletRequest request = inputMessage.getServletRequest(); 21 List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request); 22 List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); 23 24 if (outputValue != null && producibleMediaTypes.isEmpty()) { 25 throw new IllegalArgumentException("No converter found for return value of type: " + valueType); 26 } 27 28 Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); 29 for (MediaType requestedType : requestedMediaTypes) { 30 for (MediaType producibleType : producibleMediaTypes) { 31 if (requestedType.isCompatibleWith(producibleType)) { 32 compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); 33 } 34 } 35 } 36 if (compatibleMediaTypes.isEmpty()) { 37 if (outputValue != null) { 38 throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); 39 } 40 return; 41 } 42 43 List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); 44 MediaType.sortBySpecificityAndQuality(mediaTypes); 45 46 MediaType selectedMediaType = null; 47 for (MediaType mediaType : mediaTypes) { 48 if (mediaType.isConcrete()) { 49 selectedMediaType = mediaType; 50 break; 51 } 52 else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { 53 selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; 54 break; 55 } 56 } 57 58 if (selectedMediaType != null) { 59 selectedMediaType = selectedMediaType.removeQualityValue(); 60 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { 61 if (messageConverter instanceof GenericHttpMessageConverter) { 62 if (((GenericHttpMessageConverter) messageConverter).canWrite( 63 declaredType, valueType, selectedMediaType)) { 64 outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 65 (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 66 inputMessage, outputMessage); 67 if (outputValue != null) { 68 addContentDispositionHeader(inputMessage, outputMessage); 69 ((GenericHttpMessageConverter) messageConverter).write( 70 outputValue, declaredType, selectedMediaType, outputMessage); 71 if (logger.isDebugEnabled()) { 72 logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 73 "\" using [" + messageConverter + "]"); 74 } 75 } 76 return; 77 } 78 } 79 else if (messageConverter.canWrite(valueType, selectedMediaType)) { 80 outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 81 (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 82 inputMessage, outputMessage); 83 if (outputValue != null) { 84 addContentDispositionHeader(inputMessage, outputMessage); 85 ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); 86 if (logger.isDebugEnabled()) { 87 logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 88 "\" using [" + messageConverter + "]"); 89 } 90 } 91 return; 92 } 93 } 94 } 95 96 if (outputValue != null) { 97 throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); 98 } 99 }
雖然寫了一大段,可是咱們看到對outputMessage進行操做的只有在下面這個for循環裏,咱們就重點關注下這裏操做了什麼:
1 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { 2 if (messageConverter instanceof GenericHttpMessageConverter) { 3 if (((GenericHttpMessageConverter) messageConverter).canWrite( 4 declaredType, valueType, selectedMediaType)) { 5 outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 6 (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 7 inputMessage, outputMessage); 8 if (outputValue != null) { 9 addContentDispositionHeader(inputMessage, outputMessage); 10 ((GenericHttpMessageConverter) messageConverter).write( 11 outputValue, declaredType, selectedMediaType, outputMessage); 12 if (logger.isDebugEnabled()) { 13 logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 14 "\" using [" + messageConverter + "]"); 15 } 16 } 17 return; 18 } 19 }
重點應該是在write這個方法裏,這裏是Converter對內容進行轉化。
因爲咱們用的conver是FastJsonHttpMessageConverter。
來看看具體實現:
1 public void write(Object t, // 2 Type type, // 3 MediaType contentType, // 4 HttpOutputMessage outputMessage // 5 ) throws IOException, HttpMessageNotWritableException { 6 7 HttpHeaders headers = outputMessage.getHeaders(); 8 if (headers.getContentType() == null) { 9 if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { 10 contentType = getDefaultContentType(t); 11 } 12 if (contentType != null) { 13 headers.setContentType(contentType); 14 } 15 } 16 if (headers.getContentLength() == -1) { 17 Long contentLength = getContentLength(t, headers.getContentType()); 18 if (contentLength != null) { 19 headers.setContentLength(contentLength); 20 } 21 } 22 writeInternal(t, outputMessage); 23 outputMessage.getBody().flush(); 24 }
看看是否是flush動做致使了response狀態改成已經被提交,因此致使設置cookie失效呢,再來試一試:
1 @RequestMapping("queryAuditList") 2 @ResponseBody 3 @SaveParam 4 public JSONObject queryAuditList( HttpServletResponse res) { 5 res.addCookie(new Cookie("befroe", "1")); 6 try { 7 res.getOutputStream().flush(); 8 } catch (IOException e) { 9 // TODO Auto-generated catch block 10 e.printStackTrace(); 11 } 12 res.addCookie(new Cookie("after", "1")); 13 return new JSONObject(); 14 }
看看結果:
果真是這樣!
再看下servlet文檔裏的說法:
public boolean isCommitted()
劃重點:A committed response has already had its status and headers written.
因此flush操做是會致使response的commited狀態被修改的,也就是說這時response的頭信息已經被肯定了!