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

原文链接:赵侠客

前言

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

  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());
        }
    }
}
相关推荐
逐·風26 分钟前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫1 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
码农小旋风1 小时前
详解K8S--声明式API
后端
Peter_chq1 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml41 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616881 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
尚梦1 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
aloha_7892 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
GIS程序媛—椰子2 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js