手把手教你实现自定义枚举转换器
手把手教你实现自定义枚举转换器
前言
大家好,我是Leo哥🫣🫣🫣,本文将手把手教你实现自定义枚举转换器。
缘由
上周撸代码的过程中,开发了一个相关接口,其实内容倒是简单(相信大家可有手就行),主要是根据不同类型查询不同的标签列表。
@GetMapping("list")
public Result<List<LabelInfo>> labelList(@RequestParam(required = false) ItemType type) {
LambdaQueryWrapper<LabelInfo> wrapper = Wrappers.lambdaQuery(LabelInfo.class);
wrapper.eq(ObjectUtil.isNotNull(type), LabelInfo::getType, type);
return Results.success(labelInfoService.list(wrapper));
}
唯一不同的是,为了提升代码的可读性与可维护性,同时为了限制取值范围,减少输入错误。入参并没有使用具体的数字来接入参数。而是通过枚举类来进行参数接入。
具体枚举类如下:
public enum ItemType implements BaseEnum {
STANDART(1, "标产"),
NON-STANDART(2, "非标产");
@EnumValue
@JsonValue
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;
}
}
这样做看上去没啥问题对吧,但是当我们启动项目测试的时候,bug确接踵而来。
这是什么原因造成的呢?
其实 ItemType
枚举类的定义是通过 code
(如1
或2
)来表示的,但Spring框架默认只会通过枚举常量的名称来进行匹配。例如,你的ItemType
枚举常量是 STANDART
和 NON-STANDART
,而你传递的参数是 "1"
,这与 STANDART
或 NON-STANDART
不匹配,因此出现转换错误。
其中原理
其实Spring框架默认集成了转换器来进行转换,但是默认只会通过枚举常量的名称来进行匹配。而我们前端传递的参数确实 1,跟我后端枚举类的中的STANDART
或 NON-STANDART
不匹配,所以出现了转换错误。
下面我画图让大家更清晰的去明白其中的运转过程。
请求流程
说明
- SpringMVC中的WebDataBinder组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。
- Mybatis中的TypeHandler用于处理Java中的实体对象与数据库之间的数据类型转换。
响应流程:
说明:
SpringMVC中的HTTPMessageConverter组件负责将Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象),例如保存或更新标签信息的接口。
搞清楚原理了,那我们就有思路了如果去解决这个问题了。
解决方案
其实这里有两种方式,下面为大家一一介绍。
第一种
既然Spring默认的转换器不能帮我进行做到转换,那我们直接自己定义一个符合我们自己需求的转换器,然后注入给Spring容器,之后每次都走我们自定义的转换器即可。
自定义枚举转换器
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.leocoder.lease.model.enums.ItemType;
@Component
public class StringToItemTypeConverter implements Converter<String, ItemType> {
@Override
public ItemType convert(String source) {
try {
// 根据 code 转换为枚举
int code = Integer.parseInt(source);
for (ItemType itemType : ItemType.values()) {
if (itemType.getCode().equals(code)) {
return itemType;
}
}
} catch (NumberFormatException e) {
// 如果转换失败,返回 null 或抛出异常
return null;
}
return null;
}
}
在Spring配置中注册这个转换器
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToItemTypeConverter());
}
}
重新启动测试
可以看到这次启动正常,数据也可以正常访问了。
第二种
第一种方式比较清晰简单,只需要自定义住转换器 + 配置即可实现。另一种方式是将参数接收到的code
转换为对应的枚举值。你可以在枚举类 ItemType
中实现一个静态的fromCode
方法,根据code
返回相应的枚举实例。
修改 ItemType
枚举类
public enum ItemType implements BaseEnum {
STANDART(1, "标产"),
NON_STANDART(2, "非标产");
@EnumValue
@JsonValue
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;
}
// 新增静态方法,根据 code 返回对应的枚举值
public static ItemType fromCode(Integer code) {
for (ItemType type : ItemType.values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Invalid ItemType code: " + code);
}
}
修改控制器中的方法:
@GetMapping("list")
public Result<List<LabelInfo>> labelList(@RequestParam(required = false) Integer type) {
LambdaQueryWrapper<LabelInfo> wrapper = Wrappers.lambdaQuery(LabelInfo.class);
if (ObjectUtil.isNotNull(type)) {
// 使用枚举类中的 fromCode 方法进行转换
ItemType itemType = ItemType.fromCode(type);
wrapper.eq(LabelInfo::getType, itemType);
}
return Results.success(labelInfoService.list(wrapper));
}
启动测试,依然可以实现我们的功能。
两种方案对比
下面我们从几个角度来分析一下哪种方案更好一点呢,更优雅一些。
可维护性
- 方案1:自定义枚举转换器
- 如果你有多个枚举类,使用自定义转换器的方式可以通过Spring的机制自动将请求参数转换为相应的枚举。你只需编写一次转换器,它就能应用于整个项目。扩展性更强,适用于多种枚举类型。
- 方案2:枚举类中的
fromCode
静态方法- 此方法不具备全局性,适用范围仅限于该枚举类。每当需要转换时,需要手动调用静态方法,不能全局自动映射。这对于项目中枚举类型多的情况来说,扩展性稍显不足。
推荐:方案1。如果你的系统中有大量类似的枚举类型,使用自定义转换器能够更好地支持不同的枚举类型,并且在新增类似需求时扩展性更高。
优雅性
- 方案1:自定义枚举转换器
- Spring 的
Converter
是Spring内置的机制,它可以自动处理从String
到枚举类型的转换,避免了在每个地方显式调用fromCode
的方法。这种方法更加贴近Spring的设计理念,代码看起来更加简洁和自动化,符合 "约定优于配置" 的设计思想。
- Spring 的
- 方案2:枚举类中的
fromCode
静态方法- 这种方式虽然也很清晰,但每次需要手动调用
fromCode
方法,在方法调用时显得略微冗余。如果你的系统中需要频繁地进行枚举和code
之间的转换,显式地在控制器层调用转换方法会让代码略显臃肿。
- 这种方式虽然也很清晰,但每次需要手动调用
推荐:方案1。它利用了Spring的内置机制,使代码更加简洁优雅,同时减轻了工作量。
拓展性
- 方案1: 自定义枚举转换器
- 如果你有多个枚举类,使用自定义转换器的方式可以通过Spring的机制自动将请求参数转换为相应的枚举。你只需编写一次转换器,它就能应用于整个项目。扩展性更强,适用于多种枚举类型。
- 方案2: 枚举类中的
fromCode
静态方法- 此方法不具备全局性,适用范围仅限于该枚举类。每当需要转换时,需要手动调用静态方法,不能全局自动映射。这对于项目中枚举类型多的情况来说,扩展性稍显不足。
推荐:方案1。如果你的系统中有大量类似的枚举类型,使用自定义转换器能够更好地支持不同的枚举类型,并且在新增类似需求时扩展性更高。
新的问题
那么方案一真的就是最优的方案吗?
下面接往下看,在我们项目中可不仅仅只有这一个字段枚举,比如我们还有一个全局的状态枚举以及等等其他业务枚举。
难道我们需要每次都按照方案一的设计进行定义吗,那岂不是有很多冗余代码吗?
当然不是,我们需要去设计一些通用都接口来进行实现,接下来直接上代码。
通用设计
为了实现一个通用的枚举转换,我们可以设计一个基于接口的解决方案,使得所有实现 BaseEnum
接口的枚举都可以使用相同的转换器。这样,无论是 LeaseStatus
还是其他类似枚举,都可以使用同一个枚举转换逻辑,而不需要为每个枚举都写一个新的转换器。
定义 BaseEnum
接口
确保所有枚举类实现该接口,这样每个枚举都有 getCode()
和 getName()
方法。
public interface BaseEnum<T> {
T getCode();
String getName();
}
实现通用的枚举转换器
通过反射和泛型,你可以创建一个可以处理任何实现了 BaseEnum
接口的枚举类型的转换器。
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class StringToBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum<?>> {
@Override
public <T extends BaseEnum<?>> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToBaseEnumConverter<>(targetType);
}
private static class StringToBaseEnumConverter<T extends BaseEnum<?>> implements Converter<String, T> {
private final Class<T> enumType;
public StringToBaseEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
for (T enumConstant : enumType.getEnumConstants()) {
// 使用 BaseEnum 的 getCode() 来进行匹配
if (enumConstant.getCode().toString().equals(source)) {
return enumConstant;
}
}
throw new IllegalArgumentException("Invalid value '" + source + "' for enum " + enumType.getSimpleName());
}
}
}
解释:
BaseEnum
接口定义了getCode()
和getName()
,枚举通过实现这个接口可以统一转换规则。StringToBaseEnumConverterFactory
是一个工厂类,用于生成适用于所有BaseEnum
的转换器。StringToBaseEnumConverter
是实际的转换逻辑。它通过反射获取目标枚举类的所有常量,并使用getCode()
方法进行匹配。
注入Spring
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(stringToBaseEnumConverterFactory);
}
}
以后你的你的枚举只要实现了 BaseEnum
接口,那么你可以直接使用这个通用的转换器,而无需为每个枚举都单独写转换逻辑。这样不仅提高了代码的复用性,也让代码更加简洁易维护。
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是 Leo,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。