问题描述
项目中采用spring aop进行日志记录,在切面类通知方法中编写日志逻辑时,需要获取 HttpSevletRequest
中的请求参数;对于普通参数来说,没有任何问题,但是当请求方式为 POST/PUT
并并且是 @RequestBody
标记的请求,在获取JSON参数时,会出现 java.io.IOException: Stream closed
异常;
原因
HttpServletReqeust获取输入流时仅允许读取一次,spring已经对 @ReqeustBody
提前进行了处理,通过断点调试发现,aop代码中获取 request.getInputStream()
时,输入流已经关闭,因此出现流已经关闭的异常;
异常信息:
java.io.IOException: Stream closed
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:372)
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:190)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.dongzz.cms.common.utils.WebUtil.getParamsJson(WebUtil.java:151)
at com.dongzz.cms.common.annotation.aspect.LogHandlerAdvice.recordSysLogs(LogHandlerAdvice.java:83)
at com.dongzz.cms.common.annotation.aspect.LogHandlerAdvice.handleSysLogs(LogHandlerAdvice.java:44)
解决思路
重新构建 ServletRequest
,读取输入流后进行缓存,然后重写进流里面,使请求输入流支持二次读取;
-
自定义
HttpServletRequestWrapper
,重新构建请求对象;package com.dongzz.cms.common.filter; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets; /** * 重新构建 ServletRequest,读取输入流后进行缓存,然后重写进流里面,使请求输入流支持二次读取 */ public class RequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); body = read(request.getInputStream()).getBytes(StandardCharsets.UTF_8); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return bis.read(); } }; } /** * 读取输入流中的参数,转化为字符串 * * @param is * @return */ private String read(InputStream is) { StringBuilder sb = new StringBuilder(); BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); try { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (Exception e) { e.printStackTrace(); } finally { if (null != reader) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return sb.toString(); } }
-
自定义过滤器,用自定义的 ServletRequest 处理
@RequestBody
特殊请求;package com.dongzz.cms.common.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 解决读取 @RequestBody 相关参数时,HttpServletRequest的输入流只能读取一次的问题 * 问题表现: java.io.IOException: Stream closed */ @WebFilter( filterName = "bodyFilter", urlPatterns = "/*" ) public class RequestBodyFilter implements Filter { public static final Logger logger = LoggerFactory.getLogger(RequestBodyFilter.class); @Override public void init(FilterConfig config) throws ServletException { logger.debug("init request boy filter."); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { HttpServletRequest request = (HttpServletRequest) servletRequest; String mehtod = request.getMethod(); String contentType = request.getContentType(); // 特殊处理 POST/PUT请求,忽略上传请求 if ((HttpMethod.POST.name().equals(mehtod) || HttpMethod.PUT.name().equals(mehtod)) && (!MediaType.MULTIPART_FORM_DATA_VALUE.equals(contentType))) { requestWrapper = new RequestWrapper(request); // 重新构建请求,新的对象读取输入流后进行缓存,然后重写进流,因此支持二次读取 } } if (null == requestWrapper) { chain.doFilter(servletRequest, servletResponse); } else { chain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { logger.debug("destroy request body filter."); } }
此时在 aop 通知方法中通过 HttpServletRequest 获取 @RequestBody 参数时能够取得相关的参数值,不会出现流已关闭的异常;