手把手教你实现自定义枚举转换器

缘由

上周撸代码的过程中,开发了一个相关接口,其实内容倒是简单(相信大家可有手就行),主要是根据不同类型查询不同的标签列表。

 

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(如12)来表示的,但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的设计理念,代码看起来更加简洁和自动化,符合 "约定优于配置" 的设计思想。
  • 方案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());         }     } }

解释:

  1. BaseEnum 接口定义了 getCode() 和 getName(),枚举通过实现这个接口可以统一转换规则。
  2. StringToBaseEnumConverterFactory 是一个工厂类,用于生成适用于所有 BaseEnum 的转换器。
  3. 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,一个在互联网行业的小白,立志成为更好的自己。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值