枚举类型的全面处理
# 枚举类型的全面处理:序列化、反序列化与数据库交互
# 引言
在企业级应用开发中,枚举类型广泛用于代表固定集合的数据,如状态码、配置选项等。然而,处理枚举类型的序列化、反序列化及其与数据库的交互常常给开发者带来挑战。本文将详细解释如何在 Java 应用中使用 MyBatis-Plus 和 Jackson 来优雅地处理枚举类型。
# 枚举处理的常见挑战
- 前后端数据交换:前端需要可读的描述,后端需存储为枚举。
- 数据持久化:数据库通常不直接支持枚举类型,需要转换为字符串或整数。
- 代码维护:枚举类型的频繁变动需要灵活的代码设计来适应这些变更。
# 实现自定义序列化器与反序列化器 (处理出参入参中的枚举)
# IEnumToJson 接口
IEnumToJson
接口用于支持枚举类型的自定义 JSON 序列化。
@JsonDeserialize(using = OcloudJsonEnumDeserializer.class)
public interface IEnumToJson extends Serializable {
/**
* 将枚举转换成map
* @return
*/
@JsonValue
default Map<String,Object> toMap() {
return EnumUtils.getFieldValueMap(this.getClass(),this);
}
/**
* 根据枚举name获取枚举值
* @param enumName
* @param clazz
* @param <E>
* @return
*/
static <E extends Enum<E> & IEnumToJson> E toEnum(String enumName, Class<E> clazz) {
if (StringUtils.isEmpty(enumName)) {
return null;
}
return Enum.valueOf(clazz, enumName);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
用途解释:
这个接口扩展了Serializable
,意味着实现此接口的枚举可以被序列化。接口提供了两个主要的方法:
toMap()
: 默认方法,返回枚举的属性映射到一个Map对象。这个方法依赖于一个辅助类EnumUtils
,用于获取枚举类中的字段及其值。toEnum(String enumName, Class<E> clazz)
: 静态方法,根据提供的枚举名称和枚举类,返回相应的枚举实例。这是一个通用方法,用于在反序列化时将JSON字符串转换成相应的枚举类型。
# 自定义 JSON 序列化器 OcloudJsonEnumSerializer
public class OcloudJsonEnumSerializer extends JsonSerializer<IEnumToJson> {
@Override
public void serialize(IEnumToJson value, JsonGenerator gen, SerializerProvider serializers) throws IOException { ;
gen.writeStartObject();
value.toMap().entrySet().stream().forEach(entry -> {
try {
gen.writeObjectField(entry.getKey(),entry.getValue());
} catch (IOException e) {
e.printStackTrace();
}
});
gen.writeEndObject();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
用途解释:
这是一个自定义的JSON序列化器,继承自JsonSerializer<IEnumToJson>
。它的主要职责是:
- 在序列化过程中,将枚举实例转换为JSON对象。通过调用枚举的
toMap()
方法获取枚举的字段和值,然后将这些键值对写入JSON。 - 用于处理当枚举作为返回参数时,如何将枚举序列化成JSON。比如,如果一个API返回一个枚举类型的状态字段,这个序列化器将枚举转换成一个具体的JSON对象,可能包含枚举的名称、代码、描述等。
# 自定义 JSON 反序列化器 OcloudJsonEnumDeserializer
@Slf4j
public class OcloudJsonEnumDeserializer extends JsonDeserializer<IEnumToJson> {
private final String ENUM_NAME = "name";
@Override
@SuppressWarnings("unchecked")
public IEnumToJson deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
JsonNode node = jp.getCodec().readTree(jp);
String currentName = jp.currentName();
Object currentValue = jp.getCurrentValue();
@SuppressWarnings("rawtypes")
Class findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
IEnumToJson valueOf;
JsonNode jsonNode = node.get(ENUM_NAME);
if(JsonNodeType.ARRAY.equals(node.getNodeType())){
jsonNode = node.get(1).get(ENUM_NAME);
}
if (jsonNode == null) {
valueOf = (IEnumToJson) IEnumToJson.toEnum(node.asText(), findPropertyType);
} else {
valueOf = (IEnumToJson) IEnumToJson.toEnum(jsonNode.asText(), findPropertyType);
}
return valueOf;
} catch (Exception e) {
log.error("Error deserializing enum field: {}", jp.currentName(), e);
log.error("JsonNode content: {}", jp.getCodec().readTree(jp).toString());
return null;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
用途解释:
这是一个自定义的JSON反序列化器,继承自JsonDeserializer<IEnumToJson>
。其主要功能包括:
- 在反序列化过程中,从JSON数据中读取并转换为对应的枚举实例。通过解析JSON节点,尝试获取
"name"
字段,然后使用toEnum
方法将其转换为枚举实例。 - 错误处理和日志记录:在反序列化过程中出现错误时,记录错误信息,方便问题定位和调试。
- 用于处理接收到的JSON中含有枚举类型数据时,如何将这些JSON数据反序列化成具体的枚举类型。这是在解析JSON请求体时非常有用,尤其是当API需要根据接收到的枚举值来执行特定操作时。
# 简单例子解释
假设有一个API /api/updateStatus
,其接收一个JSON对象作为请求体,其中包含一个名为 status
的字段,该字段的值是一个枚举类型 StatusEnum
的名称。
枚举定义:
public enum StatusEnum implements IEnumToJson { ACTIVE("Active"), INACTIVE("Inactive"); private String description; StatusEnum(String description) { this.description = description; } @JsonValue public Map<String, Object> toMap() { Map<String, Object> map = new HashMap<>(); map.put("name", this.name()); map.put("description", this.description); return map; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18使用
OcloudJsonEnumDeserializer
反序列化: 当接收到如下JSON请求体时:{ "status": "ACTIVE" }
1
2
3反序列化器将根据
"status"
字段的值"ACTIVE"
,查找StatusEnum
枚举中对应的枚举常量StatusEnum.ACTIVE
。使用
OcloudJsonEnumSerializer
序列化: 当API处理完请求后,假设需要返回这个状态,序列化器将StatusEnum.ACTIVE
枚举常量转换成如下JSON格式:{ "status": { "name": "ACTIVE", "description": "Active" } }
1
2
3
4
5
6
这个处理方式使得枚举值在前后端之间的传递更为清晰明确,且能够携带更多的信息,比如描述等。
# 数据库交互中枚举处理
# IEnum 接口
IEnum
接口允许枚举类附带一个可以存储到数据库的值。
public interface IEnum<T extends Serializable> {
T getValue();
}
2
3
用途解释:
- getValue: 提供一个方法来获取枚举关联的数据库值(如整数或字符串)。这个值是用于数据库存储和读取的,确保枚举与数据库之间的类型兼容。
IEnum
接口是否需要取决于具体需求。如果您的枚举处理不需要与特定的数据类型(如整数、字符串等)关联,或者您不需要在枚举上实现通用的接口方法,那么您可能不需要使用 IEnum
接口。在许多情况下,直接使用 IEnumToJson
可以满足需求,尤其是在涉及到JSON序列化和反序列化时。
# 泛型的定义
泛型 <T>
在 IEnum
接口中通常用来指定与枚举相关联的数据类型。这种类型可以是任何类,例如 Integer
、String
等,用于定义枚举类型所关联的数据(如数据库中的存储值)。用于定义获取枚举实例关联值的方法,这里它有一个 getValue()
方法用于返回枚举的业务值(例如,数据库中存储的具体值)。枚举类通过实现这个接口,可以明确告诉 OcloudEnumTypeHandler
如何从枚举类型中提取这个业务值。
# 自定义 MyBatis-Plus 类型处理器 OcloudEnumTypeHandler
public class OcloudEnumTypeHandler<E extends IEnum<T>,T extends Serializable> extends BaseTypeHandler<E> {
private final Class<E> type;
private final E[] enums;
public OcloudEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
this.enums = type.getEnumConstants();
if (this.enums == null) {
//throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setObject(i,parameter.getValue());
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object value = rs.getObject(columnName);
if (value != null && rs.wasNull()) {
return null;
}
return getValue(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object value = rs.getObject(columnIndex);
if (value != null && rs.wasNull()) {
return null;
}
return getValue(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object value = cs.getObject(columnIndex);
if (value != null && cs.wasNull()) {
return null;
}
return getValue(value);
}
private E getValue(Object value) {
try {
return Arrays.stream(enums).filter(e -> Objects.equals(value,e.getValue())).findFirst().orElse(null);
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getSimpleName() + " by value["+value+"]", ex);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# OcloudEnumTypeHandler
类的工作原理
- 构造函数:
- 接受一个枚举类型
Class<E>
作为参数。 - 检查传入的类型是否为
null
,如果是,则抛出异常。 - 获取这个枚举类型的所有常量(即枚举值),存储在
enums
数组中。
- 接受一个枚举类型
- 设置参数(写入数据库):
- 当需要将枚举类型的数据写入数据库时,会调用
setNonNullParameter
方法。 - 这个方法使用枚举的
getValue()
方法来获取对应的业务值,然后将这个值作为SQL命令的参数。
- 当需要将枚举类型的数据写入数据库时,会调用
- 获取结果(从数据库读取):
- 当从数据库读取数据转换成枚举类型时,会使用
getNullableResult
方法。 - 这些方法从结果集中获取指定列的值。
- 如果该值非空,将会调用
getValue
方法来将这个值转换回对应的枚举类型。 getValue
方法遍历所有枚举常量,寻找其业务值与从数据库获取的值匹配的枚举项。
- 当从数据库读取数据转换成枚举类型时,会使用
# 配置 MyBatis-Plus 类型处理器
在 application.yml
中添加配置:
mybatis-plus:
configuration:
default-enum-type-handler: com.olight.cloud.mybatis.type.OcloudEnumTypeHandler
2
3
# 枚举支持出参入参、数据库处理
- 只需要实现
IEnum
与IEnumToJson
接口即可
@Getter
@AllArgsConstructor
public enum LogTypeEnum implements IEnum<Integer>, IEnumToJson {
/**
* 登录日志
*/
LOGIN(0),
/**
* 操作日志
*/
OPERATION(1),
/**
* 异常日志
*/
ERROR(2);
private final int code;
@Override
public Integer getValue() {
return code;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 总结
通过上述设计,我们可以全面地解决枚举类型在 Java 应用中的序列化、反序列化及数据库交互问题。这不仅提高了代码的可维护性,也提高了开发效率。
# 更多思考
除了使用 MyBatis-Plus 和 Jackson,其他技术如 JPA Attribute Converter 也可以实现类似功能,提供不同的优势,如更紧密的集成到 JPA 生命周期等。开发者可以根据项目具体需求选择最合适的技术路径。
希望本文能帮助您更好地理解和应用枚举类型的处理策略,更高效地开发和维护您的 Java 应用。