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

原文链接:赵侠客

前言

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

  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());
        }
    }
}
相关推荐
飞的肖2 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
Q_19284999064 分钟前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
Code_流苏7 分钟前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
良许Linux9 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
青灯文案110 分钟前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_7482548814 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
求知若饥21 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
ZJ_.26 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营30 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
禁默1 小时前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程