springMVC源码阅读-解决body不能重复读取问题(十二)
方式一
这种方式线上高并发看链路追踪发现new RequestWrapper耗时3秒 后来改为第二种方式
装饰requset
@Component @WebFilter(filterName = "wrapperFilter", urlPatterns = {"/**"}) public class WrapperFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request); chain.doFilter(requestWrapper, response); } }
注://会导致无法绑定 application/x-www-form-urlencoded 报文体内容。待研究。tomcat9有问题
package cn.wine.ms.promotion.configuartion; import lombok.extern.slf4j.Slf4j; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.util.Objects; /** * @author lau * 解决因为ServletInputStream 读取body流 读取后指针不能还原其他地方不能读取多次 */ @Slf4j public class RequestWrapper extends HttpServletRequestWrapper { private byte[] body; private Reader reader; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); try { StringBuffer stringBuffer = new StringBuffer(); request.getReader().lines().forEach(stringBuffer::append); body = stringBuffer.toString().getBytes(); } catch (IllegalStateException e) { body = new byte[]{}; log.error("{}接口body为空或丢失",request.getRequestURI()); //部分接口,文件上传,body为空,会报IllegalStateException } } public RequestWrapper(HttpServletRequest request, Reader reader) { super(request); this.reader = reader; StringBuffer stringBuffer = new StringBuffer(); new BufferedReader(reader).lines().forEach(stringBuffer::append); body = stringBuffer.toString().getBytes(); } @Override public BufferedReader getReader() throws IOException { if (Objects.isNull(reader)){ return new BufferedReader(new InputStreamReader(getInputStream())); }else { return new BufferedReader(reader); } } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { } @Override public int read() throws IOException { return bais.read(); } }; } }
问题原因
在处理 application/x-www-form-urlencoded 类型的请求时可能会有问题,因为 HttpServletRequest 的 getParameter 系列方法会尝试解析请求体,而这些方法通常只在第一次调用时读取请求体。
/** * @author qiang.li 解决因为ServletInputStream 读取body流 读取后指针不能还原其他地方不能读取多次 */ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.apache.commons.lang.StringUtils; public class MultiReadRequestWrapper extends HttpServletRequestWrapper { private byte[] body; private Map<String, String[]> parameterMap; public byte[] getBody() { return body; } public MultiReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取请求体并存储在内存中 body = toByteArray(request.getInputStream()); // 解析请求体并缓存参数 parseParameters(); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished() { return byteArrayInputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public String getParameter(String name) { String[] values = parameterMap.get(name); if (values == null || values.length == 0) { return null; } return values[0]; } @Override public String[] getParameterValues(String name) { return parameterMap.get(name); } @Override public Map<String, String[]> getParameterMap() { return Collections.unmodifiableMap(parameterMap); } @Override public Enumeration<String> getParameterNames() { return Collections.enumeration(parameterMap.keySet()); } private void parseParameters() { if (parameterMap != null) { return; } parameterMap = new HashMap<>(); String contentType = getContentType(); if (StringUtils.isNotBlank(contentType) && contentType.contains("application/x-www-form-urlencoded")) { try { String bodyString = new String(body, getCharacterEncoding()); parseUrlEncodedParameters(bodyString, parameterMap); } catch (IOException e) { throw new RuntimeException("Failed to parse parameters", e); } } else { // 如果不是 application/x-www-form-urlencoded 类型,使用默认的参数解析 parameterMap.putAll(super.getParameterMap()); } } private void parseUrlEncodedParameters(String bodyString, Map<String, String[]> parameterMap) { String[] pairs = bodyString.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); if (idx > 0) { String key = pair.substring(0, idx); String value = pair.substring(idx + 1); try { value = URLDecoder.decode(value, getCharacterEncoding()); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Failed to decode parameter value", e); } String[] existingValues = parameterMap.get(key); if (existingValues == null) { parameterMap.put(key, new String[]{value}); } else { String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1); newValues[newValues.length - 1] = value; parameterMap.put(key, newValues); } } } } private byte[] toByteArray(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } return out.toByteArray(); } }
方式二
package cn.wine.ms.promotion.configuartion; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.util.CollectionUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; /** * @Project 商品uaa * @PackageName cn.wine.ms.promotion.configuartion * @ClassName RequestMappingHandlerAdapterCusotmer * @Author qiang.li * @Date 2021/2/23 11:34 上午 * @Description 用于对springRequestMappingHandlerAdapter做一些定制化配置 */ @Configuration public class RequestMappingHandlerAdapterCustomizeConfig implements InitializingBean { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; @Override public void afterPropertiesSet() throws Exception { //兼容判断 防止使用非注解方式 如xml配置 if(requestMappingHandlerAdapter==null){ return; } //定制化配置@RequestBody 入参解析器 replaceRequestResponseBodyMethodProcessor(); } /** * 使用RequestResponseBodyMethodProcessorWrapper 替换默认的使用RequestResponseBodyMethodProcessor * 解决因为Request.getInputStream()只能读取一次 不能多次读取的问题 * 1.在原有的基础上增加将解析结果存入Request attributes 供后续使用 * HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); * Object data = request.getAttribute(RequestResponseBodyMethodProcessorWrapper.resolveArgumentKey); */ public void replaceRequestResponseBodyMethodProcessor(){ //获得解析器处理集合 List<HandlerMethodArgumentResolver> handlerMethodArgumentResolver= requestMappingHandlerAdapter.getArgumentResolvers(); if(!CollectionUtils.isEmpty(handlerMethodArgumentResolver)){ Optional<HandlerMethodArgumentResolver> resolverOptional=handlerMethodArgumentResolver.stream().filter(c->c instanceof RequestResponseBodyMethodProcessor).findAny(); if(resolverOptional.isPresent()){ //因为框架是不可变的 改为可变类型 List<HandlerMethodArgumentResolver> newHandlerMethodArgumentResolvers=new ArrayList<>(handlerMethodArgumentResolver); //替换为RequestResponseBodyMethodProcessorWrapper Collections.replaceAll(newHandlerMethodArgumentResolvers, resolverOptional.get(), new RequestResponseBodyMethodProcessorWrapper(resolverOptional.get())); //替换list requestMappingHandlerAdapter.setArgumentResolvers(newHandlerMethodArgumentResolvers); } } } }
package cn.wine.ms.promotion.configuartion; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; /** * @Project 商品uaa * @PackageName cn.wine.ms.promotion.configuartion * @ClassName RequestResponseBodyMethodProcessorWrapper * @Author qiang.li * @Date 2021/2/23 1:20 下午 * @Description RequestResponseBodyMethodProcessor装饰器,在原有功能上做将结果保存到request attribute的增加 */ public class RequestResponseBodyMethodProcessorWrapper implements HandlerMethodArgumentResolver { public final static String resolveArgumentKey="resolveArgumentObject"; private HandlerMethodArgumentResolver handlerMethodReturnValueHandler; public RequestResponseBodyMethodProcessorWrapper(HandlerMethodArgumentResolver handlerMethodReturnValueHandler){ this.handlerMethodReturnValueHandler=handlerMethodReturnValueHandler; } @Override public boolean supportsParameter(MethodParameter methodParameter) { //委托 return handlerMethodReturnValueHandler.supportsParameter(methodParameter); } @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { //委托 Object argumentObject= handlerMethodReturnValueHandler.resolveArgument(methodParameter,modelAndViewContainer,nativeWebRequest,webDataBinderFactory); //功能增强 将解析结果存入request attribute供后续使用 if(argumentObject!=null){ HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); request.setAttribute(resolveArgumentKey,argumentObject); } return argumentObject; } }
使用 我的场景是异常拦截器打印入参
/** * 获取body * @param httpServletRequest * @return */ public static String readBody(HttpServletRequest httpServletRequest) { try { //request body不能重复读取 处理方案请看cn.wine.ms.promotion.configuartion.RequestMappingHandlerAdapterCustomizeConfig HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); Object data = request.getAttribute(RequestResponseBodyMethodProcessorWrapper.resolveArgumentKey); return JSON.toJSONString(data); }catch (Exception e){ log.error("获取body数据异常:{}",e); return "getError"; } }