You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java 8中如何在过滤器读取请求体且不丢失application/x-www-form-urlencoded参数?

解决过滤器读取请求体后application/x-www-form-urlencoded参数丢失的问题

我明白你遇到的困境:在过滤器中读取application/x-www-form-urlencoded类型的请求体后,后续组件没法通过request.getParameter()获取参数了。这其实是因为Servlet容器的机制——这种类型的请求参数是通过读取请求体解析出来的,而请求体的输入流只能被读取一次。你的原始包装类只解决了重复读取请求体的问题,但没处理参数解析的逻辑,所以容器没法再生成参数映射了。

下面是完整的解决方案,核心思路是在请求包装类中预解析请求体为参数映射,并覆盖所有参数获取方法,让后续组件能从我们预存的映射中拿到参数:

第一步:完善RequestWrapper类

这个类不仅要支持重复读取请求体,还要负责解析表单参数并合并URL中的查询参数:

import javax.servlet.ServletInputStream;
import javax.servlet.ReadListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Arrays;

public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;
    private final Map<String, String[]> parameterMap;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 读取并保存请求体
        body = convertInputStreamToString(request.getInputStream());
        // 初始化参数映射容器
        parameterMap = new HashMap<>();

        // 处理application/x-www-form-urlencoded类型的请求体参数
        String contentType = request.getContentType();
        if (contentType != null && contentType.startsWith("application/x-www-form-urlencoded")) {
            parameterMap.putAll(parseFormData(body));
        }
        // 合并URL中的查询参数(比如GET请求的参数或POST请求URL附带的参数)
        parameterMap.putAll(request.getParameterMap());
    }

    // 将输入流转换为字符串的工具方法
    private static String convertInputStreamToString(java.io.InputStream is) throws IOException {
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024 * 50];
        int length;
        while ((length = is.read(buffer)) != -1) {
            result.write(buffer, 0, length);
        }
        return result.toString(StandardCharsets.UTF_8.name());
    }

    // 解析x-www-form-urlencoded格式的请求体为参数映射
    private Map<String, String[]> parseFormData(String formBody) {
        Map<String, String[]> params = new HashMap<>();
        if (formBody == null || formBody.isEmpty()) {
            return params;
        }

        String[] pairs = formBody.split("&");
        for (String pair : pairs) {
            int equalsIndex = pair.indexOf("=");
            String key;
            String value;

            if (equalsIndex == -1) {
                key = URLDecoder.decode(pair, StandardCharsets.UTF_8);
                value = "";
            } else {
                key = URLDecoder.decode(pair.substring(0, equalsIndex), StandardCharsets.UTF_8);
                value = URLDecoder.decode(pair.substring(equalsIndex + 1), StandardCharsets.UTF_8);
            }

            // 处理同一个key对应多个值的情况(比如复选框)
            if (params.containsKey(key)) {
                String[] existingValues = params.get(key);
                String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1);
                newValues[existingValues.length] = value;
                params.put(key, newValues);
            } else {
                params.put(key, new String[]{value});
            }
        }
        return params;
    }

    // 重写getInputStream,支持重复读取请求体
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
        return new ServletInputStream() {
            private int currentIndex = 0;
            private ReadListener readListener;

            @Override
            public boolean isFinished() {
                return currentIndex >= bodyBytes.length;
            }

            @Override
            public boolean isReady() {
                // 因为数据已经预加载到内存,所以始终返回ready
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                this.readListener = listener;
                if (!isFinished()) {
                    try {
                        listener.onDataAvailable();
                    } catch (IOException e) {
                        listener.onError(e);
                    }
                } else {
                    try {
                        listener.onAllDataRead();
                    } catch (IOException e) {
                        listener.onError(e);
                    }
                }
            }

            @Override
            public int read() throws IOException {
                if (isFinished()) {
                    return -1;
                }
                // 将byte转为无符号int(避免负数导致的解析错误)
                int byteValue = bodyBytes[currentIndex] & 0xFF;
                currentIndex++;
                if (isFinished() && readListener != null) {
                    readListener.onAllDataRead();
                }
                return byteValue;
            }
        };
    }

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

    // 重写所有参数获取方法,从预解析的parameterMap中返回值
    @Override
    public String getParameter(String name) {
        String[] values = parameterMap.get(name);
        return values != null && values.length > 0 ? values[0] : null;
    }

    @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());
    }

    // 提供外部获取请求体的方法
    public String getBody() {
        return body;
    }
}

第二步:在过滤器中使用包装类

过滤器中只需将原始请求包装成我们的RequestWrapper,之后就可以安全读取请求体,同时不影响后续组件获取参数:

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

@WebFilter(urlPatterns = "/*")
public class RequestBodyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化逻辑(如果需要)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 包装请求
        RequestWrapper requestWrapper = new RequestWrapper(httpRequest);

        // 在这里可以自由读取请求体,比如:
        String requestBody = requestWrapper.getBody();
        // 执行你的业务逻辑...

        // 将包装后的请求传递给后续的Servlet或过滤器
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {
        // 销毁逻辑(如果需要)
    }
}

关键说明

  1. 参数解析与合并:我们在构造函数中先解析请求体的表单参数,再合并URL中的查询参数,确保所有参数都能被正确获取,不管是来自URL还是请求体。
  2. 重写参数方法:通过覆盖getParameter系列方法,让后续组件直接从我们预存的参数映射中取值,不再依赖容器读取请求体解析参数。
  3. 输入流修复:修正了ServletInputStreamread()方法的返回值(转为无符号int),避免二进制数据解析错误;同时完善了ReadListener的处理,符合Servlet 3.1规范。
  4. 编码一致性:全程使用UTF-8编码,避免请求体和参数出现乱码问题。

这样改造后,你既可以在过滤器中读取请求体,后续的Servlet或其他组件调用request.getParameter()也能正常获取application/x-www-form-urlencoded类型的参数了。

内容的提问来源于stack exchange,提问作者Reza A

火山引擎 最新活动