Spring接收application/x-www-form-urlencoded请求如何仅解码key并保留参数顺序
问题原因
Spring默认的FormHttpMessageConverter处理application/x-www-form-urlencoded类型的请求时,若用@RequestBody String直接接收请求体,会先把表单参数解析为MultiValueMap,再将所有键值对重新做URL编码后拼接为字符串返回,这就导致key被强制编码,同时原始value的编码也可能被破坏。
实现方案
方案1:直接读取原始请求体自行解析(兼容性最好,无需额外配置)
直接通过HttpServletRequest读取未被Spring修改的原始请求体,自行拆分解析,仅对key做解码,完全保留value原始内容和参数顺序:
- 修改接口定义,改用
HttpServletRequest作为入参:
@PostMapping(path = "/some-api", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}) public void processWebhook(HttpServletRequest request) throws IOException { // 读取原始请求体内容 String rawBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); // 解析获取参数,顺序和原始请求完全一致 LinkedHashMap<String, String> params = parseRawFormBody(rawBody, StandardCharsets.UTF_8); // 后续业务逻辑 }
- 实现仅解码key的解析方法,用
LinkedHashMap保证参数顺序:
private LinkedHashMap<String, String> parseRawFormBody(String rawBody, Charset charset) { LinkedHashMap<String, String> paramMap = new LinkedHashMap<>(); if (rawBody == null || rawBody.isBlank()) { return paramMap; } // 按参数分隔符&拆分 String[] paramPairs = rawBody.split("&"); for (String pair : paramPairs) { if (pair.isBlank()) { continue; } // 仅拆分第一个=,避免value中包含=时被误拆分 int eqIndex = pair.indexOf('='); if (eqIndex == -1) { // 无value的参数 String decodedKey = URLDecoder.decode(pair, charset); paramMap.put(decodedKey, ""); } else { String encodedKey = pair.substring(0, eqIndex); String rawValue = pair.substring(eqIndex + 1); // 仅对key做URL解码,value保留原始编码 String decodedKey = URLDecoder.decode(encodedKey, charset); paramMap.put(decodedKey, rawValue); } } return paramMap; }
方案2:自定义消息转换器(适配原有@RequestBody写法)
如果不想修改原有接口的入参定义,可以自定义消息转换器,注册到Spring MVC中自动完成解析:
- 自定义
HttpMessageConverter实现类:
public class RawFormKeyDecodeConverter extends AbstractHttpMessageConverter<Map<String, String>> { public RawFormKeyDecodeConverter() { super(MediaType.APPLICATION_FORM_URLENCODED); } @Override protected boolean supports(Class<?> clazz) { return Map.class.isAssignableFrom(clazz); } @Override protected Map<String, String> readInternal(Class<? extends Map<String, String>> clazz, HttpInputMessage inputMessage) throws IOException { String rawBody = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8); // 调用方案1中的parseRawFormBody方法完成解析 return parseRawFormBody(rawBody, StandardCharsets.UTF_8); } @Override protected void writeInternal(Map<String, String> stringStringMap, HttpOutputMessage outputMessage) { throw new UnsupportedOperationException("仅用于接收表单参数,无需实现序列化逻辑"); } }
- 将自定义转换器注册到Spring MVC的转换器列表最前面,确保优先生效:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new RawFormKeyDecodeConverter()); } }
- 接口直接用
LinkedHashMap接收即可:
@PostMapping(path = "/some-api", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}) public void processWebhook(@RequestBody LinkedHashMap<String, String> params) { // 直接使用解析好的参数即可 }
内容的提问来源于stack exchange,提问作者MilindaD
相关产品推荐
相关产品推荐

