天天看点

「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)
           

我的回调的代码如下:

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;
    }
           

很奇怪,为什么会报出

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;
    }
           

上面分别把

getOutputStream()

getWriter()

方法截出来了。

初始化的时候

usingOutputStream=false

usingWriter = false

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

结论

在一个方法体里

getOutputStream

getWriter

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

类似的,getOutputStream() has already been called for this response也是这个问题。

同理,getReader()与getInputStream()方法也不能同时使用

尽管上面的原因会导致报错,但是我的方法体里只用到一次

getOutputStream

方法呀,还是不应该 出现这个报错呀。会不会是controller方法前,有拦截器或过滤器或aop等原因,导致了response对象中

usingOutputStream=true

usingWriter = 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);
        //...
           

这里获取参数的时候,只过滤了

HttpServletRequest

BindingResult

(参数校验对象),并没有过滤

HttpServletResponse

,所以

HttpServletResponse

对象会随着去序列化。

前置通知结束后,才会走到我们的controller中,才会出现上面的问题。

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

HttpServletResponse

即可,如

if(args[i] instanceof HttpServletResponse){
                    continue;
 }
           

继续阅读