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

    }

 

posted @ 2021-02-23 14:40  意犹未尽  阅读(489)  评论(0编辑  收藏  举报