在业务系统中,状态、类型、来源、支付方式等字段非常适合使用 Java 枚举。
但一个枚举字段从数据库到前端,实际上会经过多层类型系统:
1 2 3 4 数据库 Java/JPA Spring MVC / Jackson 前端 INT <----> ItemType <----> 1 / "1" <----> number 1 APARTMENT 1 1 2 ROOM 2 2
如果没有统一设计,项目里很容易出现这些问题:
数据库存 1,Java 却收到 APARTMENT
GET ?type=1 无法转换成枚举
POST {"type":1} 反序列化失败
JPA 默认把枚举存成序号或字符串
前端、后端、数据库分别维护不同的枚举含义
新增枚举值以后,到处写 if/else
无效枚举值有的返回 null,有的直接报 500
本文参考一篇关于 Spring MVC、MyBatis-Plus 与枚举转换的文章,将其中的思路改造成一套适用于:
Spring Boot 3 + Spring MVC + Jackson + Spring Data JPA / MyBatis-Plus + Vue/TypeScript
的完整实现。
1. 先确定枚举字段的统一约定 本文采用下面的约定:
层
表示形式
数据库
INT,例如 1、2
Java Entity / DTO
ItemType 枚举
URL Query / Path 参数
"1"、"2"
JSON 请求
1、2
JSON 响应
1、2
Vue / TypeScript
number
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public enum ItemType { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); private final Integer code; private final String description; ItemType(Integer code, String description) { this .code = code; this .description = description; } public Integer getCode () { return code; } public String getDescription () { return description; } }
前端永远只需要关心:
业务代码内部则可以直接写:
1 2 3 if (item.getType() == ItemType.APARTMENT) { }
而不是:
1 2 3 if (item.getType() == 1 ) { }
这就是本文要实现的目标。
2. 为什么不直接使用 @Enumerated JPA 原生支持:
1 2 @Enumerated(EnumType.ORDINAL) private ItemType type;
或者:
1 2 @Enumerated(EnumType.STRING) private ItemType type;
但是对于业务枚举,我通常不推荐 ORDINAL。
假设原来的枚举是:
1 2 3 4 public enum ItemType { APARTMENT, ROOM }
数据库保存:
1 2 APARTMENT -> 0 ROOM -> 1
后来有人调整成:
1 2 3 4 5 public enum ItemType { SHOP, APARTMENT, ROOM }
此时:
1 2 3 SHOP -> 0 APARTMENT -> 1 ROOM -> 2
历史数据的含义直接发生变化。
这类问题属于典型的“代码跑得很好,数据已经悄悄坏了”。
EnumType.STRING 比 ORDINAL 安全,但数据库会保存:
如果希望数据库、API 都使用稳定的业务 code:
更适合使用:
1 AttributeConverter<ItemType, Integer>
另外需要注意:
使用 AttributeConverter 的枚举字段不要再同时标注 @Enumerated。
JPA 对 @Enumerated 与属性转换器同时使用并没有定义为可移植的组合。
3. 数据库设计 示例表:
1 2 3 4 5 6 7 8 9 10 CREATE TABLE label_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键' , type INT NOT NULL COMMENT '类型:1-公寓,2-房间' , name VARCHAR (100 ) NOT NULL COMMENT '标签名称' , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (id), INDEX idx_label_info_type (type) ) COMMENT = '标签信息' ;
测试数据:
1 2 3 4 INSERT INTO label_info(type, name)VALUES (1 , '近地铁' ), (1 , '可做饭' ), (2 , '独立卫生间' );
这里不建议直接使用 MySQL 的 ENUM 类型。
业务枚举应该由 Java 代码和数据库迁移脚本共同管理,数据库字段保持普通数字类型即可。
4. 定义统一的枚举接口 项目中不会只有一个枚举,所以先抽象公共接口。
1 2 3 4 5 6 7 8 package com.example.demo.enums;public interface BaseCodeEnum { Integer getCode () ; String getDescription () ; }
所有需要参与“前端、Spring MVC、Jackson、JPA”转换的枚举都实现这个接口。
5. 定义统一的枚举查找工具 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 package com.example.demo.enums;import java.util.Arrays;public final class EnumUtils { private EnumUtils () { } public static <E extends Enum <E> & BaseCodeEnum> E fromCode ( Class<E> enumType, Integer code) { if (code == null ) { return null ; } return Arrays.stream(enumType.getEnumConstants()) .filter(item -> item.getCode().equals(code)) .findFirst() .orElseThrow(() -> new IllegalArgumentException ( "非法枚举值,enum=" + enumType.getSimpleName() + ", code=" + code ) ); } }
这里有一个很重要的原则:
遇到未知 code 时不要静默返回 null。
例如数据库中出现:
如果 99 已经不是合法业务值,最好立即暴露数据问题,而不是把它悄悄转换成 null,然后在更远的地方产生一个莫名其妙的 NPE。
6. 定义业务枚举 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 30 31 32 33 34 package com.example.demo.enums;import com.fasterxml.jackson.annotation.JsonCreator;import com.fasterxml.jackson.annotation.JsonValue;public enum ItemType implements BaseCodeEnum { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); private final Integer code; private final String description; ItemType(Integer code, String description) { this .code = code; this .description = description; } @Override @JsonValue public Integer getCode () { return code; } @Override public String getDescription () { return description; } @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public static ItemType fromCode (Integer code) { return EnumUtils.fromCode(ItemType.class, code); } }
这里的两个 Jackson 注解分别处理:
负责:
例如:
序列化为:
而不是默认的:
@JsonCreator 负责:
例如:
转换成:
所以 JSON 的完整转换路径是:
1 2 3 4 5 6 7 8 9 JSON 1 | | Jackson @JsonCreator v ItemType.APARTMENT | | Jackson @JsonValue v JSON 1
7. Spring MVC 中 URL 参数为什么还需要 Converter 这里很容易混淆。
下面这个请求:
Controller:
1 2 3 4 @GetMapping public List<LabelResponse> list ( @RequestParam(required = false) ItemType type) { }
这里的 type=1 并不是 JSON。
它是 HTTP Query Parameter,本质上先是:
Spring MVC 需要完成:
1 2 3 String "1" -> ItemType.APARTMENT
这套转换走的是 Spring 的 ConversionService,而不是 Jackson。
默认情况下,Spring 对字符串到枚举的转换更适合:
1 "APARTMENT" -> ItemType.APARTMENT
但我们的 API 希望:
1 "1" -> ItemType.APARTMENT
因此需要自定义 ConverterFactory。
8. 使用 ConverterFactory 统一处理所有枚举 不要为每一个枚举都写:
1 2 3 4 StringToItemTypeConverter StringToOrderStatusConverter StringToPayTypeConverter StringToSourceTypeConverter
因为转换逻辑完全一样。
Spring 提供的 ConverterFactory 就是解决这个问题的。
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 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 package com.example.demo.web.converter;import com.example.demo.enums.BaseCodeEnum;import org.springframework.core.convert.converter.Converter;import org.springframework.core.convert.converter.ConverterFactory;public class StringToCodeEnumConverterFactory implements ConverterFactory <String, BaseCodeEnum> { @Override public <T extends BaseCodeEnum > Converter<String, T> getConverter ( Class<T> targetType) { if (!targetType.isEnum()) { throw new IllegalArgumentException ( targetType.getName() + " 不是枚举类型" ); } return source -> convert(source, targetType); } private <T extends BaseCodeEnum > T convert ( String source, Class<T> targetType) { if (source == null || source.isBlank()) { return null ; } final Integer code; try { code = Integer.valueOf(source); } catch (NumberFormatException e) { throw new IllegalArgumentException ( "非法枚举值:" + source, e ); } T[] enumConstants = targetType.getEnumConstants(); for (T enumConstant : enumConstants) { if (enumConstant.getCode().equals(code)) { return enumConstant; } } throw new IllegalArgumentException ( "非法枚举值,enum=" + targetType.getSimpleName() + ", code=" + code ); } }
9. 注册 Spring MVC ConverterFactory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.example.demo.config;import com.example.demo.web.converter.StringToCodeEnumConverterFactory;import org.springframework.context.annotation.Configuration;import org.springframework.format.FormatterRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebMvcConfiguration implements WebMvcConfigurer { @Override public void addFormatters (FormatterRegistry registry) { registry.addConverterFactory( new StringToCodeEnumConverterFactory () ); } }
现在:
可以自动转换成:
而:
自动转换成:
这套 Converter 同样可以服务于:
1 2 @RequestParam @PathVariable
例如:
1 2 3 4 @GetMapping("/type/{type}") public List<LabelResponse> listByType ( @PathVariable ItemType type) { }
调用:
Spring MVC 会得到:
10. Spring MVC Converter 和 Jackson 不是一回事 这是整个设计里最需要记住的一点。
URL 参数
转换链:
1 2 3 4 5 6 7 8 9 10 11 HTTP Query String | v Spring MVC ConversionService | v ConverterFactory | v ItemType.APARTMENT
JSON 请求体 1 2 POST /api/labelsContent-Type : application/json
1 2 3 4 { "type" : 1 , "name" : "近地铁" }
转换链:
1 2 3 4 5 6 7 8 9 10 11 12 13 JSON | v HttpMessageConverter | v Jackson ObjectMapper | v @JsonCreator | v ItemType.APARTMENT
所以:
ConverterFactory 解决不了 @RequestBody 的 JSON 枚举反序列化。
反过来也一样:
@JsonCreator 不能替代 @RequestParam 的 Spring MVC 类型转换。
两套机制职责不同。
11. 使用 JPA AttributeConverter 做数据库转换 接下来解决:
1 2 3 数据库 INT <-> Java ItemType
JPA 标准提供了:
1 AttributeConverter<X, Y>
其中:
1 2 X = Java Entity 属性类型 Y = 数据库列对应的基础类型
对于当前场景:
1 2 X = ItemType Y = Integer
12. 抽象一个 JPA 枚举 Converter 基类 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 package com.example.demo.jpa.converter;import com.example.demo.enums.BaseCodeEnum;import com.example.demo.enums.EnumUtils;import jakarta.persistence.AttributeConverter;public abstract class AbstractCodeEnumJpaConverter < E extends Enum <E> & BaseCodeEnum> implements AttributeConverter <E, Integer> { private final Class<E> enumType; protected AbstractCodeEnumJpaConverter (Class<E> enumType) { this .enumType = enumType; } @Override public Integer convertToDatabaseColumn (E attribute) { return attribute == null ? null : attribute.getCode(); } @Override public E convertToEntityAttribute (Integer dbData) { return EnumUtils.fromCode(enumType, dbData); } }
以后每个业务枚举只需要写一个非常薄的 Converter。
13. ItemType 的 JPA Converter 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.example.demo.jpa.converter;import com.example.demo.enums.ItemType;import jakarta.persistence.Converter;@Converter(autoApply = true) public class ItemTypeJpaConverter extends AbstractCodeEnumJpaConverter <ItemType> { public ItemTypeJpaConverter () { super (ItemType.class); } }
autoApply = true 表示:
Entity 中所有 ItemType 类型的普通持久化属性,默认自动使用该 Converter。
于是实体字段不需要再重复写:
1 @Convert(converter = ItemTypeJpaConverter.class)
当然,如果项目更喜欢“显式优于隐式”,也可以写成:
1 2 @Converter(autoApply = false) public class ItemTypeJpaConverter ...
然后实体上:
1 2 @Convert(converter = ItemTypeJpaConverter.class) private ItemType type;
二选一即可。
不要同时再加:
14. Entity 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 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 59 60 61 62 63 package com.example.demo.entity;import com.example.demo.enums.ItemType;import jakarta.persistence.*;import java.time.LocalDateTime;@Entity @Table(name = "label_info") public class LabelInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "type", nullable = false) private ItemType type; @Column(name = "name", nullable = false, length = 100) private String name; @Column(name = "create_time", nullable = false) private LocalDateTime createTime; @Column(name = "update_time", nullable = false) private LocalDateTime updateTime; public Long getId () { return id; } public ItemType getType () { return type; } public void setType (ItemType type) { this .type = type; } public String getName () { return name; } public void setName (String name) { this .name = name; } public LocalDateTime getCreateTime () { return createTime; } public void setCreateTime (LocalDateTime createTime) { this .createTime = createTime; } public LocalDateTime getUpdateTime () { return updateTime; } public void setUpdateTime (LocalDateTime updateTime) { this .updateTime = updateTime; } }
Entity 中依然使用强类型:
而不是:
这样业务代码就不会到处出现:
15. Spring Data JPA Repository 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.example.demo.repository;import com.example.demo.entity.LabelInfo;import com.example.demo.enums.ItemType;import org.springframework.data.jpa.repository.JpaRepository;import java.util.List;public interface LabelInfoRepository extends JpaRepository <LabelInfo, Long> { List<LabelInfo> findByType (ItemType type) ; }
查询时可以直接传枚举:
1 repository.findByType(ItemType.APARTMENT);
不用手工:
1 repository.findByType(1 );
业务层不应该知道数据库使用的是 1 还是 2。
数据库存储细节应该由 JPA Converter 隔离掉。
16. MyBatis-Plus 方案:使用 @EnumValue Spring Data JPA 通过:
1 AttributeConverter<ItemType, Integer>
解决:
而 MyBatis-Plus 对业务枚举提供了更直接的支持。
最常用的方式是在数据库值对应的枚举字段上添加:
例如:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.example.demo.enums;import com.baomidou.mybatisplus.annotation.EnumValue;import com.fasterxml.jackson.annotation.JsonCreator;import com.fasterxml.jackson.annotation.JsonValue;public enum ItemType implements BaseCodeEnum { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); @EnumValue private final Integer code; private final String description; ItemType(Integer code, String description) { this .code = code; this .description = description; } @Override @JsonValue public Integer getCode () { return code; } @Override public String getDescription () { return description; } @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public static ItemType fromCode (Integer code) { return EnumUtils.fromCode(ItemType.class, code); } }
于是同一个字段上的职责可以理解为:
1 2 3 4 5 6 7 8 9 10 11 @EnumValue | +---- MyBatis-Plus <-> DB @JsonValue / @JsonCreator | +---- Jackson <-> JSON Spring ConverterFactory | +---- Query / Path <-> Java Enum
例如:
写入数据库时:
数据库读取:
转换回:
这和 JPA 的:
1 AttributeConverter<ItemType, Integer>
解决的是同一个层面的问题,只是实现机制不同。
17. MyBatis-Plus 方案二:实现 IEnum MyBatis-Plus 还支持让枚举实现:
例如:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.example.demo.enums;import com.baomidou.mybatisplus.annotation.IEnum;import com.fasterxml.jackson.annotation.JsonCreator;import com.fasterxml.jackson.annotation.JsonValue;public enum ItemType implements BaseCodeEnum , IEnum<Integer> { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); private final Integer code; private final String description; ItemType(Integer code, String description) { this .code = code; this .description = description; } @Override @JsonValue public Integer getCode () { return code; } @Override public String getDescription () { return description; } @Override public Integer getValue () { return code; } @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public static ItemType fromCode (Integer code) { return EnumUtils.fromCode(ItemType.class, code); } }
@EnumValue 和 IEnum 二选一即可。
个人更推荐:
原因很简单:
1 2 3 侵入更小 代码更少 一眼就能看出哪个字段是数据库值
IEnum 更适合项目已经统一要求所有 MyBatis-Plus 枚举实现同一个接口的情况。
需要注意的是,这两种方案都会让枚举依赖 MyBatis-Plus。
如果你的领域层要求完全不依赖 ORM 框架,可以继续使用:
然后在 MyBatis 中编写自己的:
不过普通业务系统没有必要为了“绝对纯洁”多写一大堆模板代码,@EnumValue 通常已经足够实用。
18. MyBatis-Plus Entity MyBatis-Plus 实体可以直接把字段定义成枚举:
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 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 59 60 61 62 63 package com.example.demo.entity;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import com.example.demo.enums.ItemType;import java.time.LocalDateTime;@TableName("label_info") public class LabelInfoMp { @TableId(type = IdType.AUTO) private Long id; private ItemType type; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public ItemType getType () { return type; } public void setType (ItemType type) { this .type = type; } public String getName () { return name; } public void setName (String name) { this .name = name; } public LocalDateTime getCreateTime () { return createTime; } public void setCreateTime (LocalDateTime createTime) { this .createTime = createTime; } public LocalDateTime getUpdateTime () { return updateTime; } public void setUpdateTime (LocalDateTime updateTime) { this .updateTime = updateTime; } }
数据库仍然是:
Java 中仍然是:
这点和 JPA 的最终效果一致:
1 2 3 4 DB Java -------------------------------- 1 <-> APARTMENT 2 <-> ROOM
19. MyBatis-Plus Mapper Mapper:
1 2 3 4 5 6 7 8 9 10 package com.example.demo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.demo.entity.LabelInfoMp;import org.apache.ibatis.annotations.Mapper;@Mapper public interface LabelInfoMapper extends BaseMapper <LabelInfoMp> { }
插入:
1 2 3 4 5 LabelInfoMp label = new LabelInfoMp (); label.setType(ItemType.APARTMENT); label.setName("近地铁" ); labelInfoMapper.insert(label);
Java 中传入的是:
MyBatis-Plus 根据:
1 2 @EnumValue private final Integer code;
最终写入:
1 2 INSERT INTO label_info(type, name)VALUES (1 , '近地铁' );
查询数据库得到:
又会映射回:
因此业务层完全不需要自己写:
1 ItemType.fromCode(entity.getType())
ORM 层已经完成了转换。
20. MyBatis-Plus LambdaQueryWrapper 查询枚举 查询条件也应该直接使用枚举:
1 2 3 4 5 6 7 List<LabelInfoMp> list = labelInfoMapper.selectList( new LambdaQueryWrapper <LabelInfoMp>() .eq( LabelInfoMp::getType, ItemType.APARTMENT ) );
不要为了数据库是 INT 就退化成:
推荐始终保持:
1 2 3 4 .eq( LabelInfoMp::getType, ItemType.APARTMENT )
动态条件:
1 2 3 4 5 6 7 8 9 10 11 public List<LabelInfoMp> list (ItemType type) { return labelInfoMapper.selectList( new LambdaQueryWrapper <LabelInfoMp>() .eq( type != null , LabelInfoMp::getType, type ) ); }
Controller:
1 2 3 4 5 6 @GetMapping public List<LabelInfoMp> list ( @RequestParam(required = false) ItemType type) { return service.list(type); }
调用:
Spring MVC 首先转换:
1 2 3 4 "1" | v ItemType.APARTMENT
MyBatis-Plus 再转换:
1 2 3 4 ItemType.APARTMENT | v 1
最终 SQL:
1 2 3 4 5 6 7 8 SELECT id, type, name, create_time, update_timeFROM label_infoWHERE type = 1 ;
21. MyBatis-Plus XML 查询 MyBatis-Plus 最大的优势之一,是复杂报表或动态 SQL 场景下仍然可以自然使用 MyBatis XML。
Mapper:
1 2 3 4 5 6 7 8 @Mapper public interface LabelInfoMapper extends BaseMapper <LabelInfoMp> { List<LabelInfoMp> selectByType ( @Param("type") ItemType type ) ; }
XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.example.demo.mapper.LabelInfoMapper" > <select id ="selectByType" resultType ="com.example.demo.entity.LabelInfoMp" > SELECT id, type, name, create_time, update_time FROM label_info WHERE type = #{type} </select > </mapper >
Java 调用:
1 mapper.selectByType(ItemType.APARTMENT);
业务代码传的是:
数据库参数仍然按声明的枚举值处理。
如果 SQL 比较复杂:
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 <select id ="selectList" resultType ="com.example.demo.entity.LabelInfoMp" > SELECT id, type, name, create_time, update_time FROM label_info <where > <if test ="type != null" > AND type = #{type} </if > <if test ="name != null and name != ''" > AND name LIKE CONCAT('%', #{name}, '%') </if > </where > ORDER BY id DESC</select >
Java 层仍然不需要出现:
更不需要:
这种设计对复杂报表场景尤其友好:
1 2 3 SQL 的复杂性留在 SQL 层 枚举的业务语义留在 Java 层 枚举与数据库 code 的转换交给 TypeHandler
22. MyBatis-Plus 为什么能自动转换 MyBatis 本身有:
1 2 EnumTypeHandler EnumOrdinalTypeHandler
分别可以按:
进行持久化。
MyBatis-Plus 在此基础上提供:
用于处理声明过业务值的枚举。
例如:
1 2 @EnumValue private final Integer code;
或者:
1 implements IEnum <Integer>
MyBatis-Plus 就知道真正应该写入数据库的是:
而不是:
也不是:
在当前 MyBatis-Plus 版本中,通用枚举可以直接使用,不需要再配置旧版本中的:
1 2 mybatis-plus: type-enums-package: xxx
这个配置已经不是新版本的推荐方式。
因此现代项目里通常只需要:
即可完成:
23. Spring Boot 3 引入 MyBatis-Plus Spring Boot 3 项目使用对应的 Boot 3 Starter:
1 2 3 4 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-spring-boot3-starter</artifactId > </dependency >
如果你的项目已经通过:
或者公司统一 BOM 管理版本,这里不需要在每个业务模块重复写版本号。
Spring Data JPA 则通常使用:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jpa</artifactId > </dependency >
二者并不是互斥的技术。
同一个大型系统里完全可能存在:
1 2 3 4 5 6 7 8 9 简单 CRUD / 聚合关系 | v Spring Data JPA 复杂报表 / 多表动态 SQL | v MyBatis-Plus
关键不是强迫整个系统只能选一个 ORM,而是让枚举协议保持一致。
24. 同一个项目同时使用 JPA 和 MyBatis-Plus 怎么办 有些项目会同时存在:
1 2 3 Spring Data JPA + MyBatis-Plus
这时有两种做法。
24.1 方案一:同一个枚举同时服务两个 ORM 可以:
1 2 3 4 5 6 7 8 9 10 public enum ItemType implements BaseCodeEnum { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); @EnumValue private final Integer code; ... }
JPA:
1 2 3 4 5 6 7 8 @Converter(autoApply = true) public class ItemTypeJpaConverter extends AbstractCodeEnumJpaConverter <ItemType> { public ItemTypeJpaConverter () { super (ItemType.class); } }
于是:
1 2 3 4 5 6 7 8 9 ItemType.code | +---- @EnumValue ------------> MyBatis-Plus | +---- AttributeConverter ----> JPA | +---- @JsonValue ------------> JSON Response | +---- @JsonCreator ----------> JSON Request
这种方案最省代码。
缺点是:
会直接依赖:
1 com.baomidou.mybatisplus.annotation.EnumValue
24.2 方案二:领域枚举完全不依赖 ORM 枚举只保留:
1 2 public enum ItemType implements BaseCodeEnum { }
然后:
1 2 3 4 5 6 7 JPA | +--> AttributeConverter MyBatis / MyBatis-Plus | +--> 自定义 TypeHandler
这种方式架构边界更干净。
适合:
Domain 模块要求零 ORM 依赖
枚举定义放在独立公共模块
同一个 Domain 被多个持久化实现复用
对六边形架构 / Clean Architecture 边界要求较高
普通后台业务系统则没有必要过度设计。
25. JPA 与 MyBatis-Plus 枚举映射对比 两种方案实现的最终目标完全一致:
区别主要在实现方式。
对比项
Spring Data JPA
MyBatis-Plus
推荐机制
AttributeConverter
@EnumValue / IEnum
Java 字段
ItemType
ItemType
DB 字段
INT
INT
查询条件
直接传枚举
直接传枚举
每个枚举额外代码
通常需要一个 Converter
@EnumValue 基本即可
ORM 侵入枚举
可以做到无侵入
@EnumValue / IEnum 会依赖 MP
CRUD
Repository 很方便
BaseMapper 很方便
动态 SQL
Specification / Criteria 等
Wrapper / XML 更直接
复杂 SQL
通常需要 JPQL / Native SQL
XML 非常自然
ORM 标准化
Jakarta Persistence 标准
MyBatis-Plus 框架能力
映射控制
ORM 属性转换
TypeHandler 体系
适合场景
领域模型、常规 CRUD
SQL 驱动、复杂查询、报表
如果项目本身就是:
推荐:
如果项目本身就是:
推荐:
如果项目两者都有,则完全可以共用:
这个枚举业务协议。
26. 两种 ORM 下完整链路对比 Spring Data JPA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GET ?type=1 | v Spring MVC ConverterFactory | v ItemType.APARTMENT | v Repository | v AttributeConverter | v DB type = 1
读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 DB type = 1 | v AttributeConverter | v ItemType.APARTMENT | v Jackson @JsonValue | v JSON 1
MyBatis-Plus 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 GET ?type=1 | v Spring MVC ConverterFactory | v ItemType.APARTMENT | v BaseMapper / Wrapper / XML | v MybatisEnumTypeHandler | v @EnumValue code = 1 | v DB type = 1
读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 DB type = 1 | v MybatisEnumTypeHandler | v ItemType.APARTMENT | v Jackson @JsonValue | v JSON 1
可以发现:
这三层根本不用关心底层到底是:
还是:
变化只发生在 Persistence Layer。
这也是比较合理的分层方式。
27. DTO 建议 Controller 不要直接把 Entity 暴露给前端。
请求:
1 2 3 4 5 6 7 8 9 package com.example.demo.dto;import com.example.demo.enums.ItemType;public record CreateLabelRequest ( ItemType type, String name ) { }
响应:
1 2 3 4 5 6 7 8 9 10 package com.example.demo.dto;import com.example.demo.enums.ItemType;public record LabelResponse ( Long id, ItemType type, String name ) { }
由于 ItemType#getCode() 上存在:
所以:
1 2 3 4 5 new LabelResponse ( 1L , ItemType.APARTMENT, "近地铁" );
最终 JSON 是:
1 2 3 4 5 { "id" : 1 , "type" : 1 , "name" : "近地铁" }
而不是:
1 2 3 4 5 { "id" : 1 , "type" : "APARTMENT" , "name" : "近地铁" }
28. Service 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 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 package com.example.demo.service;import com.example.demo.dto.CreateLabelRequest;import com.example.demo.dto.LabelResponse;import com.example.demo.entity.LabelInfo;import com.example.demo.enums.ItemType;import com.example.demo.repository.LabelInfoRepository;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;import java.util.List;@Service public class LabelInfoService { private final LabelInfoRepository repository; public LabelInfoService (LabelInfoRepository repository) { this .repository = repository; } @Transactional(readOnly = true) public List<LabelResponse> list (ItemType type) { List<LabelInfo> entities = type == null ? repository.findAll() : repository.findByType(type); return entities.stream() .map(this ::toResponse) .toList(); } @Transactional public LabelResponse create (CreateLabelRequest request) { LabelInfo entity = new LabelInfo (); entity.setType(request.type()); entity.setName(request.name()); entity.setCreateTime(LocalDateTime.now()); entity.setUpdateTime(LocalDateTime.now()); return toResponse(repository.save(entity)); } private LabelResponse toResponse (LabelInfo entity) { return new LabelResponse ( entity.getId(), entity.getType(), entity.getName() ); } }
29. Controller 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 30 31 32 33 34 package com.example.demo.controller;import com.example.demo.dto.CreateLabelRequest;import com.example.demo.dto.LabelResponse;import com.example.demo.enums.ItemType;import com.example.demo.service.LabelInfoService;import org.springframework.web.bind.annotation.*;import java.util.List;@RestController @RequestMapping("/api/labels") public class LabelInfoController { private final LabelInfoService service; public LabelInfoController (LabelInfoService service) { this .service = service; } @GetMapping public List<LabelResponse> list ( @RequestParam(required = false) ItemType type) { return service.list(type); } @PostMapping public LabelResponse create ( @RequestBody CreateLabelRequest request) { return service.create(request); } }
30. GET 请求完整转换过程 前端请求:
第一步,HTTP 中的:
本质是字符串:
Spring MVC:
1 2 3 4 5 6 7 8 9 10 @RequestParam | v ConversionService | v StringToCodeEnumConverterFactory | v ItemType.APARTMENT
Controller 得到:
调用:
1 repository.findByType(ItemType.APARTMENT);
JPA:
1 2 3 4 5 6 7 ItemType.APARTMENT | v ItemTypeJpaConverter | v 1
最终 SQL 的逻辑类似:
1 2 3 4 5 6 7 8 SELECT id, type, name, create_time, update_timeFROM label_infoWHERE type = 1 ;
查询结果:
JPA 再通过:
转换回:
最后 Jackson:
再次把:
输出为:
最终响应:
1 2 3 4 5 6 7 8 9 10 11 12 [ { "id" : 1 , "type" : 1 , "name" : "近地铁" } , { "id" : 2 , "type" : 1 , "name" : "可做饭" } ]
整个链路:
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 前端 type=1 | v Spring MVC ConverterFactory | v ItemType.APARTMENT | v Spring Data JPA | v AttributeConverter | v DB type=1 | v AttributeConverter | v ItemType.APARTMENT | v Jackson @JsonValue | v JSON type=1
31. POST 请求完整转换过程 请求:
1 2 POST /api/labelsContent-Type : application/json
1 2 3 4 { "type" : 2 , "name" : "独立卫生间" }
Jackson 读取:
调用:
得到:
Controller 实际收到:
1 2 3 4 CreateLabelRequest( ItemType.ROOM, "独立卫生间" )
保存:
1 entity.setType(ItemType.ROOM);
JPA Converter:
数据库:
1 2 INSERT INTO label_info(type, name)VALUES (2 , '独立卫生间' );
响应阶段:
1 2 3 4 5 6 7 ItemType.ROOM | v @JsonValue | v 2
最终:
1 2 3 4 5 { "id" : 3 , "type" : 2 , "name" : "独立卫生间" }
32. Vue / TypeScript 如何定义 前端也不要到处直接写:
可以统一定义:
1 2 3 4 5 6 7 export const ItemType = { APARTMENT : 1 , ROOM : 2 } as const export type ItemType = typeof ItemType [keyof typeof ItemType ]
下拉选项:
1 2 3 4 5 6 7 8 9 10 export const itemTypeOptions = [ { label : '公寓' , value : ItemType .APARTMENT }, { label : '房间' , value : ItemType .ROOM } ]
查询:
1 2 3 4 5 6 7 import axios from 'axios' axios.get ('/api/labels' , { params : { type : ItemType .APARTMENT } })
浏览器发送:
新增:
1 2 3 4 axios.post ('/api/labels' , { type : ItemType .ROOM , name : '独立卫生间' })
请求:
1 2 3 4 { "type" : 2 , "name" : "独立卫生间" }
这样前后端协议始终保持数字 code。
33. 如果前端还需要中文名称怎么办 有两种方式。
方案一:前端自己维护枚举字典 适合非常稳定的枚举:
1 2 3 4 export const itemTypeOptions = [ { label : '公寓' , value : 1 }, { label : '房间' , value : 2 } ]
优点:
缺点:
方案二:后端提供枚举字典接口 定义:
1 2 3 4 5 public record EnumOption ( Integer value, String label ) { }
Controller:
1 2 3 4 5 6 7 8 9 10 @GetMapping("/item-types") public List<EnumOption> itemTypes () { return Arrays.stream(ItemType.values()) .map(item -> new EnumOption ( item.getCode(), item.getDescription() )) .toList(); }
响应:
1 2 3 4 5 6 7 8 9 10 [ { "value" : 1 , "label" : "公寓" } , { "value" : 2 , "label" : "房间" } ]
前端:
1 const { data } = await axios.get ('/api/labels/item-types' )
适合:
枚举很多
后端是枚举定义的唯一事实来源
多个前端共同使用同一套 API
枚举描述可能调整
企业项目里,如果枚举数量比较多,我更推荐后端统一暴露字典接口。
34. 枚举非法值统一返回 400 如果请求:
或者:
1 2 3 4 { "type" : 99 , "name" : "错误数据" }
应该返回:
而不是 500。
Spring Boot 3 可以直接使用 ProblemDetail。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.example.demo.web;import org.springframework.http.HttpStatus;import org.springframework.http.ProblemDetail;import org.springframework.http.ResponseEntity;import org.springframework.http.converter.HttpMessageNotReadableException;import org.springframework.web.bind.MethodArgumentTypeMismatchException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<ProblemDetail> handleTypeMismatch ( MethodArgumentTypeMismatchException exception) { ProblemDetail detail = ProblemDetail.forStatus( HttpStatus.BAD_REQUEST ); detail.setTitle("请求参数错误" ); detail.setDetail( "参数 " + exception.getName() + " 的值非法:" + exception.getValue() ); return ResponseEntity .badRequest() .body(detail); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<ProblemDetail> handleMessageNotReadable ( HttpMessageNotReadableException exception) { ProblemDetail detail = ProblemDetail.forStatus( HttpStatus.BAD_REQUEST ); detail.setTitle("请求体格式错误" ); detail.setDetail("请求 JSON 中存在非法字段值" ); return ResponseEntity .badRequest() .body(detail); } }
例如:
返回:
1 2 3 4 5 6 { "type" : "about:blank" , "title" : "请求参数错误" , "status" : 400 , "detail" : "参数 type 的值非法:99" }
生产环境不建议直接把底层异常堆栈或 Java 类名返回给前端。
35. 一个完整枚举最终长什么样 最终一个普通业务枚举只需要:
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 30 31 32 33 34 package com.example.demo.enums;import com.fasterxml.jackson.annotation.JsonCreator;import com.fasterxml.jackson.annotation.JsonValue;public enum ItemType implements BaseCodeEnum { APARTMENT(1 , "公寓" ), ROOM(2 , "房间" ); private final Integer code; private final String description; ItemType(Integer code, String description) { this .code = code; this .description = description; } @Override @JsonValue public Integer getCode () { return code; } @Override public String getDescription () { return description; } @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public static ItemType fromCode (Integer code) { return EnumUtils.fromCode(ItemType.class, code); } }
再配一个 JPA Converter:
1 2 3 4 5 6 7 8 @Converter(autoApply = true) public class ItemTypeJpaConverter extends AbstractCodeEnumJpaConverter <ItemType> { public ItemTypeJpaConverter () { super (ItemType.class); } }
Spring MVC 的:
1 StringToCodeEnumConverterFactory
只需要全局定义一次。
36. 为什么 JPA 这里没有完全做成一个通用 Converter 可能有人会想到这样写:
1 2 3 4 @Converter(autoApply = true) public class BaseCodeEnumJpaConverter implements AttributeConverter <BaseCodeEnum, Integer> { }
看起来这样所有枚举都不用再写:
1 2 3 ItemTypeJpaConverter OrderStatusJpaConverter PayTypeJpaConverter
但不建议这么做。
JPA 的 AttributeConverter<X, Y> 需要根据明确的属性 Java 类型决定 Converter 是否适用。
例如:
1 AttributeConverter<ItemType, Integer>
JPA 很明确知道:
而:
1 AttributeConverter<BaseCodeEnum, Integer>
面对实体属性:
是否把“实现了接口”视为自动匹配,并不是一个应该依赖的可移植假设。
而且从数据库读到:
通用 Converter 本身也不知道应该构造:
还是:
因此更合理的做法是:
1 2 3 4 5 6 7 8 9 10 公共转换算法 | v AbstractCodeEnumJpaConverter | +--> ItemTypeJpaConverter | +--> OrderStatusJpaConverter | +--> PayTypeJpaConverter
每个枚举只增加一个几行代码的类型声明。
这点和 Spring MVC 不一样。
Spring MVC 的 ConverterFactory 在运行时能够拿到:
因此它知道本次需要转换成哪个具体枚举。
37. 推荐的项目目录 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 30 31 32 33 34 35 36 src/main/java └── com.example.demo ├── config │ └── WebMvcConfiguration.java │ ├── controller │ └── LabelInfoController.java │ ├── dto │ ├── CreateLabelRequest.java │ ├── EnumOption.java │ └── LabelResponse.java │ ├── entity │ └── LabelInfo.java │ ├── enums │ ├── BaseCodeEnum.java │ ├── EnumUtils.java │ └── ItemType.java │ ├── jpa │ └── converter │ ├── AbstractCodeEnumJpaConverter.java │ └── ItemTypeJpaConverter.java │ ├── repository │ └── LabelInfoRepository.java │ ├── service │ └── LabelInfoService.java │ └── web ├── GlobalExceptionHandler.java └── converter └── StringToCodeEnumConverterFactory.java
38. 最终转换关系 整个系统可以归纳成四套机制。
场景
输入
输出
负责组件
Query / Path
"1"
ItemType.APARTMENT
Spring MVC ConverterFactory
JSON 请求
1
ItemType.APARTMENT
Jackson @JsonCreator
JSON 响应
ItemType.APARTMENT
1
Jackson @JsonValue
JPA 写库
ItemType.APARTMENT
1
AttributeConverter
JPA 读库
1
ItemType.APARTMENT
AttributeConverter
也就是说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Spring MVC ConverterFactory | HTTP "1" ------------>| v ItemType.APARTMENT / \ / \ v v AttributeConverter Jackson | @JsonValue v | DB INT 1 v JSON number 1
这才是一个完整的枚举转换体系。
39. 常见错误 39.1 使用 ordinal() 不要:
业务 code 应该显式定义:
code 属于稳定的业务协议,枚举声明顺序不是。
39.2 Controller 参数全部写 Integer 例如:
1 2 3 4 5 6 @GetMapping public void list (@RequestParam Integer type) { if (type == 1 ) { } }
这样虽然“能跑”,但会导致类型信息从 Controller 开始就丢失。
推荐:
1 2 3 @GetMapping public void list (@RequestParam ItemType type) { }
Spring MVC 在边界完成转换,进入业务层以后全部使用强类型。
39.3 Entity 中直接保存 Integer 不要为了数据库方便写:
然后业务里:
1 2 if (entity.getType() == 1 ) { }
推荐:
让 JPA Converter 负责持久化细节。
39.4 认为 @JsonValue 能处理 Query 参数 不能。
走的是 Spring MVC ConversionService。
才走 Jackson。
这是两条独立链路。
39.5 同时使用 @Enumerated 和 @Convert 不要这样:
1 2 3 @Enumerated(EnumType.STRING) @Convert(converter = ItemTypeJpaConverter.class) private ItemType type;
如果选择 AttributeConverter,就让它完整负责该字段的 DB 映射。
39.6 非法 code 返回 null 不要:
推荐 fail-fast:
1 2 3 throw new IllegalArgumentException ( "非法枚举值:" + code );
否则脏数据会一直向后传播。
40. 企业项目进一步优化 如果项目里只有五六个枚举,目前的方案已经足够。
如果项目里有几十甚至上百个业务枚举,可以继续做三件事。
40.1 缓存 code 到 enum 的映射 当前:
1 Arrays.stream(enumType.getEnumConstants())
每次会遍历枚举。
枚举数量一般很小,所以通常不是性能问题。
但如果转换频率极高,可以按枚举类型缓存:
1 2 3 Class<ItemType> -> Map<Integer, ItemType>
把查找复杂度从线性遍历变成哈希查找。
40.2 自动生成前端枚举 如果前后端都是自己维护,可以进一步把 Java 枚举作为唯一事实来源,然后:
1 2 3 4 5 6 7 Java Enum | v 构建期代码生成 | v TypeScript Enum / const
从根源上避免:
这种协议漂移。
40.3 建立统一枚举字典接口 例如:
返回:
1 2 3 4 5 6 7 8 9 10 11 12 { "itemType" : [ { "value" : 1 , "label" : "公寓" } , { "value" : 2 , "label" : "房间" } ] }
前端表单、筛选器、详情展示都从同一个接口获取。
对于后台管理系统非常实用。
41. 推荐规范 最终建议把项目规范定成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 1. 所有业务枚举至少包含 code + description 2. code 一旦发布,不随意修改语义 3. 禁止使用 ordinal 作为数据库业务值 4. 数据库存稳定业务 code 5. Entity 使用枚举,不使用 Integer 魔法数字 6. Spring MVC Query / Path 使用 ConverterFactory 7. JSON 使用 Jackson @JsonValue + @JsonCreator 8. Spring Data JPA 使用 AttributeConverter 9. AttributeConverter 与 @Enumerated 不同时使用 10. 非法枚举值 fail-fast,并统一转成 HTTP 400 11. Controller / Service / Repository 内部尽量始终使用枚举 12. 前后端协议统一使用数字 code
最终达到:
1 2 3 4 5 6 数据库简单 前端简单 API 稳定 Java 强类型 业务代码没有魔法数字 转换逻辑集中管理
42. 实际项目怎么选 如果只是问:
JPA 和 MyBatis-Plus 哪个枚举转换方式更好?
其实没有必要硬分胜负。
对于 JPA:
就是最自然的标准扩展点。
对于 MyBatis-Plus:
就是最简单直接的方式。
真正应该统一的是整个项目的业务协议:
1 2 3 4 5 数据库:稳定 code Java:强类型 Enum HTTP Query / Path:code JSON:code 前端:code
然后在持久化层根据技术栈分别接入:
1 2 3 4 5 6 7 8 9 10 11 12 BaseCodeEnum | +-----------+-----------+ | | v v Spring Data JPA MyBatis-Plus AttributeConverter @EnumValue | | +-----------+-----------+ | v DB INT
如果项目中:
简单 CRUD 使用 JPA
复杂报表使用 MyBatis-Plus
两种持久化技术同时存在
也完全没有问题。
枚举本身的:
1 2 3 4 code description JSON 协议 HTTP 参数协议
保持一致即可。
43. 总结 原始问题看起来只是:
“数据库的 1 怎么变成 Java 枚举?”
实际上,一个 Web 项目至少有三条完全不同的转换链路:
1 2 3 HTTP 参数 -> Spring MVC ConversionService JSON -> Jackson 数据库字段 -> JPA AttributeConverter / MyBatis-Plus Enum TypeHandler
如果只解决其中一层,枚举转换迟早还会在另外一层出问题。持久化层则应根据项目实际使用 Spring Data JPA 还是 MyBatis-Plus,分别选择 AttributeConverter 或 @EnumValue / IEnum。
因此更推荐把它设计成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌──────────────────────────┐ │ BaseCodeEnum │ │ code + description │ └────────────┬─────────────┘ │ ItemType / OrderStatus │ ┌────────────────────────┼────────────────────────┐ │ │ │ v v v Spring MVC Jackson Spring Data JPA ConverterFactory JsonCreator/JsonValue AttributeConverter │ │ │ v v v Query/Path String JSON number DB INT
对外始终是:
对内始终是:
1 2 ItemType.APARTMENT ItemType.ROOM
这就是比较适合 Spring Boot 3 项目的枚举统一转换方式。
参考资料
CSDN:SpringBoot + Vue 尚庭公寓实战——根据类型查询标签列表接口实现https://blog.csdn.net/weixin_53961667/article/details/139561983
Spring Framework Reference:Spring Type Conversionhttps://docs.spring.io/spring-framework/reference/core/validation/convert.html
Spring Framework Reference:Spring MVC Type Conversionhttps://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/typeconversion.html
Spring Framework Reference:@RequestParamhttps://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/requestparam.html
Jakarta Persistence:AttributeConverterhttps://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/attributeconverter
Jakarta Persistence:@Converterhttps://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/converter
Jackson:@JsonValuehttps://fasterxml.github.io/jackson-annotations/javadoc/2.10/com/fasterxml/jackson/annotation/JsonValue.html
Jackson:@JsonCreatorhttps://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonCreator.html
MyBatis-Plus:自动映射枚举https://baomidou.com/guides/auto-convert-enum/
MyBatis-Plus:配置说明与默认枚举 TypeHandlerhttps://baomidou.com/reference/
MyBatis-Plus:Spring Boot 3 安装https://baomidou.com/getting-started/install/
MyBatis 官方文档https://mybatis.org/mybatis-3/