异常统一处理:HttpMessageNotReadableException(Http消息不可读异常)
一、引言
本篇内容是“异常统一处理”系列文章的重要组成部分,主要聚焦于对 HttpMessageNotReadableException 的原理解析与异常处理机制,并给出测试案例。
- 关于 全局异常统一处理 的原理和完整实现逻辑,请参考文章:
《SpringBoot 全局异常统一处理(AOP):@RestControllerAdvice + @ExceptionHandler + @ResponseStatus》
- 本文仅详细解析 HttpMessageNotReadableException 的异常处理;其他类型异常的原理和处理方法,请参阅本文所在专栏内的其他文章。
二、异常原理
HttpMessageNotReadableException 是 Spring Framework(尤其是 Spring Web MVC 和 Spring Boot 应用程序中)处理 HTTP 请求时抛出的一个异常。当 Spring 在尝试将接收到的 HTTP 请求消息体转换为 Java 对象,例如使用 @RequestBody 注解的方法参数时,如果无法正确解析或读取消息内容,就会抛出此异常。
这个异常通常发生在以下几种情况:
-
请求体为空:
- 当一个方法通过 @RequestBody 注解期待接收请求体中的数据,但实际请求中并没有提供任何有效的内容。
-
请求体格式不正确:
- 如果客户端发送的数据格式与控制器期望接收的数据类型(如 JSON、XML 等)不符,或者数据结构不符合对应的 Java 类型映射规则。
- 数据内容本身有语法错误,例如 JSON 格式不完整或包含非法字符。
-
内容类型不匹配:
- 客户端发送的 Content-Type 头部与实际请求体内容类型不符,Spring 无法找到合适的 HttpMessageConverter 来转换数据。
-
类型转换失败:
- 控制器期望将请求体映射到一个特定类型的对象,但在映射过程中发现某个字段值无法转换为目标类型,例如,JSON 中本应是数字的字段却传来了一个字符串,而目标对象的相应属性是 Integer 类型。
-
GET 请求与 @RequestBody 使用:
- 虽然在某些情况下 GET 请求可以携带请求体,但这不是标准做法,大多数浏览器和工具不会这样做。若控制器中对 GET 请求使用了 @RequestBody,且实际请求没有请求体,则会引发此异常。
解决该异常的一般步骤包括:
- 检查并确保客户端发送的请求体内容格式正确无误。
- 确认 Content-Type 头部设置正确,与预期的媒体类型一致。
- 检查目标方法参数上的 @RequestBody 注解是否合理,特别是对于 GET 请求来说,一般应避免使用此注解。
- 校验模型类(Java Bean)的字段类型与请求体中的数据类型是否匹配,并进行必要的转换或调整。
- 可以通过捕获异常并在异常处理器中进行适当的错误处理及反馈给客户端有意义的错误信息。
三、异常处理代码
在Spring Boot应用中,我们可以通过使用@ExceptionHandler注解来捕获并处理HttpMessageNotReadableException异常。
3.1 异常处理示意图
3.2 异常处理核心代码
package com.example.core.advice; import com.example.core.advice.util.UserTipGenerator; import com.example.core.model.Result; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.HandlerMethod; /** * 全局异常处理器 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * Http消息不可读异常。 *
* 报错原因包括(不完全的列举): *
* (1)缺少请求体(RequestBody)异常; *
* (2)无效格式异常。比如:参数为数字,但是前端传递的是字符串且无法解析成数字。 *
* (3)Json解析异常(非法Json格式)。传递的数据不是合法的Json格式。比如:key-value对中的value(值)为String类型,却没有用双引号括起来。 *
* 举例: * (1)缺少请求体(RequestBody)异常。报错: * DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: * Required request body is missing: * public void com.example.web.user.controller.UserController.addUser(com.example.web.model.param.UserAddParam)] */ @ExceptionHandler @ResponseStatus(value = HttpStatus.BAD_REQUEST) public Result handle(HttpMessageNotReadableException e, HandlerMethod handlerMethod) { Throwable rootCause = e.getRootCause(); // 无效格式异常处理。比如:目标格式为数值,输入为非数字的字符串("80.5%"、"8.5.1"、"张三")。 if (rootCause instanceof InvalidFormatException) { String userMessage = UserTipGenerator.getUserMessage((InvalidFormatException) rootCause); String format = "HttpMessageNotReadableException-InvalidFormatException(Http消息不可读异常-无效格式异常):%s"; String errorMessage = String.format(format, e.getMessage()); return Result.fail(userMessage, String.valueOf(HttpStatus.BAD_REQUEST.value()), errorMessage); } String userMessage = "Http消息不可读异常!请稍后重试,或联系业务人员处理。"; String errorMessage = String.format("HttpMessageNotReadableException(Http消息不可读异常):%s", e.getMessage()); return Result.fail(userMessage, String.valueOf(HttpStatus.BAD_REQUEST.value()), errorMessage); } }
上述代码中,当出现HttpMessageNotReadableException异常时,系统将返回一个状态码为400(Bad Request)的结果,并附带具体的错误信息。
3.3 用户提示生成器
package com.example.core.advice.util; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import java.math.BigDecimal; import java.math.BigInteger; /** * 用户提示生成器。 * * @author songguanxun * @since 2023-8-24 */ public class UserTipGenerator { public static String getUserMessage(InvalidFormatException rootCause) { // 目标类型 Class targetType = rootCause.getTargetType(); // 目标类型提示信息 String targetTypeNotification = ""; if (targetType == BigInteger.class || targetType == Integer.class || targetType == Long.class || targetType == Short.class || targetType == Byte.class) { targetTypeNotification = "参数类型应为:整数;"; } else if (targetType == BigDecimal.class || targetType == Double.class || targetType == Float.class) { targetTypeNotification = "参数类型应为:数值;"; } Object value = rootCause.getValue(); return String.format("参数格式错误!%s当前输入参数:[%s]", targetTypeNotification, value); } }
四、测试案例
本文以新增用户接口为例,进行测试。
4.1 测试代码
package com.example.web.user.controller; import com.example.web.model.param.UserAddParam; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @Slf4j @RestController @RequestMapping("users") @Tag(name = "用户管理") public class UserController { @PostMapping @Operation(summary = "新增用户") public void addUser(@Valid @RequestBody UserAddParam param) { log.info("测试,新增用户。param={}", param); } }
package com.example.web.model.param; import com.example.core.constant.RegexConstant; import com.example.core.validation.idcard.IdCard; import com.example.core.validation.phone.strict.MobilePhone; import com.example.core.validation.zipcode.ZipCode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import javax.validation.constraints.*; import java.util.Date; @Data @Schema(name = "新增用户Param") public class UserAddParam { @NotBlank(message = "姓名,不能为空") @Schema(description = "姓名", example = "张三") private String name; @NotEmpty(message = "手机号码,不能为空") @MobilePhone @Schema(description = "手机号码", example = "18612345678", pattern = RegexConstant.MOBILE_PHONE) private String mobilePhone; @Email @Schema(description = "电子邮箱", example = "zhangsan@example.com") private String email; @Schema(description = "开始时间", example = "2023-01-01 01:20:30") private Date beginTime; @Schema(description = "结束时间", example = "2023-01-01 01:20:30") private Date endTime; @ZipCode @Schema(description = "邮政编码", example = "201100", pattern = RegexConstant.ZIP_CODE) private String zipCode; @IdCard @Schema(description = "身份证号码", example = "110101202301024130") private String idCard; @Min(value = 18, message = "年龄必须大于18周岁") @Schema(description = "年龄", example = "18") private Integer age; @Max(value = 100, message = "上限必须小于100") @Schema(description = "上限", example = "100") private Double upperLimit; }
4.2 正确请求示例
4.3 未处理异常时的报错
(1)缺少请求体(RequestBody)异常
请求响应
控制台的错误日志
(2)无效格式异常
请求响应
参数应为整数,但是前端传递的是字符串且无法解析成整数。
参数应为Double,但是前端传递的是字符串且无法解析成数字。
控制台的错误日志
(3)Json解析异常(非法Json格式)
请求响应
控制台的错误日志
4.4 已处理异常时的响应
(1)缺少请求体(RequestBody)异常
(2)无效格式异常
参数应为整数,但是前端传递的是字符串且无法解析成整数。
参数应为Double,但是前端传递的是字符串且无法解析成数字。
(3)Json解析异常(非法Json格式)
传递的数据不是合法的Json格式。比如:key-value对中的value(值)为String类型,却没有用双引号括起来。
-