原文链接:赵侠客
前言
枚举类型在开发中是很常见的,有非常多的应用场景,如状态管理、类型分类、权限控制、配置管理、错误码管理、日志级别等。正确合理的使用枚举可以给我们带来非常多的好处:
- 增强代码可读性:枚举可以使得代码更加清晰、易于理解。它们提供了一种方式来组织和表示相关的常量值,使得代码更易于阅读和维护。
- 类型安全性:枚举类型能够限制变量的值,只能取枚举类型中定义的常量之一,从而避免了错误的赋值。这有助于减少代码中的错误,并提高代码的稳定性。
- 更好的维护性:枚举类型可以在编译时进行类型检查,这有助于更早地发现和修复问题。此外,由于枚举类型中的常量值是预定义的,因此可以减少对常量值的修改,从而简化代码的维护。
- 更好的性能:枚举类型的值是在编译时确定的,因此在运行时访问枚举类型的值会更快。此外,由于枚举类型中的常量值是唯一的,因此可以直接使用“==”进行两个值之间的对比,这有助于提高性能。
- 更好的组织性:枚举类型可以帮助我们将相关的值组织在一起,使代码更加整洁。通过将相关的常量值组合在一起,可以使代码更加易于理解和维护。
- 可扩展性:枚举类型可以轻松地扩展或更新,而不会对其他部分的代码造成影响。这有助于保持代码的灵活性和可扩展性。
- 便于测试:枚举类型可以方便地进行测试,因为它们具有有限且确定的值域。这使得测试人员可以更容易地覆盖所有可能的场景,并确保代码的正确性。
虽然枚举有诸多的好处,但是使用枚举也给我们带来了一些困扰:
- 前后端数据格式转换:前端主要给用户展示数据,不能直接显示枚举值,需要前端将枚举转成用户可读的数据显示
- 数据库的存储:代码中的枚举类型无法直接存储数据库,一般转成数值类型,这样还可以减少存储空间
- 代码中大量类型转换:查询时需要数值类型转成枚举类型,保存时又需要将枚举类型转成数值类型
针对枚举存在的问题,本文介绍一种枚举从数据库–>后端代码–>前端代码–>页面和从页面–>前端代码–>后端代码–>数据库的自动转换方案,大大方便前后端使用枚举类型。
自动转换目标
我们以用户状态为例,用户有两种状态:禁用和启用
- 前端页面:前端页面显示用户状态时用“禁用、启用”;
- 前端代码:前端代码里处理用户状态时用:“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,"禁用");@EnumValueprivate int code;private String name;StatusEnum(int code, String name) {this.code = code;this.name=name;}@Overridepublic int getCode() {return code;}@Overridepublic String getName() {return name;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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> {@Overridepublic <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;}@Overridepublic T convert(Number s) {return BaseEnum.getInstance(enumType,s.toString());}}
}public class StrBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {@Overridepublic <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;}@Overridepublic T convert(String s) {return BaseEnum.getInstance(enumType,s);}}
}
以上两个消息转换器可以在数据格式以表单形式提交时将数值类型(0、1)和枚举值类型(ENABLE、DISABLE)转成枚举类型。
将自定义好的数据转换器注入到Spring中,这样就完成所有枚举自动转换。
@Configuration
public class WebConfig implements WebMvcConfigurer {@Beanpublic 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;}@Overridepublic 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格式传参
PUT http://localhost:90/user
Content-Type: application/x-www-form-urlencodedid=3581209395268&status=ENABLE
###
PUT http://localhost:90/user
Content-Type: application/x-www-form-urlencodedid=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格式传参
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代码:
@RestController
public class UserController {@Resourceprivate 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> {@Overridepublic 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());}}
}