当前位置: 首页 > news >正文

Spring MVC参数解析:深入剖析415异常与@RequestBody处理机制问题场景

问题场景

在日志中我们看到以下关键错误信息:

text

Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported

java

public String contentTypeJson(DeviceInfo)

这个415错误发生在Spring尝试将application/x-www-form-urlencoded类型请求绑定到@RequestBody注解的参数时,这是典型的Content-Type不匹配问题。

核心源码解析

1. 参数解析入口

java

// InvocableHandlerMethod.java
protected Object[] getMethodArgumentValues(...) {for (int i = 0; i < parameters.length; i++) {if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException("No suitable resolver");}args[i] = this.resolvers.resolveArgument(...);}
}
2. 参数解析器选择

java

// HandlerMethodArgumentResolverComposite.java
public boolean supportsParameter(MethodParameter parameter) {return getArgumentResolver(parameter) != null;
}private HandlerMethodArgumentResolver getArgumentResolver(...) {for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {if (resolver.supportsParameter(parameter)) {return resolver; // 找到@RequestBody对应的RequestResponseBodyMethodProcessor}}return null;
}
3. @RequestBody处理核心

java

// RequestResponseBodyMethodProcessor.java
public Object resolveArgument(...) throws Exception {Object arg = readWithMessageConverters(...);// 数据绑定和校验...
}protected <T> Object readWithMessageConverters(...) {// 关键检测点:Content-Type支持检查for (HttpMessageConverter<?> converter : this.messageConverters) {if (converter.canRead(targetClass, contentType)) { // 这里检测失败!body = converter.read(...);break;}}if (body == NO_VALUE) {// 抛出415异常的根源throw new HttpMediaTypeNotSupportedException(...);}return body;
}
4. JSON转换器检测逻辑

java

// AbstractJackson2HttpMessageConverter.java
public boolean canRead(Class<?> clazz, MediaType mediaType) {if (!canRead(mediaType)) { // 检查Content-Type是否支持return false;}return this.objectMapper.canDeserialize(javaType);
}

问题根源分析

  1. Content-Type不匹配

    • 请求头:application/x-www-form-urlencoded

    • 要求类型:application/json

  2. 转换器匹配失败

    java

    // MappingJackson2HttpMessageConverter支持的媒体类型
    public MappingJackson2HttpMessageConverter() {super(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8);
    }
  3. 异常传播路径
    canRead()返回false → readWithMessageConverters()检测不到转换器 → 抛出HttpMediaTypeNotSupportedException → 415响应

解决方案

方案1:前端修改Content-Type(推荐)

javascript

// 正确设置请求头
fetch('/endpoint', {method: 'POST',headers: {'Content-Type': 'application/json' // 必须设置为JSON},body: JSON.stringify({ ... })
})
方案2:后端接收方式调整

java

// 移除@RequestBody,使用表单接收
public String contentTypeJson(DeviceInfo deviceInfo) { ... }// 或使用MultiValueMap接收
public String handleForm(@RequestParam MultiValueMap<String, String> formData) {DeviceInfo device = new DeviceInfo();device.setName(formData.getFirst("name"));...
}
方案3:自定义消息转换器(高级)

java

@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {// 扩展支持x-www-form-urlencodedconverters.add(new FormDataConverter());}static class FormDataConverter extends AbstractHttpMessageConverter<Object> {public FormDataConverter() {super(MediaType.APPLICATION_FORM_URLENCODED);}@Overrideprotected boolean supports(Class<?> clazz) {return true; // 支持所有类型}@Overrideprotected Object readInternal(...) {// 实现表单到对象的转换逻辑}}
}

最佳实践建议

  1. 前后端协作规范

    • 明确API文档中要求的Content-Type

    • 使用Postman等工具预先测试

  2. 防御性编码

    java

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> handleJson(@Valid @RequestBody DeviceInfo dto) {// ...
    }
  3. 异常处理增强

    java

    @ControllerAdvice
    public class ApiExceptionHandler {@ExceptionHandler(HttpMediaTypeNotSupportedException.class)public ResponseEntity<ErrorResponse> handle415() {return ResponseEntity.status(415).body(new ErrorResponse("请使用JSON格式提交数据"));}
    }

总结思考

  1. 设计一致性原则

    • @RequestBody应严格对应JSON/XML等结构化数据

    • 表单数据应使用@ModelAttribute@RequestParam

  2. 框架设计启示

    DispatcherServlet

    HandlerMapping

    HandlerAdapter

    ArgumentResolvers

    MessageConverters

    Content-Type匹配?

    成功转换

    415异常

415错误本质是HTTP语义化错误码的典型应用:客户端请求携带了服务器无法理解的表述形式。理解Spring的参数解析流程,能帮助我们在类似问题中快速定位核心冲突点。

 ##源码

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);setResponseStatus(webRequest);if (returnValue == null) {if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {disableContentCachingIfNecessary(webRequest);mavContainer.setRequestHandled(true);return;}}else if (StringUtils.hasText(getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");try {this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);}catch (Exception ex) {if (logger.isTraceEnabled()) {logger.trace(formatErrorForReturnValue(returnValue), ex);}throw ex;}}	protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if (this.returnValueHandlers != null) {invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}invocableMethod.setDataBinderFactory(binderFactory);invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);ModelAndViewContainer mavContainer = new ModelAndViewContainer();mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));modelFactory.initModel(webRequest, mavContainer, invocableMethod);mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);asyncWebRequest.setTimeout(this.asyncRequestTimeout);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.setTaskExecutor(this.taskExecutor);asyncManager.setAsyncWebRequest(asyncWebRequest);asyncManager.registerCallableInterceptors(this.callableInterceptors);asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);if (asyncManager.hasConcurrentResult()) {Object result = asyncManager.getConcurrentResult();mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];asyncManager.clearConcurrentResult();LogFormatUtils.traceDebug(logger, traceOn -> {String formatted = LogFormatUtils.formatValue(result, !traceOn);return "Resume with async result [" + formatted + "]";});invocableMethod = invocableMethod.wrapConcurrentResult(result);}invocableMethod.invokeAndHandle(webRequest, mavContainer);if (asyncManager.isConcurrentHandlingStarted()) {return null;}return getModelAndView(mavContainer, modelFactory, webRequest);}finally {webRequest.requestCompleted();}}protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {MethodParameter[] parameters = getMethodParameters();if (ObjectUtils.isEmpty(parameters)) {return EMPTY_ARGS;}Object[] args = new Object[parameters.length];for (int i = 0; i < parameters.length; i++) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = findProvidedArgument(parameter, providedArgs);if (args[i] != null) {continue;}if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}catch (Exception ex) {// Leave stack trace for later, exception may actually be resolved and handled...if (logger.isDebugEnabled()) {String exMsg = ex.getMessage();if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {logger.debug(formatArgumentError(parameter, exMsg));}}throw ex;}}return args;}@Overridepublic boolean supportsParameter(MethodParameter parameter) {return getArgumentResolver(parameter) != null;}/*** Iterate over registered* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}* and invoke the one that supports it.* @throws IllegalArgumentException if no suitable argument resolver is found*/@Override@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);if (resolver == null) {throw new IllegalArgumentException("Unsupported parameter type [" +parameter.getParameterType().getName() + "]. supportsParameter should be called first.");}return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}/*** Find a registered {@link HandlerMethodArgumentResolver} that supports* the given method parameter.*/@Nullableprivate HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);if (result == null) {for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {if (resolver.supportsParameter(parameter)) {result = resolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;}public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));}@Overridepublic boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {if (!canRead(mediaType)) {return false;}JavaType javaType = getJavaType(type, contextClass);AtomicReference<Throwable> causeRef = new AtomicReference<>();if (this.objectMapper.canDeserialize(javaType, causeRef)) {return true;}logWarningIfNecessary(javaType, causeRef.get());return false;}@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {parameter = parameter.nestedIfOptional();Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());String name = Conventions.getVariableNameForParameter(parameter);if (binderFactory != null) {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());}}if (mavContainer != null) {mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());}}return adaptArgumentIfNecessary(arg, parameter);}@Overrideprotected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);Assert.state(servletRequest != null, "No HttpServletRequest");ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);Object arg = readWithMessageConverters(inputMessage, parameter, paramType);if (arg == null && checkRequired(parameter)) {throw new HttpMessageNotReadableException("Required request body is missing: " +parameter.getExecutable().toGenericString(), inputMessage);}return arg;}	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {MediaType contentType;boolean noContentType = false;try {contentType = inputMessage.getHeaders().getContentType();}catch (InvalidMediaTypeException ex) {throw new HttpMediaTypeNotSupportedException(ex.getMessage());}if (contentType == null) {noContentType = true;contentType = MediaType.APPLICATION_OCTET_STREAM;}Class<?> contextClass = parameter.getContainingClass();Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);if (targetClass == null) {ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);targetClass = (Class<T>) resolvableType.resolve();}HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);Object body = NO_VALUE;EmptyBodyCheckingHttpInputMessage message;try {message = new EmptyBodyCheckingHttpInputMessage(inputMessage);for (HttpMessageConverter<?> converter : this.messageConverters) {Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();GenericHttpMessageConverter<?> genericConverter =(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :(targetClass != null && converter.canRead(targetClass, contentType))) {if (message.hasBody()) {HttpInputMessage msgToUse =getAdvice().beforeBodyRead(message, parameter, targetType, converterType);body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);}else {body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);}break;}}}catch (IOException ex) {throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);}if (body == NO_VALUE) {if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||(noContentType && !message.hasBody())) {return null;}throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);}MediaType selectedContentType = contentType;Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn -> {String formatted = LogFormatUtils.formatValue(theBody, !traceOn);return "Read \"" + selectedContentType + "\" to [" + formatted + "]";});return body;}@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);if (handler == null) {throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());}handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);}@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {mavContainer.setRequestHandled(true);if (returnValue == null) {return;}ServletServerHttpRequest inputMessage = createInputMessage(webRequest);ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);Assert.isInstanceOf(HttpEntity.class, returnValue);HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;HttpHeaders outputHeaders = outputMessage.getHeaders();HttpHeaders entityHeaders = responseEntity.getHeaders();if (!entityHeaders.isEmpty()) {entityHeaders.forEach((key, value) -> {if (HttpHeaders.VARY.equals(key) && outputHeaders.containsKey(HttpHeaders.VARY)) {List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders);if (!values.isEmpty()) {outputHeaders.setVary(values);}}else {outputHeaders.put(key, value);}});}if (responseEntity instanceof ResponseEntity) {int returnStatus = ((ResponseEntity<?>) responseEntity).getStatusCodeValue();outputMessage.getServletResponse().setStatus(returnStatus);if (returnStatus == 200) {HttpMethod method = inputMessage.getMethod();if ((HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method))&& isResourceNotModified(inputMessage, outputMessage)) {outputMessage.flush();return;}}else if (returnStatus / 100 == 3) {String location = outputHeaders.getFirst("location");if (location != null) {saveFlashAttributes(mavContainer, webRequest, location);}}}// Try even with null body. ResponseBodyAdvice could get involved.writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);// Ensure headers are flushed even if no body was written.outputMessage.flush();}

相关文章:

  • 创客匠人:创始人 IP 打造引领知识变现新路径​
  • 【HarmonyOS NEXT】跳转到华为应用市场进行应用下载并更新
  • Cesium快速入门到精通系列教程十一:Cesium1.74中高性能渲染上万Polyline
  • TDengine 如何打破工业实时数据库势力边界?
  • Redis高级数据结构深度解析:BitMap、布隆过滤器、HyperLogLog与Geo应用实践
  • 某音Web端消息体ProtoBuf结构解析
  • 【网络安全】网络安全中的离散数学
  • BUUCTF在线评测-练习场-WebCTF习题[BJDCTF2020]Easy MD51-flag获取、解析
  • 第九节:Vben Admin 最新 v5.0 (vben5) 快速入门 - 菜单管理(上)
  • 笔记07:网表的输出与导入
  • 家政维修平台实战30:处理售后
  • ABP VNext + 多数据库混合:SQL Server+PostgreSQL+MySQL
  • 开疆智能ModbusTCP转CClinkIE网关连接台达DVP-ES3 PLC配置案例
  • 嵌入式硬件与应用篇---寄存器GPIO控制
  • 【音视频】H.264详细介绍及测试代码
  • 电子电气架构 --- 车辆产品的生产周期和研发周
  • 深入解析 Electron 架构:主进程 vs 渲染进程
  • Blender速成班-知识补充
  • Opencv计算机视觉PPT-算法篇
  • 在项目中如何巧妙使用缓存