跳至主要內容

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

Leo基础拓展基础拓展约 2616 字大约 9 分钟

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

前言

大家好,我是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确接踵而来。

CleanShot 2024-09-09 at 11.00.03@2x
CleanShot 2024-09-09 at 11.00.03@2x

这是什么原因造成的呢?

其实 ItemType 枚举类的定义是通过 code(如12)来表示的,但Spring框架默认只会通过枚举常量的名称来进行匹配。例如,你的ItemType枚举常量是 STANDARTNON-STANDART,而你传递的参数是 "1",这与 STANDARTNON-STANDART 不匹配,因此出现转换错误。

其中原理

其实Spring框架默认集成了转换器来进行转换,但是默认只会通过枚举常量的名称来进行匹配。而我们前端传递的参数确实 1,跟我后端枚举类的中的STANDARTNON-STANDART 不匹配,所以出现了转换错误。

下面我画图让大家更清晰的去明白其中的运转过程。

请求流程

image-20240909112211471
image-20240909112211471

说明

  • SpringMVC中的WebDataBinder组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。
  • Mybatis中的TypeHandler用于处理Java中的实体对象与数据库之间的数据类型转换。

响应流程:

image-20240909113855771
image-20240909113855771

说明:

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());
    }
}

重新启动测试

可以看到这次启动正常,数据也可以正常访问了。

CleanShot 2024-09-09 at 11.49.09@2x
CleanShot 2024-09-09 at 11.49.09@2x

第二种

第一种方式比较清晰简单,只需要自定义住转换器 + 配置即可实现。另一种方式是将参数接收到的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的设计理念,代码看起来更加简洁和自动化,符合 "约定优于配置" 的设计思想。
  • 方案2:枚举类中的 fromCode 静态方法
    • 这种方式虽然也很清晰,但每次需要手动调用 fromCode 方法,在方法调用时显得略微冗余。如果你的系统中需要频繁地进行枚举和code之间的转换,显式地在控制器层调用转换方法会让代码略显臃肿。

推荐:方案1。它利用了Spring的内置机制,使代码更加简洁优雅,同时减轻了工作量。

拓展性

  • 方案1: 自定义枚举转换器
    • 如果你有多个枚举类,使用自定义转换器的方式可以通过Spring的机制自动将请求参数转换为相应的枚举。你只需编写一次转换器,它就能应用于整个项目。扩展性更强,适用于多种枚举类型。
  • 方案2: 枚举类中的 fromCode 静态方法
    • 此方法不具备全局性,适用范围仅限于该枚举类。每当需要转换时,需要手动调用静态方法,不能全局自动映射。这对于项目中枚举类型多的情况来说,扩展性稍显不足。

推荐:方案1。如果你的系统中有大量类似的枚举类型,使用自定义转换器能够更好地支持不同的枚举类型,并且在新增类似需求时扩展性更高。

新的问题

那么方案一真的就是最优的方案吗?

下面接往下看,在我们项目中可不仅仅只有这一个字段枚举,比如我们还有一个全局的状态枚举以及等等其他业务枚举。

CleanShot 2024-09-09 at 11.57.34@2x
CleanShot 2024-09-09 at 11.57.34@2x

难道我们需要每次都按照方案一的设计进行定义吗,那岂不是有很多冗余代码吗?

当然不是,我们需要去设计一些通用都接口来进行实现,接下来直接上代码。

通用设计

为了实现一个通用的枚举转换,我们可以设计一个基于接口的解决方案,使得所有实现 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());
        }
    }
}

解释:

  1. BaseEnum 接口定义了 getCode()getName(),枚举通过实现这个接口可以统一转换规则。
  2. StringToBaseEnumConverterFactory 是一个工厂类,用于生成适用于所有 BaseEnum 的转换器。
  3. 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,后面文章会首先同步至公众号。

公众号封面
公众号封面