当前位置: 首页 > news >正文

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() 或自定义 code

public 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(整型或字符串),枚举定义code+desc,配套fromCode()反查方法。

坑2:MyBatis 查出来的枚举值是 null

MyBatis 默认不知道如何把数据库的值转成你的枚举类型。

错误场景:

@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 IEnum<Integer> { 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 Result<Void> 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 JsonSerializer<Object>() { @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 Result<List<Order>> list(@PathVariable OrderStatus status) { // 报错:无法将 String 转成 OrderStatus }

解决方案:加 @RequestParam + 配合 Converter

// 第一步:定义转换器 @Component public class StatusConverter implements Converter<String, 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 Result<List<Order>> list(@PathVariable("status") OrderStatus status) { return Result.ok(orderService.findByStatus(status)); }

简化方案:用 Integer 做参数

@GetMapping("/orders/{statusCode}") public Result<List<Order>> 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,类型不匹配

枚举的 == 绝对安全,因为:

  1. 枚举类是 final 的,所有枚举值都是单例
  2. 编译器保证==比较的是引用地址

唯一需要 equals() 的场景:当心 NullPointerException

OrderStatus 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 IEnum<Integer> { 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); } } }

总结

枚举是个好工具,但用不好就是埋雷。记住三个核心原则:

  1. 数据库存 code,别用 ordinal()
  2. 枚举带行为,别把逻辑散在外面
  3. 序列化统一配置,前后端约定好格式

8 个坑全避开,你的代码能甩别人三条街。

http://www.jsqmd.com/news/650462/

相关文章:

  • 2026年开源客服系统哪家好?大模型多语言数据分析呼叫中心集成 - 品牌2026
  • 别再只会点菜单了!EPLAN拖放操作全解析:从符号宏到DWG文件,效率翻倍的隐藏技巧
  • 分析想找小班授课的形象设计培训学校,太原哪家比较靠谱 - 工业品网
  • 从静态防护到流转治理:API风险监测系统如何重塑企业数据安全体系
  • 抖音无水印批量下载工具:如何轻松保存你喜欢的视频内容?
  • Unity WebGL 缓存失效排查:从 Cache API 错误到 loader.js 修复
  • 小目标检测技术演进:从数据增强到无锚点方法的全面解析
  • Matlab图像显示进阶:pcolor与imagesc的格网精细化控制
  • 2026年在线客服哪家好?客服系统机器人推荐及选型指南 - 品牌2026
  • 保姆级教程:用群晖Docker和technosoft2000镜像,5分钟搞定Calibre Web私人书库(附权限避坑指南)
  • 终极中文文献管理方案:如何用Jasminum插件解决Zotero中文元数据识别难题
  • 基于STM32的TCRT5000循迹传感器实战指南:从原理到代码实现
  • 【从0开始学设计模式-8| 桥接模式】
  • 给测试新人的TBOX入门指南:从零看懂车载通信测试到底在测啥
  • 阿里放大招!Qwen3.5-Omni发布,企业AI落地成本大幅降低
  • 2026年新疆乌鲁木齐:车闪电新能源汽车防护升级服务全景报道 - 精选优质企业推荐榜
  • 如何快速实现B站m4s视频格式转换:3分钟无损转换完整指南
  • vxe-table 自定义单元格提示模板实战:从基础配置到高级应用
  • CAN离线记录仪从入门到精通:手把手教你配置与使用(附常见问题解决)
  • 魔兽世界GSE宏编辑器终极指南:5步打造你的智能技能循环
  • 终极番茄小说下载器:从网页到电子书的完整解决方案
  • 【MySQL】深入解析 Handler 接口:从语法到实战的逐行数据操作指南
  • 2026年呼和浩特GEO优化领域3家主流服务商选型参考深度分析报告 - 商业小白条
  • 生成式AI灰度发布失败率下降73%的关键策略:从流量切分、语义一致性校验到回滚SLA量化设计
  • 从游戏私服后台到系统权限:一次ASPcms漏洞的完整利用链剖析
  • 杰理之PC硬回踩没效果【篇】
  • 轻量翻译模型HY-MT1.5-1.8B:术语干预功能使用教程
  • 牛客网热门Java 面试八股文解析 + 大厂面试攻略
  • QrazyBox终极指南:如何轻松修复损坏二维码,恢复重要数据
  • 分享靠谱的小红书代运营专业公司,选购要点与价格分析 - myqiye