Spring Controller如何分别获取查询参数与URL编码请求体
处理HTML注册表单的POST控制器,表单以application/x-www-form-urlencoded格式提交,包含username、password等字段;同时请求URI中带有client_id、redirect_uri及用于预填输入框的username查询参数。
使用UserSignUpFormModel(POJO)绑定请求体时,若POST请求同时存在查询参数username=bar和请求体username=foo,调用user.getUsername()会返回foo,bar(两者被逗号拼接),但实际需要仅获取请求体中的foo,并单独获取查询参数的bar。
控制器代码:
@PostController public String performSignUp(UserSignUpFormModel user) { // username = "foo,bar" 但期望仅为请求体中的"foo" String username = user.getUsername(); // ...业务逻辑 }
模型代码:
@Data @Builder @NoArgsConstructor @AllArgsConstructor @ToString @JsonIgnoreProperties(ignoreUnknown = true) public class UserSignUpFormModel { private String email; private String username; private String password; private String passwordConfirmation; private Boolean hasAcceptedTerms; }
Spring MVC中,控制器方法的POJO参数默认会被当作@ModelAttribute处理,它会从所有请求参数源(查询参数、表单参数、路径变量等)收集同名参数,当多个源存在同名参数时,会自动将它们用逗号拼接成字符串赋值给POJO字段。
方案1:分别绑定查询参数与请求体(推荐)
通过@RequestParam单独获取查询参数中的username,同时用@RequestBody绑定请求体到POJO(需指定请求类型为application/x-www-form-urlencoded),这样POJO只会读取请求体的内容,避免参数合并。
修改后的控制器代码:
import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @PostController public class SignUpController { @PostMapping(value = "/sign-up", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public String performSignUp( // 获取查询参数中的username(非必填,因为可能不存在) @RequestParam(value = "username", required = false) String queryUsername, // 仅绑定请求体中的表单参数到POJO @RequestBody UserSignUpFormModel user) { String formUsername = user.getUsername(); // 来自请求体的"foo" String prefilledUsername = queryUsername; // 来自查询参数的"bar"(若存在) // ...业务逻辑处理 return "redirect:/success"; } }
方案2:手动解析请求体参数
若不想使用@RequestBody,可通过HttpServletRequest直接读取并解析请求体的表单参数,同时用@RequestParam获取查询参数:
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; import org.springframework.beans.BeanUtils; @PostController public class SignUpController { @PostMapping("/sign-up") public String performSignUp( @RequestParam(value = "username", required = false) String queryUsername, HttpServletRequest request) throws IOException { // 读取并解析请求体的表单参数 String requestBody = request.getReader().lines().collect(Collectors.joining()); Map<String, String> formParams = Arrays.stream(requestBody.split("&")) .map(param -> param.split("=")) .collect(Collectors.toMap( arr -> URLDecoder.decode(arr[0], StandardCharsets.UTF_8), arr -> arr.length > 1 ? URLDecoder.decode(arr[1], StandardCharsets.UTF_8) : "" )); // 将表单参数映射到POJO UserSignUpFormModel user = new UserSignUpFormModel(); BeanUtils.populate(user, formParams); String formUsername = user.getUsername(); // 请求体中的"foo" // ...业务逻辑处理 return "redirect:/success"; } }
方案3:自定义参数绑定规则(复杂场景)
通过@InitBinder自定义WebDataBinder,限制参数仅从请求体中获取。这种方式需要自定义绑定逻辑,适合复杂的参数处理场景:
import org.springframework.web.bind.InitBinder; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.PostController; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinder; @PostController public class SignUpController { @InitBinder public void initBinder(WebDataBinder binder) { if (binder instanceof ServletRequestDataBinder servletBinder) { // 自定义参数获取逻辑,仅从请求体读取表单参数,忽略查询参数 servletBinder.setServletRequestParameterNameFilter(paramName -> { // 可根据需求过滤参数来源,实现逻辑需结合HttpServletRequest区分参数源 // 注:该方式需深入Spring绑定逻辑,推荐优先使用方案1 return true; }); } } @PostMapping("/sign-up") public String performSignUp( @RequestParam(value = "username", required = false) String queryUsername, UserSignUpFormModel user) { String formUsername = user.getUsername(); // ...业务逻辑处理 return "redirect:/success"; } }
推荐使用方案1,它利用Spring原生注解机制,代码简洁且易于维护,能直接区分查询参数与请求体参数,避免自动合并的问题。
内容的提问来源于stack exchange,提问作者Maxime Esnol

