最近上线一个版本之后,发现微信支付回调那里老是报如下的异常
熟悉支付的同学应该知道,回调里处理完业务逻辑之后,要给微信/支付宝回调成功的响应
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;
}