项目中如何优雅的使用枚举

原文链接:赵侠客

前言

枚举类型在开发中是很常见的,有非常多的应用场景,如状态管理、类型分类、权限控制、配置管理、错误码管理、日志级别等。正确合理的使用枚举可以给我们带来非常多的好处:

  1. 增强代码可读性:枚举可以使得代码更加清晰、易于理解。它们提供了一种方式来组织和表示相关的常量值,使得代码更易于阅读和维护。
  2. 类型安全性:枚举类型能够限制变量的值,只能取枚举类型中定义的常量之一,从而避免了错误的赋值。这有助于减少代码中的错误,并提高代码的稳定性。
  3. 更好的维护性:枚举类型可以在编译时进行类型检查,这有助于更早地发现和修复问题。此外,由于枚举类型中的常量值是预定义的,因此可以减少对常量值的修改,从而简化代码的维护。
  4. 更好的性能:枚举类型的值是在编译时确定的,因此在运行时访问枚举类型的值会更快。此外,由于枚举类型中的常量值是唯一的,因此可以直接使用"=="进行两个值之间的对比,这有助于提高性能。
  5. 更好的组织性:枚举类型可以帮助我们将相关的值组织在一起,使代码更加整洁。通过将相关的常量值组合在一起,可以使代码更加易于理解和维护。
  6. 可扩展性:枚举类型可以轻松地扩展或更新,而不会对其他部分的代码造成影响。这有助于保持代码的灵活性和可扩展性。
  7. 便于测试:枚举类型可以方便地进行测试,因为它们具有有限且确定的值域。这使得测试人员可以更容易地覆盖所有可能的场景,并确保代码的正确性。

虽然枚举有诸多的好处,但是使用枚举也给我们带来了一些困扰:

  1. 前后端数据格式转换:前端主要给用户展示数据,不能直接显示枚举值,需要前端将枚举转成用户可读的数据显示
  2. 数据库的存储:代码中的枚举类型无法直接存储数据库,一般转成数值类型,这样还可以减少存储空间
  3. 代码中大量类型转换:查询时需要数值类型转成枚举类型,保存时又需要将枚举类型转成数值类型

针对枚举存在的问题,本文介绍一种枚举从数据库-->后端代码-->前端代码-->页面和从页面-->前端代码-->后端代码-->数据库的自动转换方案,大大方便前后端使用枚举类型。

自动转换目标

我们以用户状态为例,用户有两种状态:禁用和启用

  • 前端页面:前端页面显示用户状态时用"禁用、启用";
  • 前端代码:前端代码里处理用户状态时用:"ENABLE、DISABLE"或者用"0、1";
  • 后端代码:后端代码使用StatusEnum枚举类;
  • 数据库:数据库存储用户状态时禁用存1、启用存0。 我们的目标是让枚举在各个环境流转时全自动转换。

代码与数据库自动转换

第一步创建统一的枚举基类BaseEnum

java 复制代码
public interface BaseEnum {
    int getCode();
    String getName();
    String getEnumName();
    static <T extends BaseEnum> T getInstance(Class<T> clazz, String value) {
        T[] constants = clazz.getEnumConstants();
        for (T t : constants) {
            if(StrUtil.isNumeric(value)){
                if (t.getCode() == Integer.parseInt(value)) {
                    return t;
                }
            }else {
                if (t.getEnumName().equals(value)) {
                    return t;
                }
            }
        }
        return null;
    }
}

第二步创建用户状态类StatusEnum实现BaseEnum接口

java 复制代码
public enum StatusEnum implements BaseEnum {
    ENABLE(0,"启用"),
    DISABLE(1,"禁用");
    @EnumValue
    private int code;
    private String name;
    StatusEnum(int code, String name) {
        this.code = code;
        this.name=name;
    }
    @Override
    public int getCode() {
        return code;
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public String getEnumName() {
        return this.name();
    }
}

BaseEnum主要有三个方法

  1. getCode()获取枚举的数值如"0、1";
  2. getName()获取枚举显示值如"禁用、启用" ;
  3. getEnumName()获取枚举的枚举值如"ENABLE、DISABLE".

如果使用MybatisPlus, 可以使用@EnumValue注解很方便的帮我们解决数据库与实体对象中枚举类型的相互转换,如果只使用的Mybatis可以自定义TypeHandler来解决数据库到JAVA枚举对象的自动转换。

第三步创建用户类User用户状态使用StatusEnum

java 复制代码
@Data
@TableName("user")
public class User {
    private Long id;
    private String userName;
    private StatusEnum status;
}

前后端相互转换

当前端查询用户时,我们希望将枚举的三个属性都返回给前端,前端页面显示时取status.name代码中使用status.enum或者status.code

json 复制代码
{
  "id": 3581209395268,
  "userName": "test2@8531.cn",
  "status": {
    "name": "禁用",
    "enum": "DISABLE",
    "code": 1
  }
}

为了达到将枚举序列化成一个json对象,我们需要自定义序列化器和反序列化器,以下以SpringBoot自带的Jackson为例:

java 复制代码
public class BaseEnumSerializer extends StdSerializer<BaseEnum> {
    public BaseEnumSerializer() {
        this(null);
    }
    public BaseEnumSerializer(Class<BaseEnum> t) {
        super(t);
    }
    @Override
    public void serialize(BaseEnum value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("name",value.getName());
        gen.writeStringField("enum",value.getEnumName());
        gen.writeNumberField("code",value.getCode());
        gen.writeEndObject();;
    }
}
public class BaseEnumDeserializer<T extends BaseEnum> extends StdDeserializer<T> {
    private Class<T> type;
    public BaseEnumDeserializer() {
        this(null);
    }
    public BaseEnumDeserializer(Class<T> vc) {
        super(vc);
        type = vc;
    }
    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return BaseEnum.getInstance(type, p.getText());
    }
}

自定义Jackson序列化器与反序列化器只能解决数据类型为application/json格式的请求,当请求类型为application/x-www-form-urlencoded我们还需要自定义Spring消息转换器

java 复制代码
'public class NumBaseEnumConverterFactory implements ConverterFactory<Number, BaseEnum> {
    @Override
    public <T extends BaseEnum> Converter<Number, T> getConverter(Class<T> aClass) {
        return new NumberToEnumConverter<>(aClass);
    }
    private final class NumberToEnumConverter<T extends BaseEnum> implements Converter<Number, T> {
        private Class<T> enumType;
        public NumberToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }
        @Override
        public T convert(Number s) {
            return BaseEnum.getInstance(enumType,s.toString());
        }
    }
}

public class StrBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
    @Override
    public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> aClass) {
        return new StringToEnumConverter<>(aClass);
    }
    private final class StringToEnumConverter<T extends BaseEnum> implements Converter<String, T> {
        private Class<T> enumType;
        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }
        @Override
        public T convert(String s) {
            return BaseEnum.getInstance(enumType,s);
        }
    }
}

以上两个消息转换器可以在数据格式以表单形式提交时将数值类型(0、1)和枚举值类型(ENABLE、DISABLE)转成枚举类型。

将自定义好的数据转换器注入到Spring中,这样就完成所有枚举自动转换。

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addSerializer(BaseEnum.class, new BaseEnumSerializer());
        module.addDeserializer(BaseEnum.class, new BaseEnumDeserializer<>());
        mapper.registerModule(module);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper;
    }
     @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new StrBaseEnumConverterFactory());
        registry.addConverterFactory(new NumBaseEnumConverterFactory());
    }
}

查询用户

json 复制代码
GET http://localhost:90/user/3581209395268
返回:
{
  "id": 3581209395268,
  "userName": "test2@8531.cn",
  "status": {
    "name": "启用",
    "enum": "ENABLE",
    "code": 0
  }
}

application/json格式传参

json 复制代码
POST http://localhost:90/user
Content-Type: application/json

{
  "id": 3581209395268,
  "status": "DISABLE"
}

###
POST http://localhost:90/user
Content-Type: application/json

{
  "id": 3581209395268,
  "status": "0"
}

application/x-www-form-urlencoded格式传参

bash 复制代码
PUT http://localhost:90/user
Content-Type: application/x-www-form-urlencoded

id=3581209395268&status=ENABLE
###
PUT http://localhost:90/user
Content-Type: application/x-www-form-urlencoded

id=3581209395268&status=1

###
PUT http://localhost:90/user/3581209395268?status=ENABLE
Content-Type: application/x-www-form-urlencoded

###
PUT http://localhost:90/user/3581209395268?status=1
Content-Type: application/x-www-form-urlencoded

@PathVariable格式传参

bash 复制代码
PUT http://localhost:90/user/3581209395268/ENABLE
Content-Type: application/x-www-form-urlencoded

###
PUT http://localhost:90/user/3581209395268/1
Content-Type: application/x-www-form-urlencoded

对应JAVA代码:

JAVA 复制代码
@RestController
public class UserController {
    @Resource
    private UserMapper userMapper;

    @GetMapping("/user/{id}")
    public User getById(@PathVariable Long id) {
        return userMapper.selectById(id);
    }
    
    @PostMapping("/user")
    public User upadteById(@RequestBody User user) {
        userMapper.updateById(user);
        return user;
    }

    @PutMapping("/user")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user;
    }

    @PutMapping("/user/{id}/{status}")
    public User updateStatus(@PathVariable Long id,@PathVariable StatusEnum status) {
        User user=userMapper.selectById(id);
        user.setStatus(status);
        userMapper.updateById(user);
        return user;
    }

    @PutMapping("/user/{id}")
    public User updateUserStatus(@PathVariable Long id,@RequestParam StatusEnum status) {
        User user=userMapper.selectById(id);
        user.setStatus(status);
        userMapper.updateById(user);
        return user;
    }
}

这样很方便的解决了枚举在各个环节的自动转换问题,其它枚举只要实现BaseEnum接口就能实现全自动转换,前后端用起来也方便了不少。

总结

本文主要介绍了项目中使用枚举的优缺点,并针对缺点给出了解决方案,解决了枚举在项目中频繁转换的问题,当然解决的还不是非常完美,比如返回给前端的枚举格式是:{"enum":"DISABLE","code":1} 但是保存时传此数据结构,后端却无法正确的转成枚举,我们可以创建StatusEnumDeserializer,将子json对象转成对应枚举就好了,但是范型的写法目前还不知道怎么写,不可能增加一个枚举写一个反序列化器,有知道的可以回复一下,相互学习。

java 复制代码
public class StatusEnumDeserializer   extends JsonDeserializer<StatusEnum>  {
    @Override
    public StatusEnum  deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonNode node= p.getCodec().readTree(p);
        if(node.isObject()){
            String name= node.get("enum").toString();
            return BaseEnum.getInstance(StatusEnum.class, name);
        }else {
            return BaseEnum.getInstance(StatusEnum.class, node.textValue());
        }
    }
}
相关推荐
儿时可乖了4 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol5 分钟前
java基础概念37:正则表达式2-爬虫
java
雯0609~18 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ21 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
xmh-sxh-131422 分钟前
jdk各个版本介绍
java
学习前端的小z27 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
XINGTECODE35 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码41 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶41 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端