文章标题可能有点绕口。先来解释下遇到的问题。
我写了一个拦截器,希望能够实现保存特定方法的请求参数到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。
然后我又试了下将saveParam移到preHandle中,结果就正常了。
而且这种情况只有在被@ResponseBody注释的方法上才会发生。
由于给response添加cookie的本质应该就是在reponse的header里写入一些信息。所以应该是某个流程后,再往response里写信息就无效了(之前看servlet的API里也有类似的情况,当response被提交过后,再对其进行一些操作会抛出异常)。
于是我猜想,这跟SpringMVC处理请求的流程有关。想起前些天Spring绑定请求参数的流程中,handler被invoke之后,有一个设置response的status的动作。
先随便找一个控制器试试:
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 }
从浏览器中查看结果:
发现两个cookie都是正常的。看来真想并没有这么简单。
于是只好从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文档里的说法:
isCommitted
public boolean isCommitted()
- Returns a boolean indicating if the response has been committed. A committed response has already had its status code and headers written.
划重点:A committed response has already had its status and headers written.
所以flush操作是会导致response的commited状态被修改的,也就是说这时response的头信息已经被确定了!