目录
一、问题描述
在测试接口的时候遇到以下问题:
后台报错:
2024-08-10T01:53:38.750+08:00 WARN 6344 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'com.haishi.test.model.enums.ItemType'; Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.haishi.test.model.enums.ItemType] for value '1']
controller层执行的代码:
@Operation(summary = "[根据类型]查询配套信息列表")
@GetMapping("list")
public Result<List<FacilityInfo>> listFacility(@RequestParam(required = false) ItemType type) {
LambdaQueryWrapper<FacilityInfo> queryWrapper = new LambdaQueryWrapper();
if (type != null) {
queryWrapper.eq(FacilityInfo::getType, type);
}
List<FacilityInfo> list = facilityInfoService.list(queryWrapper);
return Result.ok(list);
}
二、项目概述
2.1 依赖
常见的SpringBoot+web+mybatis-plus+mysql+lombok+knife4j(测试方便)。
这里尽量使用mybatis-plus而不是mybatis,详细原因之后会解释。
<!-- 继承Spring Boot父项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
</parent>
<dependencies>
<!--包含spring web相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--包含spring test相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2.2 项目思路
首先让我们来看下数据库
可以看到在数据库中,type字段的存储类型是tinyint,用1和2来区分类型(前端接受的数据类型与数据库一致)。数据库设计需要极致地压缩空间,需要采用tinyint,但是在我们编写后端程序的时候容易逻辑紊乱,特别是团队开发的时候。故此在JAVA程序中使用enum类进行区分是一种规范的操作。
对应的实体类FacilityInfo:
@Schema(description = "配套信息表")
@TableName(value = "facility_info")
@Data
public class FacilityInfo extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "配套所属对象类型")
@TableField(value = "type")
private ItemType type;
@Schema(description = "名称")
@TableField(value = "name")
private String name;
@Schema(description = "图标")
@TableField(value = "icon")
private String icon;
}
先看下所有enum类的接口BaseEnum(后续会解释为什么需要这个接口):
public interface BaseEnum {
Integer getCode();
String getName();
}
然后是type字段对应的ItemType枚举类:
public enum ItemType implements BaseEnum {
APARTMENT(1, "公寓"),
ROOM(2, "房间");
private Integer code;
private String name;
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getName() {
return name;
}
ItemType(Integer code, String name) {
this.code = code;
this.name = name;
}
}
即在前端和数据库中的"1"对应后端的ItemType.APARTMENT,"0"则对应ItemType.ROOM。
三、问题分析
从异常名称分析这是一个类型转换失败导致的异常,那么首先让我们从头到尾了解一下数据是如何在前端发送至数据库,或者从数据库发送至前端的过程中进行类型转换的。
从上图中我们可以分析出问题主要可能出在WebDataBinder组件,TypeHandler和HTTPMessageConverter上,接下来让我们一一分析。
3.1 WebDataBinder问题分析
WebDataBinder组件主要的作用是将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。
很容易猜到的是,这里的问题是从数值类型到枚举类型的转换发生异常。
WebDataBinder依赖于Converter进行转换,其中虽然有默认常用转换规则的存在,但很明显在此处发生了异常,后台系统发生的异常也是由它抛出。
默认转换规则中提供了一个String类型到枚举类型的转换规则,根据实例名称到枚举对象实例进行转换,例如可以将一个"APARTMENT"转换为ItemType.APARTMENT,并不适用于当下这种情况,需要自定义Converter做到图中的功能。
3.2 TypeHandler 问题分析
Mybatis中的TypeHandler用于处理Java的实体对象与数据库之间的数据类型转换
与WebDataBinder的默认转换规则相似,不能支持此处场景下的枚举类型和数值类型的转换,需要自定义TypeHandler,否则会发生异常。
3.3 HTTPMessageConverter 问题分析
HTTPMessageConverter组件负责将Controller方法的返回值转换为HTTP响应体中的JSON字符串,或者反之。
默认规则与前两者相同,不支持此种类型转换,需要自定义转换规则。
但与前两者不同的是,由于本项目中HTTPMessageConverter组件只负责将方法返回值转换为响应体字符串,因此如果没有自定义规则,不会报异常,但是前端接受的数据则为"type":"APARTMENT",需要注意检查。
四、解决问题
从问题分析中我们得知,这里虽然只会出现一个报错信息,但想要枚举类类型能够按我们所要求的那样在程序中正常地进行类型转换,我们需要解决三个问题。
4.1 WebDataBinder类型转换
既然SpringMVC中的WebDataBinder组件自带的类型转换器无法处理从数字字符串到ItemType类型的转换,那么我们需要手动添加Converter,告诉WebDataBinder组件如何处理源类型到目标类型的转换。
首先,我们通过实现Spring框架提供的Converter<S,T>接口来编写一个Converter转换器
@Component
public class StringToItemTypeConverter implements Converter<String, ItemType> {
@Override
public ItemType convert(String source) {
ItemType[] values = ItemType.class.getEnumConstants();
for (ItemType itemType : values) {
if (itemType.getCode().equals(Integer.valueOf(source))) {
return itemType;
}
}
throw new IllegalArgumentException("code:"+source+"非法");
}
}
拓展:
当我们有多个枚举类需要编写Converter转换器,那么一个个地去编写转换器无疑过于繁琐,我们则可以通过编写一个ConverterFactory来一劳永逸的解决。
我们可以看到,目标类型T必须是传入类型或其父类,因此此处使用前面提到的所有枚举类的接口BaseEnum,就能解决所有枚举类的类型Converter转换问题了。
@Component
public class StringToBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
return new Converter<String, T>() {
@Override
public T convert(String source) {
T[] values = targetType.getEnumConstants();
for (T t : values) {
if (t.getCode().equals(Integer.valueOf(source))) {
return t;
}
}
throw new IllegalArgumentException("非法的枚举值:"+source);
}
};
}
}
然后向通过Configuration向WebDataBinder组件注册该Converter或ConverterFactory即可
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
private StringToBaseEnumConverterFactory factory;
@Autowired
private StringToItemTypeConverter converter;
@Override
public void addFormatters(FormatterRegistry registry) {
// registry.addConverter(this.converter);
registry.addConverterFactory(this.factory);
}
}
4.2 TypeHandler类型转换
与WebDataBinder类似,TypeHandler需要自定义如何进行类型转换。
如果使用的是mybatis,那么我们必须要手动编写一个TypeHandler类。不过mybatis-plus则提供了一个通用的处理枚举类型的TypeHandler,只需要在ItemType中添加一个@EnumValue注解,mybatis-plus就可以自动完成从ItemType类型到code属性的相互映射。
@EnumValue
private Integer code;
4.3 HTTPMessageConverter 类型转换
与TypeHandler相同,其提供了一个注解@JsonValue,使用该注解Jackson就可以自动完成从ItemType类型到code属性的相互映射。官方文档
@EnumValue
@JsonValue
private Integer code;
五、总结
MVC框架自带的类型转换功能有时候并不能满足我们的需求,这时候我们就需要手动添加响应的类型转换功能。虽然问题有时候看起来会无从下手,但冷静下来仔细分析,逐步排查可能的原因,就能找到正确的解决办法。