从零开始 Spring Boot 16:枚举
在开发Web应用时,无法避免的是会定义一些“离散值”,比如书籍类型,包含艺术、小说、工程书籍等。在数据库中,我们一般会使用整数来表示这些值,比如1代表小说,2代表艺术,3代表工程相关书籍等。但在代码中使用整数来表示类型可读性就很差了,以前一般使用类常量来表示这些值,但更好的做法是使用枚举。
关于Java中枚举的基本知识,可以阅读Java编程笔记19:枚举 - 魔芋红茶’s blog (icexmoon.cn)。
下面通过在我们的图书应用中引入枚举类型来说明如何在Spring Boot项目中使用枚举,以及相关的注意事项。
下面的示例代码都将由从零开始 Spring Boot 15:Http Client - 魔芋红茶’s blog (icexmoon.cn)中的最终代码修改而来,相关完整代码见learn_spring_boot/ch15 (github.com)。
为了演示,我们首先需要在项目相关的数据库中添加一个表示书籍类型的字段:
CREATE TABLE `book` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`description` text NOT NULL,
`user_id` int NOT NULL,
`type` tinyint NOT NULL DEFAULT '5' COMMENT '书籍类型 1艺术 2小说 3科幻 4历史 5其它',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb3
book
表的type
字段表示书籍类型。
IEnum
然后我们就需要考虑如何将数据库中整数读取为枚举。
因为我们项目使用了MyBatis Plus,所以可以很容易地通过其定义的通用枚举接口IEnum<T>
来实现这一点。只要让自定义枚举实现这个接口,MyBatis Plus在执行SQL后就会自动完成相关的类型转换,Entity
相关数据实体就会有相应的枚举常量。
- 关于MyBatis Plus的通用枚举的更多用法可以阅读通用枚举 | MyBatis-Plus。
- 如果使用的是“纯MyBatis”(没有集成MyBatis Plus),可以通过添加类型处理器(type handler)来完成类似的功能,具体可以参考从零开始 Spring Boot 25:MyBatis II - 红茶的个人站点 (icexmoon.cn)。
但实际使用中我发现往往需要给实现了IEnum
的枚举实现一个额外的getDesc
方法用于返回枚举的说明文字,但因为该方法不在IEnum
定义中,有时候只有IEnum
引用就很难调用该方法。因此我会定义一个扩展自IEnum
的自定义接口作为从数据库加载的枚举类型的实现接口:
public interface IDescEnum<T extends Serializable> extends IEnum<T> {
String getDesc();
}
然后就需要创建一个实现了IDescEnum
接口的枚举:
public enum BookType implements IDescEnum<Integer> {
ART(1, "艺术"), NOVEL(2, "小说"), SF(3, "科幻"), HISTORY(4, "历史"), OTHER(5, "其它");
BookType(Integer value, String desc) {
this.value = value;
this.desc = desc;
}
private Integer value;
private String desc;
@Override
public String getDesc() {
return desc;
}
@Override
public Integer getValue() {
return value;
}
}
当然还要在数据实体(DAO)中添加枚举类型:
public class Book implements Serializable {
...
private BookType type;
}
很简单,这样就可以使用MyBatis Plus提供的相关API查询数据库,查询后的Book
实体会以枚举的形式保存type
属性。当然添加和更新时也会将枚举常量转换成对应的整数来保存到数据库。
输入输出
@JsonValue
现在我们已经处理好了应用和数据库之间的枚举转换,但还需要处理HTTP请求和应用之间的枚举转换。
正常情况下我们都希望通过接口传入的枚举相关参数是整数形式。比如在添加图书的时候,需要添加图书类型,传入的参数可能是这样:
{
"name": "自由与和平",
"desc": "自由与和平",
"type": 1
}
但实际如果你将DTO中的相关属性定义为枚举,默认情况下接受的是常量的字面量,比如:
{
"name": "自由与和平",
"desc": "自由与和平",
"type": "ART"
}
当然也可以使用整数,但枚举常量对应的整数并不是我们定义的value
,而是其在enum
中的定义顺序,该值可以通过Enum.ordinal
方法获取。这个值从0开始,在上边的示例中,枚举常量ART的值是0,NOVEL是1,以此类推。
我认为这样存在一些问题,一来不直观,从enum
定义中很难确认具体的枚举常量对应的值,除非数一遍。二来这个值依赖在枚举中定义的顺序,也就是说定义后就不能随便移动位置了,否则就会将入参解析为另一个枚举常量。
实际上这些默认的将入参解析为枚举常量的行为都是依赖于Spring Boot中的JSON解析器的,Spring Boot默认的JSON解析器是Jackson,而Jackson默认对JSON中的枚举类型的解析行为就是上面介绍的那样。
所以要想改变这种默认行为,让传入参数中的枚举类型按照我们定义的IDescEnum
的value
属性进行解析,就需要调整Jackson。
幸运的是Jackson提供一个注解@JsonValue
可以很容易地做到这点:
public enum BookType implements IDescEnum<Integer> {
...
@JsonValue
private Integer value;
private String desc;
...
}
就像上面展示的,只要给枚举中的value
属性添加@JsonValue
,传入参数中的枚举类型就会自动按照其value
值转换为对应的枚举常量,此外,返回值中的枚举常量也会被转换为相应的value
值。
可以很容易编写一个简单示例来验证这一点:
@Data