HandlerMethodArgumentResolver参数绑定:@RequestBody是怎么工作的
HandlerMethodArgumentResolver参数绑定:@RequestBody是怎么工作的
适读人群:Spring MVC使用者,想深入理解参数绑定机制的Java开发者 | 阅读时长:约17分钟
开篇故事
有一次做接口文档评审,一个新来的同学问我:老张,@RequestBody和@RequestParam的底层是同一套逻辑吗?
我一时语塞。我知道结果不一样,但要我说清楚底层原理,还真说不清楚。
那之后我专门把HandlerMethodArgumentResolver的源码翻了一遍。原来Spring MVC用了一个非常优雅的设计:定义了HandlerMethodArgumentResolver接口,所有参数解析逻辑都实现这个接口,然后在处理请求时遍历所有Resolver,找到第一个能处理当前参数的就调用它。
@RequestBody和@RequestParam是完全独立的两个Resolver,处理逻辑大相径庭:前者读取请求体(InputStream)然后用HttpMessageConverter反序列化;后者从请求参数Map里取值然后做类型转换。
搞清楚这些,以后碰到参数绑定问题,你能精准定位到是哪个Resolver出了问题,而不是一脸懵逼。
一、HandlerMethodArgumentResolver接口设计
// spring-webmvc/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java
public interface HandlerMethodArgumentResolver {
// 判断此Resolver能否处理给定的参数
boolean supportsParameter(MethodParameter parameter);
// 解析参数值
@Nullable
Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception;
}两个方法:supportsParameter负责判断,resolveArgument负责执行。这是一个典型的策略模式。
Spring MVC内置了超过30个HandlerMethodArgumentResolver实现,覆盖了几乎所有参数注解场景。
二、源码核心路径解析
2.1 参数解析流程
2.2 @RequestBody解析器:RequestResponseBodyMethodProcessor
这是处理@RequestBody的核心类,同时也处理@ResponseBody返回值。
// RequestResponseBodyMethodProcessor.java 第88行
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
// 第105行
@Override
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 1. 用HttpMessageConverter读取请求体并反序列化
Object arg = readWithMessageConverters(webRequest, parameter,
parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
// 2. 触发@Validated校验
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 将BindingResult放入Model
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name,
binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}2.3 HttpMessageConverter的选择逻辑
// AbstractMessageConverterMethodArgumentResolver.java 第147行(简化)
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType;
// 读取Content-Type
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
// 遍历所有MessageConverter,找第一个能读的
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType =
(Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
if (genericConverter != null) {
if (genericConverter.canRead(targetType, contextClass, contentType)) {
// 找到了,调用read
return genericConverter.read(targetType, contextClass, msgToUse);
}
} else if (targetClass != null && converter.canRead(targetClass, contentType)) {
return ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse);
}
}
throw new HttpMediaTypeNotSupportedException(contentType,
getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}关键点:是Content-Type决定用哪个MessageConverter,而不是参数类型。
2.4 常用Resolver一览
2.5 @RequestParam的解析逻辑(对比)
// RequestParamMethodArgumentResolver.java
@Override
protected Object resolveName(String name, MethodParameter parameter,
NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
// 从请求参数Map取值
String[] paramValues = null;
if (servletRequest != null) {
String[] mpValues = MultipartResolutionDelegate
.resolveMultipartArgument(name, parameter, servletRequest);
if (mpValues != MultipartResolutionDelegate.UNRESOLVABLE) {
paramValues = mpValues;
} else {
paramValues = servletRequest.getParameterValues(name);
}
}
// 单值 vs 数组
if (paramValues != null) {
return (paramValues.length == 1 ? paramValues[0] : paramValues);
}
return null;
}和@RequestBody完全不同:不读InputStream,直接从request.getParameterValues()取字符串,再通过ConversionService做类型转换。
三、完整代码示例
3.1 自定义ArgumentResolver:从请求头解析JWT用户信息
// 定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
boolean required() default true;
}
// 用户信息载体
public record LoginUser(Long userId, String username, List<String> roles) {}
// 自定义ArgumentResolver
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private JwtTokenService jwtTokenService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 同时满足:有@CurrentUser注解 + 参数类型是LoginUser
return parameter.hasParameterAnnotation(CurrentUser.class)
&& LoginUser.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
assert request != null;
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
CurrentUser annotation = parameter.getParameterAnnotation(CurrentUser.class);
if (annotation != null && annotation.required()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Missing Authorization header");
}
return null;
}
String token = authHeader.substring(7);
try {
return jwtTokenService.parseToken(token);
} catch (JwtException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token");
}
}
}
// 注册到MVC配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private CurrentUserArgumentResolver currentUserResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserResolver);
}
}
// 在Controller中使用
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/profile")
public UserProfileDTO getProfile(@CurrentUser LoginUser user) {
// 直接注入,不需要手动解析token
return userService.getProfile(user.userId());
}
@GetMapping("/orders")
public List<OrderDTO> getOrders(@CurrentUser(required = false) LoginUser user) {
if (user == null) {
return Collections.emptyList(); // 未登录返回空
}
return orderService.getUserOrders(user.userId());
}
}3.2 自定义MessageConverter:支持CSV格式请求体
// 自定义CSV MessageConverter
public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<Map<String, String>>> {
private static final MediaType CSV_MEDIA_TYPE =
new MediaType("text", "csv");
public CsvHttpMessageConverter() {
super(StandardCharsets.UTF_8, CSV_MEDIA_TYPE);
}
@Override
protected boolean supports(Class<?> clazz) {
return List.class.isAssignableFrom(clazz);
}
@Override
protected List<Map<String, String>> readInternal(
Class<? extends List<Map<String, String>>> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
List<Map<String, String>> result = new ArrayList<>();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputMessage.getBody(), StandardCharsets.UTF_8));
String headerLine = reader.readLine();
if (headerLine == null) return result;
String[] headers = headerLine.split(",");
String line;
while ((line = reader.readLine()) != null) {
String[] values = line.split(",", -1);
Map<String, String> row = new LinkedHashMap<>();
for (int i = 0; i < headers.length; i++) {
row.put(headers[i].trim(), i < values.length ? values[i].trim() : "");
}
result.add(row);
}
return result;
}
@Override
protected void writeInternal(List<Map<String, String>> data,
HttpOutputMessage outputMessage) throws IOException {
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(outputMessage.getBody(), StandardCharsets.UTF_8));
if (!data.isEmpty()) {
writer.println(String.join(",", data.get(0).keySet()));
for (Map<String, String> row : data) {
writer.println(String.join(",", row.values()));
}
}
writer.flush();
}
}
// 注册到WebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new CsvHttpMessageConverter()); // 加到最前面,优先级最高
}
}
// Controller中使用(注意Content-Type要是 text/csv)
@PostMapping(value = "/import", consumes = "text/csv")
public ImportResult importData(@RequestBody List<Map<String, String>> csvData) {
return importService.process(csvData);
}四、踩坑实录
坑1:自定义ArgumentResolver加到列表末尾却不生效
现象:addArgumentResolvers加了自定义Resolver,但方法参数没被解析,还是NPE。
根因:addArgumentResolvers只是追加到列表末尾,但Spring内置的RequestParamMethodArgumentResolver有一个"兜底"模式(useDefaultResolution=true),会处理所有没有注解的简单类型参数。如果你的自定义参数类型被它先匹配了,就轮不到你了。
解决方案:不要用addArgumentResolvers,用WebMvcConfigurationSupport的getArgumentResolvers()方法完全控制列表,或者让自定义参数类型加上注解来避免被兜底Resolver误匹配。
坑2:@RequestBody读取一次后InputStream关闭
现象:在Filter里读了一遍请求体做签名验证,到达Controller后@RequestBody报HttpMessageNotReadableException。
根因:HttpServletRequest的InputStream只能读一次,一旦读完就关闭了。
解决方案:用Spring提供的ContentCachingRequestWrapper包装请求,它会缓存请求体:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 包装请求,允许多次读取body
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
// 把响应体复制回真实响应流
wrappedResponse.copyBodyToResponse();
}
}坑3:@Validated在@RequestBody上不生效
现象:加了@Validated注解,@NotNull校验没有触发。
根因:@RequestBody的校验是在RequestResponseBodyMethodProcessor.resolveArgument里通过validateIfApplicable触发的,它检查的是@Validated或@Valid注解。但如果你的Validator没有注册,或者ConstraintValidator没被Spring管理(比如用了@Autowired但没加@Component),校验器不会被调用。
// 确保约束注解的验证器被Spring管理
@Component // 必须有这个!
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
return value.matches("^1[3-9]\\d{9}$");
}
}坑4:多个参数共用同一个类型导致解析歧义
现象:两个@RequestBody String参数,第二个解析失败。
根因:HTTP请求体只有一个,只能解析一次。一个方法里有多个@RequestBody参数在Spring MVC里是不支持的(会抛IllegalStateException)。解决方案是把多个参数合并成一个包装对象。
五、总结与延伸
HandlerMethodArgumentResolver的设计让Spring MVC的参数绑定非常灵活,你可以通过实现这个接口添加任意类型的参数解析逻辑,而不需要修改框架代码。
核心链路:InvocableHandlerMethod → HandlerMethodArgumentResolverComposite → 具体Resolver → (对于@RequestBody)HttpMessageConverter。
理解这条链路,以后遇到参数绑定问题,三步定位:
- 确认哪个Resolver在处理你的参数(打断点在
getArgumentResolver) - 确认Resolver的
resolveArgument逻辑 - 如果是
@RequestBody,进一步确认是哪个MessageConverter在工作
下一篇我们聊@Configuration的CGLIB代理,这是很多人知其然不知其所以然的一个点。
