在Spring Boot项目中,常常会遇到实体类的某个字段是枚举类,但是数据库/前端 使用的是枚举类的code,所以会涉及到Json 序列化与反序列化的需求,这里以一个例子来记录如何正确地使用Json注解。

项目中使用的Json工具是Jackson,以该工具为例

1. 实体类定义

1
2
3
4
5
6
7
8
9
10
public class Transaction {

@Schema(description = "Transaction ID")
@TableId(value = "id")
private Long id;

@Schema(description = "交易类型")
@JsonAlias({"transaction_type", "type"})
private TransactionType type;
}

2. 枚举类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Getter
public enum TransactionType {
BUY(1, "Buy"),
SELL(2, "Sell"),
;

private final int code;
private final String desc;

TransactionType(int code, String desc) {
this.code = code;
this.desc = desc;
}

}

3. 数据库定义

1
2
3
4
5
6
CREATE TABLE t_transaction
(
id bigint NOT NULL AUTO_INCREMENT COMMENT 'Transaction ID',
type smallint NOT NULL COMMENT '交易类型',
PRIMARY KEY (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='交易记录表';

4. 场景

4.1 实体类与数据库之间的映射处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
public enum TransactionType {
BUY(1, "Buy"),
SELL(2, "Sell"),
;

@EnumValue
private final int code;
private final String desc;

TransactionType(int code, String desc) {
this.code = code;
this.desc = desc;
}

}

@EnumValue 是 MyBatis-Plus 提供的一个注解,用于标记枚举类中的某个字段作为在数据库中存储的值。 在这个例子中,code 字段被标记为 @EnumValue,这意味着当你在数据库中存储 TransactionType 枚举时,会存储 code 字段的值。例如,如果你有一个 TransactionType 类型的字段,并且它的值是 TransactionType.BUT,那么在数据库中存储的值将是 1。 同样,当从数据库中读取这个字段的值时,MyBatis-Plus 会使用 code 字段的值来查找对应的枚举实例。例如,如果数据库中的值是 1,那么 MyBatis-Plus 会将其转换为 TransactionType.BUY。

4.2 接口响应类中的枚举

接口响应类中的枚举时如果不作任何处理,则调用.name()来序列化。

枚举序列化方式

  1. 全局配置调用.name()序列化为字符串(默认)
  2. 全局配置调用.toString()序列化为字符串
  3. 全局配置调用.ordinal()序列化为数字
  4. 全局配置调用.ordinal()序列化为数字
  5. 通过注解自定义序列化,如:@JsonValue指定序列化的属性

通过.ordinal()获得枚举常量索引进行序列化是最不推荐的方式,因为它是按照常量在枚举里定义的顺序从上到下从 0 开始计数的,在代码重构及演进过程中,很可能不小心改变了顺序,这样会造成序列化值的错乱并失去兼容性!

1
2
3
4
5
6
7
// 接口响应示例如下:
"data": {
"transaction": {
"id": 1,
"type": "BUY",
}
}

但是,前端有时候不需要字符串类型的type, 只需要type对应的code值,或者枚举类。这时候就需要用到额外的注解。

4.2.1 @JsonValue注解指定序列化属性,接口响应返回code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Getter
public enum TransactionType {
BUY(1, "Buy"),
SELL(2, "Sell"),
;

@EnumValue
private final int code;
private final String desc;

TransactionType(int code, String desc) {
this.code = code;
this.desc = desc;
}

@JsonValue
public Integer getCode() {
return code;
}

}
1
2
3
4
5
6
7
// 接口响应示例如下:
"data": {
"transaction": {
"id": 1,
"type": 1,
}
}

4.2.2 @JsonFormat(shape = JsonFormat.Shape.*OBJECT*)注解也可以指定序列化方式,该注解使得枚举类被序列化为Json对象。但是该注解优先级低于@JsonValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum TransactionType {
BUY(1, "Buy"),
SELL(2, "Sell"),
;

@EnumValue
private final int code;
private final String desc;

TransactionType(int code, String desc) {
this.code = code;
this.desc = desc;
}

}
1
2
3
4
5
6
7
8
9
10
// 接口响应示例如下:
"data": {
"transaction": {
"id": 1,
"type": {
"code": 1,
"desc": "Buy"
},
}
}

4.3 枚举类中的反序列化

有时前端会传枚举类的code值(或者Json对象)到接口,后端需要根据code值查到枚举类,这里需要@JsonCreator。Jackson 库可以通过默认的构造函数来创建对象实例,然后通过反射来设置对象的属性。但是,如果对象的构造函数不是默认的或者带有参数,Jackson 就无法直接使用默认的方式来创建对象实例了。@JsonCreator 的作用就是告诉 Jackson 序列化/反序列化时,应该使用标记了该注解的构造函数或者静态工厂方法来创建对象实例。通常情况下,@JsonCreator 注解会和 @JsonProperty 注解一起使用,@JsonProperty 用来标识 JSON 中的属性名,@JsonCreator 用来标识构造函数或静态工厂方法,以便正确地将 JSON 数据映射到 Java 对象的属性上。

1
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
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum TransactionType {
BUY(1, "Buy"),
SELL(2, "Sell"),
;

@EnumValue
private final int code;
private final String desc;

TransactionType(int code, String desc) {
this.code = code;
this.desc = desc;
}

private static final Map ALL_TRANSACTION_TYPE = Stream.of(TransactionType.values())
.collect(Collectors.toMap(TransactionType::getCode, Function.identity()));

@JsonCreator
public static TransactionType des(final JsonNode node) {
return Optional.ofNullable(node.get("code")) // Json对象取code值
.or(() -> Optional.of(node)) // 或者直接用code值
.map(JsonNode::asInt)
.map(ALL_TRANSACTION_TYPE::get)
.orElseThrow(() -> new GkException(GkError.TRANSACTION_TYPE_ERROR));
}

}