原文链接:赵侠客
前言
枚举类型在开发中是很常见的,有非常多的应用场景,如状态管理、类型分类、权限控制、配置管理、错误码管理、日志级别等。正确合理的使用枚举可以给我们带来非常多的好处:
- 增强代码可读性:枚举可以使得代码更加清晰、易于理解。它们提供了一种方式来组织和表示相关的常量值,使得代码更易于阅读和维护。
- 类型安全性:枚举类型能够限制变量的值,只能取枚举类型中定义的常量之一,从而避免了错误的赋值。这有助于减少代码中的错误,并提高代码的稳定性。
- 更好的维护性:枚举类型可以在编译时进行类型检查,这有助于更早地发现和修复问题。此外,由于枚举类型中的常量值是预定义的,因此可以减少对常量值的修改,从而简化代码的维护。
- 更好的性能:枚举类型的值是在编译时确定的,因此在运行时访问枚举类型的值会更快。此外,由于枚举类型中的常量值是唯一的,因此可以直接使用"=="进行两个值之间的对比,这有助于提高性能。
- 更好的组织性:枚举类型可以帮助我们将相关的值组织在一起,使代码更加整洁。通过将相关的常量值组合在一起,可以使代码更加易于理解和维护。
- 可扩展性:枚举类型可以轻松地扩展或更新,而不会对其他部分的代码造成影响。这有助于保持代码的灵活性和可扩展性。
- 便于测试:枚举类型可以方便地进行测试,因为它们具有有限且确定的值域。这使得测试人员可以更容易地覆盖所有可能的场景,并确保代码的正确性。
虽然枚举有诸多的好处,但是使用枚举也给我们带来了一些困扰:
- 前后端数据格式转换:前端主要给用户展示数据,不能直接显示枚举值,需要前端将枚举转成用户可读的数据显示
- 数据库的存储:代码中的枚举类型无法直接存储数据库,一般转成数值类型,这样还可以减少存储空间
- 代码中大量类型转换:查询时需要数值类型转成枚举类型,保存时又需要将枚举类型转成数值类型
针对枚举存在的问题,本文介绍一种枚举从数据库-->后端代码-->前端代码-->页面和从页面-->前端代码-->后端代码-->数据库的自动转换方案,大大方便前后端使用枚举类型。
自动转换目标
我们以用户状态为例,用户有两种状态:禁用和启用
- 前端页面:前端页面显示用户状态时用"禁用、启用";
- 前端代码:前端代码里处理用户状态时用:"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
主要有三个方法
getCode()
获取枚举的数值如"0、1";getName()
获取枚举显示值如"禁用、启用" ;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());
}
}
}