Spring-Boot-枚举使用-这8个坑90的人都踩过
枚举是 Java 用来描述有限集合的利器订单状态、支付方式、用户角色...几乎每个项目都有枚举的身影。但枚举用不对轻则接口报错重则线上事故。本文梳理了 8 个高频踩坑点看完直接落地。坑1枚举存数据库用 name() 还是 ordinal()错误写法public enum OrderStatus { PENDING, // 0 PAID, // 1 SHIPPED, // 2 COMPLETED // 3 } // 保存时用 ordinal() order.setStatus(OrderStatus.PENDING.ordinal()); // 存的是 0问题ordinal() 是从 0 开始的索引值如果哪天在 PENDING 前面插入一个 DRAFT所有历史数据的含义就全乱了。正确写法用 name() 或自定义 codepublic enum OrderStatus { PENDING(待支付), PAID(已支付), SHIPPED(已发货), COMPLETED(已完成); private final String desc; OrderStatus(String desc) { this.desc desc; } // 推荐给数据库存一个稳定的值 public int getCode() { return this.ordinal() 1; // 或直接写固定值 } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.getCode() code) { return status; } } throw new IllegalArgumentException(未知状态码: code); } }最佳实践数据库存code整型或字符串枚举定义codedesc配套fromCode()反查方法。坑2MyBatis 查出来的枚举值是 nullMyBatis 默认不知道如何把数据库的值转成你的枚举类型。错误场景Mapper public interface OrderMapper { Order findById(Long id); } // 查询结果order.status null ❌解决方案加 TypeHandler方案一通用枚举处理器// MybatisConfig.java Configuration public class MybatisConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new MybatisPlusEnumInterceptor()); return interceptor; } }方案二实体类字段注解TableName(orders) public class Order { // 指定枚举处理方式 TableField(typeHandler EnumTypeHandler.class) private OrderStatus status; }方案三枚举实现 IEnum 接口推荐public enum OrderStatus implements IEnumInteger { PENDING(1, 待支付), PAID(2, 已支付), SHIPPED(3, 已发货), COMPLETED(4, 已完成); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } Override public Integer getValue() { return code; } }MyBatis-Plus 3.5 会自动识别实现了IEnum的枚举无需额外配置。坑3前端传枚举值后端接不到接口定义PostMapping(/orders) public ResultVoid create(RequestBody OrderCreateDTO dto) { // dto.status 永远是 null ❌ }问题原因前端传PENDING或1后端不知道该转成哪个枚举。解决方案一Jackson 序列化配置spring: jackson: deserialization: FAIL_ON_UNKNOWN_PROPERTIES: false serialization: WRITE_ENUMS_USING_TO_STRING: true然后前端传字符串 name{ status: PENDING }解决方案二自定义序列化器推荐public class JsonConfig { Bean public ObjectMapper objectMapper() { ObjectMapper om new ObjectMapper(); om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); om.registerModule(new JavaTimeModule()); // 枚举序列化/反序列化用 code om.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, false); om.setSerializerFactory(new EnumSerializerFactory() { Override public JsonSerializer? createSerializer(SerializerProvider prov, Enum? value) { return new JsonSerializerObject() { Override public void serialize(Object v, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeNumber(((IEnum?) v).getValue()); } }; } }); return om; } }最简方案DTO 用 Integer/String 接收Service 层转枚举public class OrderCreateDTO { private Integer statusCode; // 前端传 1, 2, 3... public OrderStatus getStatus() { return OrderStatus.fromCode(statusCode); } }坑4枚举值改了线上数据不动了典型场景产品经理说订单状态再加一个已取消 CANCELLED结果线上历史数据查询报错。根本原因枚举值和数据库值没有强绑定。防御方案1. 禁止 ordinal() 一切场景// 团队规范禁止使用 ordinal()2. 每个枚举值写死 code且不重复public enum OrderStatus { PENDING(1), // 永远不变 PAID(2), SHIPPED(3), COMPLETED(4), CANCELLED(5); // 新增从 5 开始别复用旧 code private final int code; // ... }3. 数据库加约束ALTER TABLE orders ADD CONSTRAINT chk_status CHECK (status IN (1, 2, 3, 4));坑5枚举用在 RequestParam 或 PathVariable错误示例GetMapping(/orders/{status}) public ResultListOrder list(PathVariable OrderStatus status) { // 报错无法将 String 转成 OrderStatus }解决方案加 RequestParam 配合 Converter// 第一步定义转换器 Component public class StatusConverter implements ConverterString, OrderStatus { Override public OrderStatus convert(String source) { return OrderStatus.fromCode(Integer.parseInt(source)); } } // 第二步注册到 WebMvcConfigurer Configuration public class WebConfig implements WebMvcConfigurer { Autowired private StatusConverter statusConverter; Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(statusConverter); } } // 第三步接口参数 GetMapping(/orders/{status}) public ResultListOrder list(PathVariable(status) OrderStatus status) { return Result.ok(orderService.findByStatus(status)); }简化方案用 Integer 做参数GetMapping(/orders/{statusCode}) public ResultListOrder list(PathVariable Integer statusCode) { OrderStatus status OrderStatus.fromCode(statusCode); return Result.ok(orderService.findByStatus(status)); }坑6枚举比较用 还是 equals()先说结论优先用 OrderStatus status1 OrderStatus.PENDING; OrderStatus status2 OrderStatus.PENDING; status1 status2; // ✅ true同一个对象 status1.equals(status2); // ✅ true PAID.equals(status1); // ❌ false类型不匹配枚举的 绝对安全因为枚举类是 final 的所有枚举值都是单例编译器保证比较的是引用地址唯一需要 equals() 的场景当心 NullPointerExceptionOrderStatus status null; status OrderStatus.PENDING; // ❌ NPE Objects.equals(status, OrderStatus.PENDING); // ✅ false更安全坑7枚举配合 Switch 语句漏掉分支问题代码public String getStatusText(OrderStatus status) { switch (status) { case PENDING: return 待支付; case PAID: return 已支付; // 忘了 COMPLETED新加状态就 GG } return 未知; }编译器无法检测漏掉的分支这是个隐患。正确做法枚举自己带行为public enum OrderStatus { PENDING(1, 待支付), PAID(2, 已支付), SHIPPED(3, 已发货), COMPLETED(4, 已完成); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } // 行为内聚到枚举本身 public String getText() { return this.desc; } // 状态对应的后续操作 public boolean canCancel() { return this PENDING || this PAID; } public boolean canShip() { return this PAID; } } // 调用方简洁清晰 public String getStatusText(OrderStatus status) { return status.getText(); // 每个状态自己知道怎么描述自己 }Java 12 语法糖可选public enum OrderStatus { PENDING { Override public String getText() { return 待支付; } Override public boolean canCancel() { return true; } }, PAID { Override public String getText() { return 已支付; } Override public boolean canCancel() { return true; } }; public abstract String getText(); public abstract boolean canCancel(); }坑8枚举序列化返回给前端不生效问题现象接口返回时枚举变成了{status:PENDING}还是{status: 1}根本原因没有统一配置序列化规则。最佳实践统一配置方案一application.yml最简spring: jackson: serialization: WRITE_ENUMS_USING_TO_STRING: true deserialization: FAIL_ON_UNKNOWN_PROPERTIES: false方案二注解控制细粒度public enum OrderStatus { JsonValue // 序列化时用这个方法 PENDING(1, 待支付), PAID(2, 已支付); private final int code; private final String desc; // 必须配合 fromString JsonCreator public static OrderStatus fromString(String name) { return OrderStatus.valueOf(name); } }返回效果{ status: PENDING } // 序列化用 name { status: 1 } // 序列化用 code最佳实践速查表检查项说明✅ 数据库存 code用整数或字符串别存 ordinal()✅ 枚举实现 IEnumMyBatis-Plus 自动识别✅ 每个枚举配 fromCode()反查方法防 NPE✅ 禁止 ordinal()团队规范违者代码 review 拒掉✅ 枚举带行为getText()、canCancel() 等内聚✅ JsonValue JsonCreator统一前后端序列化格式✅ 全局 EnumConverterPathVariable 枚举参数支持✅ 数据库加 CHECK 约束防止脏数据入库枚举最佳代码模板import com.baomidou.mybatisplus.annotation.IEnum; /** * 订单状态枚举 * 使用规范 * 1. 数据库存储 code * 2. MyBatis-Plus 自动识别 IEnum * 3. 每个状态自带描述和行为 */ public enum OrderStatus implements IEnumInteger { PENDING(1, 待支付) { Override public boolean canCancel() { return true; } Override public boolean canShip() { return false; } }, PAID(2, 已支付) { Override public boolean canCancel() { return true; } Override public boolean canShip() { return true; } }, SHIPPED(3, 已发货) { Override public boolean canCancel() { return false; } Override public boolean canShip() { return false; } }, COMPLETED(4, 已完成) { Override public boolean canCancel() { return false; } Override public boolean canShip() { return false; } }; private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } Override public Integer getValue() { return code; } public String getText() { return desc; } // 由子类实现具体行为 public abstract boolean canCancel(); public abstract boolean canShip(); // 静态反查防 NPE public static OrderStatus fromCode(Integer code) { if (code null) { return null; } for (OrderStatus status : values()) { if (status.code.equals(code)) { return status; } } throw new IllegalArgumentException(未知订单状态码: code); } // 静态反查 name public static OrderStatus fromName(String name) { if (name null) { return null; } try { return OrderStatus.valueOf(name); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(未知订单状态: name); } } }总结枚举是个好工具但用不好就是埋雷。记住三个核心原则数据库存 code别用 ordinal()枚举带行为别把逻辑散在外面序列化统一配置前后端约定好格式8 个坑全避开你的代码能甩别人三条街。