• 被@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的头信息已经被确定了!

  • 相关阅读:
    简单实现Http代理工具
    Silverlight+WCF 新手实例 象棋 棋子(三)
    Qt for S60 安装
    简单实现Http代理工具完善支持QQ代理
    openSUSE 11.2 初用与上网设置
    简单实现Http代理工具端口复用与QQ代理
    QT 智能提示设置
    Solaris 10 x86 继续折腾Mono
    Silverlight+WCF 新手实例 象棋 介绍(一)
    Qt Creator 运行s60 Emulator
  • 原文地址:https://www.cnblogs.com/insaneXs/p/7738109.html
Copyright © 2020-2023  润新知