利用HttpServletRequestWrapper来支持可重复读取HttpServletRequest中的请求输入流且不影响Controller层的参数获取

HttpServletRequest中的请求输入流不可重复读取的原因就不叙述了,一堆搜索结果随便看,解决方案有2种,1是使用Spring MVC自带的类ContentCachingRequestWrapper,2是自定义请求包装器,方案2请看如下步骤正文:

1. 定义请求包装器,继承于 HttpServletRequestWrapper


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 自定义请求包装器,用于存储请求body数据,以解决请求输入流只能读取一次的问题
 */
@Slf4j
public class MyRequestWrapper extends HttpServletRequestWrapper {

    public static final Charset defaultCharset = StandardCharsets.UTF_8;

    private final Charset charset;

    private final byte[] bodyBytes;

    private final MultiValueMap<String, String> formParameters;

    public MyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String charEncode = request.getCharacterEncoding();
        String contentType = request.getContentType();
        if (StringUtils.isNotBlank(charEncode)) {
            charset = Charset.forName(charEncode);
        } else if (StringUtils.isNotBlank(contentType)
                && contentType.contains("charset=")) {
            charset = Charset.forName(contentType.substring(contentType.indexOf("=") + 1));
        } else {
            charset = defaultCharset;
        }
        bodyBytes = handleInputStream(request.getInputStream());
        if (StringUtils.isNotBlank(contentType)
                && contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            formParameters = handleFormParameters();
        } else {
            formParameters = new LinkedMultiValueMap(0);
        }
    }

    private byte[] handleInputStream(InputStream inputStream) throws IOException {
        /*BufferedInputStream bis = new BufferedInputStream(inputStream);
        byte[] buffer = new byte[1024];
        int volume;
        byte[] target = new byte[0];
        while ((volume = bis.read(buffer)) != -1) {
            target = ArrayUtils.addAll(target, buffer);
        }
        bis.close();*/
        StringBuffer sb = new StringBuffer();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, charset));
        String line;
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        br.close();
        byte[] target = sb.toString().getBytes(charset);
        return target;
    }

    public String getBody() {
        return new String(bodyBytes, this.charset);
    }

    /**
     * 参照HttpPutFormContentFilter
     */
    private MultiValueMap<String, String> handleFormParameters() throws UnsupportedEncodingException {
        String[] pairs = tokenizeToStringArray(getBody(), "&", true, true);
        MultiValueMap<String, String> result = new LinkedMultiValueMap(pairs.length);
        String[] var8 = pairs;
        int var9 = pairs.length;

        for(int var10 = 0; var10 < var9; ++var10) {
            String pair = var8[var10];
            int idx = pair.indexOf(61);
            if (idx == -1) {
                result.add(URLDecoder.decode(pair, charset.name()), null);
            } else {
                String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
                String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
                result.add(name, value);
            }
        }

        return result;
    }

    private String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
        if (str == null) {
            return new String[0];
        } else {
            StringTokenizer st = new StringTokenizer(str, delimiters);
            ArrayList<String> tokens = new ArrayList<>();

            while(true) {
                String token;
                do {
                    if (!st.hasMoreTokens()) {
                        return tokens.toArray(new String[0]);
                    }

                    token = st.nextToken();
                    if (trimTokens) {
                        token = token.trim();
                    }
                } while(ignoreEmptyTokens && token.length() <= 0);

                tokens.add(token);
            }
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyBytes);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public String getParameter(String name) {
        String queryStringValue = super.getParameter(name);
        String formValue = this.formParameters.getFirst(name);
        return queryStringValue != null ? queryStringValue : formValue;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> result = new LinkedHashMap();
        Enumeration names = this.getParameterNames();

        while(names.hasMoreElements()) {
            String name = (String)names.nextElement();
            result.put(name, this.getParameterValues(name));
        }

        return result;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        Set<String> names = new LinkedHashSet();
        names.addAll(Collections.list(super.getParameterNames()));
        names.addAll(this.formParameters.keySet());
        return Collections.enumeration(names);
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] parameterValues = super.getParameterValues(name);
        List<String> formParam = this.formParameters.get(name);
        if (formParam == null) {
            return parameterValues;
        } else if (parameterValues != null && this.getQueryString() != null) {
            List<String> result = new ArrayList(parameterValues.length + formParam.size());
            result.addAll(Arrays.asList(parameterValues));
            result.addAll(formParam);
            return result.toArray(new String[0]);
        } else {
            return formParam.toArray(new String[0]);
        }
    }

}

2. 定义过滤器,用于将 HttpServletRequest 对象替换成自定义请求包装器 MyRequestWrapper 对象


import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 在过滤器中将 HttpServletRequest 对象替换成自定义请求包装器 MyRequestWrapper 对象
 */
@Slf4j
public class MyRequestWrapperFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.debug("执行HttpServletRequestWrapper包装器替换");
        MyRequestWrapper requestWrapper = new MyRequestWrapper((HttpServletRequest) servletRequest);
        filterChain.doFilter(requestWrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

3. 定义拦截器,提前获取请求body数据


import com.alibaba.fastjson.JSON;
import com.me.config.filter.MyRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@Slf4j
public class AccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, String[]> parameterMap;
        String body;
        /*if (request instanceof MyRequestWrapper) {
            MyRequestWrapper requestWrapper = (MyRequestWrapper) request;
            body = requestWrapper.getBody();
            parameterMap = requestWrapper.getParameterMap();
        } else {
            body = "";
            parameterMap = request.getParameterMap();
        }*/
        MyRequestWrapper requestWrapper = WebUtils.getNativeRequest(request, MyRequestWrapper.class);
        if (requestWrapper != null) {
            body = requestWrapper.getBody();
            parameterMap = requestWrapper.getParameterMap();
        } else {
            body = StringUtils.EMPTY;
            parameterMap = request.getParameterMap();
        }
        log.info("clientURL----->{}", request.getRemoteAddr());
        log.info("requestURL----->{} {}", request.getMethod(), request.getRequestURL().toString());
        log.info("parameterMap----->{}", JSON.toJSONString(parameterMap));
        log.info("requestBody----->{}", body);
        return true;
    }
}

4. 在配置类中注册过滤器与拦截器,使其生效


import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.*;

@Slf4j
@Configuration
public class BaseConfig extends WebMvcConfigurationSupport {

	@Bean
	public MyRequestWrapperFilter getMyRequestWrapperFilter() {
		return new MyRequestWrapperFilter();
	}

	@Bean
	public FilterRegistrationBean someFilterRegistration() {
		FilterRegistrationBean registration = new FilterRegistrationBean();
		registration.setFilter(getMyRequestWrapperFilter());
		registration.addUrlPatterns("/*");
		registration.setName("myInputStreamFilter");
		// 设置过滤器执行优先级,数值越小优先级越高
		// 当前只要保证Filter过滤器在Interceptor拦截器之前执行即可,所以设置为最低优先级
		registration.setOrder(Ordered.LOWEST_PRECEDENCE);
		return registration;
	}

	@Bean
	public AccessInterceptor getAccessInterceptor() {
		return new AccessInterceptor();
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(getAccessInterceptor())
				.addPathPatterns("/**")
				.excludePathPatterns("/static/**");
    }

	/**
	 * 解决跨域问题
	 */
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
				.allowedOrigins("*")
				.allowedHeaders("Content-Type,X-Requested-With,Cookies,Cookie,X-Auth-Token,token,auth,Authorization")
				.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
				.allowCredentials(true)
				.maxAge(3600);
	}
	@Bean
	public CorsFilter corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration corsConfiguration = new CorsConfiguration();
		// 请求常用的三种配置,*代表允许所有,当时你也可以自定义属性(比如header只能带什么,method只能是post方式等等)
		corsConfiguration.addAllowedOrigin("*");
		corsConfiguration.addAllowedHeader("Content-Type,X-Requested-With,Cookies,Cookie,X-Auth-Token,token,auth,Authorization");
		corsConfiguration.addAllowedMethod("GET,POST,PUT,DELETE,OPTIONS");
		corsConfiguration.setAllowCredentials(true);
		corsConfiguration.setMaxAge(3600L);
		source.registerCorsConfiguration("/**", corsConfiguration);
		return new CorsFilter(source);
	}

}

5. 定义Controller类,这里就不贴源码了,与正常写法无差异。

总结:最主要的在于请求包装器 MyRequestWrapper 中对输入流的处理和存储以及重写获取参数的几个方法,再加上过滤器中的请求包装器替换来保证后续输入流和参数的正常可重复获取。

### Spring Boot 中 Filter Chain 参数解析异常的解决方案 在使用 `filterChain.doFilter` 调用时,如果遇到参数解析错误的问题,通常是因为请求体已经被上游的过滤器读取过一次,而 HTTP 请求中的输入流只能被读取一次。一旦输入流被消耗掉,后续的责任链(如 Controller 或其他过滤器)就无法再次访问该数据。 以下是针对这一问题的具体分析和解决方案: #### 1. **问题原因** HTTP 协议规定,请求体的内容是以的形式传输的,这种是一次性的资源。当某个组件(例如自定义过滤器)尝试读取请求体时,会将完全消费掉[^3]。因此,在调用 `filterChain.doFilter(request, response)` 后续环节中,由于请求体已被清空,可能导致参数解析失败或其他逻辑异常。 --- #### 2. **解决方案** ##### 方法一:封装可重复读取Request 对象 可以通过创建一个包装类来实现对原始 `HttpServletRequest` 的增强功能,使其支持多次读取请求体。具体做法如下: ```java import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; public class MultiReadRequestWrapper extends HttpServletRequestWrapper { private byte[] requestBody; public MultiReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 将请求体转换为字节数组以便于重用 InputStream inputStream = request.getInputStream(); this.requestBody = IOUtils.toByteArray(inputStream); // 使用 Apache Commons IO 工具库简化操作 } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.requestBody); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) {} @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } } ``` 上述代码实现了对原生 `HttpServletRequest` 的扩展,允许其请求体能够被多次读取[^4]。 --- ##### 方法二:调整过滤器顺序 某些情况下,可能需要修改核心逻辑,而是通过调整过滤器执行顺序解决问题。例如,确保 JWT 校验等敏感操作优先完成后再进入业务逻辑。可以在配置文件中指定过滤器的加载顺序: ```xml <filter> <filter-name>CustomJwtFilter</filter-name> <filter-class>com.example.CustomJwtFilter</filter-class> </filter> <filter-mapping> <filter-name>CustomJwtFilter</filter-name> <url-pattern>/api/*</url-pattern> <!-- 设置较低的 priority 值 --> <dispatcher>PRIORITY</dispatcher> </filter-mapping> ``` 注意这里的 `<dispatcher>` 属性可以根据实际需求设置为 REQUEST、FORWARD 等同类型的分发方式[^2]。 --- ##### 方法三:避免提前读取请求体 如果是必要场景,则应尽量减少直接读取请求体的行为。对于简单的验证任务(比如路径匹配或者头部信息检查),可以直接基于现有字段完成判断而涉及复杂的数据结构解析过程[^1]。 --- ### 总结 综合来看,“封装可重复读取Request 对象”的方法最为通用灵活,适用于大多数需要频繁访问请求内容的应用场合;而对于特定环境下的优化则可以选择适当调整过滤器注册位置或行为策略。最终目标都是保障整个 Web 应用程顺畅运转的同时满足安全性和功能性要求。 ---
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值