前言
枚举类型在开发中是很常见的,有非常多的应用场景,如状态管理、类型分类、权限控制、配置管理、错误码管理、日志级别等。正确合理的使用枚举可以给我们带来非常多的好处:
- 增强代码可读性:枚举可以使得代码更加清晰、易于理解。它们提供了一种方式来组织和表示相关的常量值,使得代码更易于阅读和维护。
- 类型安全性:枚举类型能够限制变量的值,只能取枚举类型中定义的常量之一,从而避免了错误的赋值。这有助于减少代码中的错误,并提高代码的稳定性。
- 更好的维护性:枚举类型可以在编译时进行类型检查,这有助于更早地发现和修复问题。此外,由于枚举类型中的常量值是预定义的,因此可以减少对常量值的修改,从而简化代码的维护。
- 更好的性能:枚举类型的值是在编译时确定的,因此在运行时访问枚举类型的值会更快。此外,由于枚举类型中的常量值是唯一的,因此可以直接使用“==”进行两个值之间的对比,这有助于提高性能。
- 更好的组织性:枚举类型可以帮助我们将相关的值组织在一起,使代码更加整洁。通过将相关的常量值组合在一起,可以使代码更加易于理解和维护。
- 可扩展性:枚举类型可以轻松地扩展或更新,而不会对其他部分的代码造成影响。这有助于保持代码的灵活性和可扩展性。
- 便于测试:枚举类型可以方便地进行测试,因为它们具有有限且确定的值域。这使得测试人员可以更容易地覆盖所有可能的场景,并确保代码的正确性。
虽然枚举有诸多的好处,但是使用枚举也给我们带来了一些困扰:
- 前后端数据格式转换:前端主要给用户展示数据,不能直接显示枚举值,需要前端将枚举转成用户可读的数据显示
- 数据库的存储:代码中的枚举类型无法直接存储数据库,一般转成数值类型,这样还可以减少存储空间
- 代码中大量类型转换:查询时需要数值类型转成枚举类型,保存时又需要将枚举类型转成数值类型
针对枚举存在的问题,本文介绍一种枚举从数据库-->后端代码-->前端代码-->页面和从页面-->前端代码-->后端代码-->数据库的自动转换方案,大大方便前后端使用枚举类型。
自动转换目标
我们以用户状态为例,用户有两种状态:禁用和启用
前端页面:前端页面显示用户状态时用“禁用、启用”;
前端代码:前端代码里处理用户状态时用:“ENABLE、DISABLE”或者用“0、1”;
后端代码:后端代码使用StatusEnum枚举类;
数据库:数据库存储用户状态时禁用存1、启用存0。
我们的目标是让枚举在各个环境流转时全自动转换。
代码与数据库自动转换
第一步创建统一的枚举基类BaseEnum
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
接口
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
主要有三个方法
getCode()
获取枚举的数值如“0、1”;getName()
获取枚举显示值如“禁用、启用” ;getEnumName()
获取枚举的枚举值如“ENABLE、DISABLE”.
如果使用MybatisPlus, 可以使用@EnumValue
注解很方便的帮我们解决数据库与实体对象中枚举类型的相互转换,如果只使用的Mybatis可以自定义TypeHandler
来解决数据库到JAVA枚举对象的自动转换。
第三步创建用户类User
用户状态使用StatusEnum
类
@Data @TableName("user") public class User { private Long id; private String userName; private StatusEnum status; }
前后端相互转换
当前端查询用户时,我们希望将枚举的三个属性都返回给前端,前端页面显示时取status.name
代码中使用status.enum
或者status.code
{ "id": 3581209395268, "userName": "test2@8531.cn", "status": { "name": "禁用", "enum": "DISABLE", "code": 1 } }
为了达到将枚举序列化成一个json对象,我们需要自定义序列化器和反序列化器,以下以SpringBoot自带的Jackson为例:
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消息转换器
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中,这样就完成所有枚举自动转换。
@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()); } }
查询用户
GET http://localhost:90/user/3581209395268 返回: { "id": 3581209395268, "userName": "test2@8531.cn", "status": { "name": "启用", "enum": "ENABLE", "code": 0 } }
application/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格式传参
@PathVariable格式传参
对应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对象转成对应枚举就好了,但是范型的写法目前还不知道怎么写,不可能增加一个枚举写一个反序列化器,有知道的可以回复一下,相互学习。
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()); } } }