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() { // 销毁逻辑(如果需要) } }
关键说明
- 参数解析与合并:我们在构造函数中先解析请求体的表单参数,再合并URL中的查询参数,确保所有参数都能被正确获取,不管是来自URL还是请求体。
- 重写参数方法:通过覆盖
getParameter系列方法,让后续组件直接从我们预存的参数映射中取值,不再依赖容器读取请求体解析参数。 - 输入流修复:修正了
ServletInputStream中read()方法的返回值(转为无符号int),避免二进制数据解析错误;同时完善了ReadListener的处理,符合Servlet 3.1规范。 - 编码一致性:全程使用UTF-8编码,避免请求体和参数出现乱码问题。
这样改造后,你既可以在过滤器中读取请求体,后续的Servlet或其他组件调用request.getParameter()也能正常获取application/x-www-form-urlencoded类型的参数了。
内容的提问来源于stack exchange,提问作者Reza A




