缘由
上周撸代码的过程中,开发了一个相关接口,其实内容倒是简单(相信大家可有手就行),主要是根据不同类型查询不同的标签列表。
less
代码解读
复制代码
@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)); }
唯一不同的是,为了提升代码的可读性与可维护性,同时为了限制取值范围,减少输入错误。入参并没有使用具体的数字来接入参数。而是通过枚举类来进行参数接入。
具体枚举类如下:
typescript
代码解读
复制代码
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确接踵而来。
CleanShot 2024-09-09 at 11.00.03@2x
这是什么原因造成的呢?
其实 ItemType
枚举类的定义是通过 code
(如1
或2
)来表示的,但Spring框架默认只会通过枚举常量的名称来进行匹配。例如,你的ItemType
枚举常量是 STANDART
和 NON-STANDART
,而你传递的参数是 "1"
,这与 STANDART
或 NON-STANDART
不匹配,因此出现转换错误。
其中原理
其实Spring框架默认集成了转换器来进行转换,但是默认只会通过枚举常量的名称来进行匹配。而我们前端传递的参数确实 1,跟我后端枚举类的中的STANDART
或 NON-STANDART
不匹配,所以出现了转换错误。
下面我画图让大家更清晰的去明白其中的运转过程。
请求流程
image-20240909112211471
说明
- SpringMVC中的WebDataBinder组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。
- Mybatis中的TypeHandler用于处理Java中的实体对象与数据库之间的数据类型转换。
响应流程:
image-20240909113855771
说明:
SpringMVC中的HTTPMessageConverter组件负责将Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象),例如保存或更新标签信息的接口。
搞清楚原理了,那我们就有思路了如果去解决这个问题了。
解决方案
其实这里有两种方式,下面为大家一一介绍。
第一种
既然Spring默认的转换器不能帮我进行做到转换,那我们直接自己定义一个符合我们自己需求的转换器,然后注入给Spring容器,之后每次都走我们自定义的转换器即可。
自定义枚举转换器
typescript
代码解读
复制代码
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配置中注册这个转换器
typescript
代码解读
复制代码
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()); } }
重新启动测试
可以看到这次启动正常,数据也可以正常访问了。
CleanShot 2024-09-09 at 11.49.09@2x
第二种
第一种方式比较清晰简单,只需要自定义住转换器 + 配置即可实现。另一种方式是将参数接收到的code
转换为对应的枚举值。你可以在枚举类 ItemType
中实现一个静态的fromCode
方法,根据code
返回相应的枚举实例。
修改 ItemType
枚举类
typescript
代码解读
复制代码
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); } }
修改控制器中的方法:
less
代码解读
复制代码
@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。如果你的系统中有大量类似的枚举类型,使用自定义转换器能够更好地支持不同的枚举类型,并且在新增类似需求时扩展性更高。
新的问题
那么方案一真的就是最优的方案吗?
下面接往下看,在我们项目中可不仅仅只有这一个字段枚举,比如我们还有一个全局的状态枚举以及等等其他业务枚举。
CleanShot 2024-09-09 at 11.57.34@2x
难道我们需要每次都按照方案一的设计进行定义吗,那岂不是有很多冗余代码吗?
当然不是,我们需要去设计一些通用都接口来进行实现,接下来直接上代码。
通用设计
为了实现一个通用的枚举转换,我们可以设计一个基于接口的解决方案,使得所有实现 BaseEnum
接口的枚举都可以使用相同的转换器。这样,无论是 LeaseStatus
还是其他类似枚举,都可以使用同一个枚举转换逻辑,而不需要为每个枚举都写一个新的转换器。
定义 BaseEnum
接口
确保所有枚举类实现该接口,这样每个枚举都有 getCode()
和 getName()
方法。
csharp
代码解读
复制代码
public interface BaseEnum<T> { T getCode(); String getName(); }
实现通用的枚举转换器
通过反射和泛型,你可以创建一个可以处理任何实现了 BaseEnum
接口的枚举类型的转换器。
typescript
代码解读
复制代码
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
java
代码解读
复制代码
@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { private final StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory; @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(stringToBaseEnumConverterFactory); } }
以后你的你的枚举只要实现了 BaseEnum
接口,那么你可以直接使用这个通用的转换器,而无需为每个枚举都单独写转换逻辑。这样不仅提高了代码的复用性,也让代码更加简洁易维护。
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是 Leo,一个在互联网行业的小白,立志成为更好的自己。