被@ResponseBoby注释的方法在拦截器的posthandle方法中设置cookie失效的问题

文章标题可能有点绕口。先来解释下遇到的问题。

我写了一个拦截器,希望能够实现保存特定方法的请求参数到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的头信息已经被确定了!

原文地址:https://www.cnblogs.com/insaneXs/p/7738109.html