当前位置:   article > 正文

「getWriter() has already been called for this response」问题处理全过程

getwriter() has already been called for this response

最近上线一个版本之后,发现微信支付回调那里老是报如下的异常

熟悉支付的同学应该知道,回调里处理完业务逻辑之后,要给微信/支付宝回调成功的响应

java.lang.IllegalStateException: getWriter() has already been called for this response
        at org.apache.catalina.connector.Response.getOutputStream(Response.java:591)
        at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:194)
        at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100)
        at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100)
  • 1
  • 2
  • 3
  • 4
  • 5

我的回调的代码如下:

public String onNotify(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        logger.info("微信支付回调数据开始");
        //业务逻辑省略
        String resXml ="<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                    + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
        }

      
		//执行完业务之后,将支付成功的标志响应给微信,否则微信得不到我们SUCCESS的
		//响应,会一直重试
        BufferedOutputStream out = new BufferedOutputStream(
                response.getOutputStream());
        out.write(resXml.getBytes());
        out.flush();
        out.close();
        log.info("微信支付回调数据结束");
        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

很奇怪,为什么会报出getWriter() has already been called for this response这个错误信息,我的方法体里没有用到getWriter()呀?


好吧,先从为什么报这个错开始来看吧。
从报错堆栈信息来看,response.getOutputStream()->ResponseFacade.getOutputStream()->response.getOutputStream()

其实response.getWriter()的调用逻辑与上面的很类似,你会在Response类里发现getWriter()这个方法

protected boolean usingOutputStream = false;
protected boolean usingWriter = false;
public ServletOutputStream getOutputStream()
        throws IOException {

        if (usingWriter) {
            throw new IllegalStateException
                (sm.getString("coyoteResponse.getOutputStream.ise"));
        }

        usingOutputStream = true;
        if (outputStream == null) {
            outputStream = new CoyoteOutputStream(outputBuffer);
        }
        return outputStream;

    }
    
	@Override
    public PrintWriter getWriter()
        throws IOException {

        if (usingOutputStream) {
            throw new IllegalStateException
                (sm.getString("coyoteResponse.getWriter.ise"));
        }

        if (ENFORCE_ENCODING_IN_GET_WRITER) {
            setCharacterEncoding(getCharacterEncoding());
        }

        usingWriter = true;
        outputBuffer.checkConverter();
        if (writer == null) {
            writer = new CoyoteWriter(outputBuffer);
        }
        return writer;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

上面分别把getOutputStream()getWriter()方法截出来了。

初始化的时候usingOutputStream=falseusingWriter = false
但是调用上面两个方法中的一个的时候,会把另一个标志变为true。而标志位变为true,则会抛出异常

结论

在一个方法体里getOutputStreamgetWriter方法,只能使用其中一个,否则会报出最上面的错误信息。

类似的,getOutputStream() has already been called for this response也是这个问题。
同理,getReader()与getInputStream()方法也不能同时使用


尽管上面的原因会导致报错,但是我的方法体里只用到一次getOutputStream方法呀,还是不应该 出现这个报错呀。会不会是controller方法前,有拦截器或过滤器或aop等原因,导致了response对象中usingOutputStream=trueusingWriter = true


通过排查代码,发现真有一个aop前置通知进行请求参数获取并且序列化。

@Before("execution(* com.wojiushiwo..*Controller.*(..))")
    public void putParams(JoinPoint joinPoint) throws JsonProcessingException {

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) (RequestContextHolder.getRequestAttributes());

        //获取参数值
        Object[] args = joinPoint.getArgs();
        //获取参数名称
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        //参数值和名称是一一对应的
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            if (Objects.nonNull(args[i])) {
                //最后一个参数名称是request 这里过滤掉
                if (args[i] instanceof HttpServletRequest) {
                    continue;
                }
                //过滤掉参数中的BindingResult
                if(args[i] instanceof BindingResult){
                    continue;
                }
               
            }
            map.put(parameterNames[i], args[i]);
        }
        //将参数放到session中
        requestAttributes.setAttribute("params", map, RequestAttributes.SCOPE_SESSION);
        //序列化参数
         ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        objectMapper.writeValueAsString(map);
        //...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

这里获取参数的时候,只过滤了HttpServletRequestBindingResult(参数校验对象),并没有过滤HttpServletResponse,所以HttpServletResponse对象会随着去序列化。
前置通知结束后,才会走到我们的controller中,才会出现上面的问题。


所以改正方式只需要在获取参数的时候,过滤HttpServletResponse即可,如

if(args[i] instanceof HttpServletResponse){
                    continue;
 }
  • 1
  • 2
  • 3
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/66336
推荐阅读
相关标签
  

闽ICP备14008679号