Skip to content

枚举定义与转换

✨ 统一的枚举设计规范和自动转换机制,支持请求参数、JSON 序列化/反序列化的无缝集成。

  • 🎯 统一接口:BaseEnum<T> 统一枚举值管理
  • 🔄 自动转换:请求参数自动转换为枚举对象
  • 📤 智能序列化:JSON 输出枚举值而非枚举名称
  • 🔢 类型支持:支持 String、Integer、Long 等多种枚举值类型
  • 🛠️ 附加值:支持扩展附加属性和 Map 附加值
  • 性能优化:基于 ConcurrentHashMap 的转换缓存

快速开始

定义枚举

推荐方式:实现 BaseEnum 接口

java
import com.fasterxml.jackson.annotation.JsonCreator;
import host.springboot.framework3.core.enumeration.BaseEnum;

/**
 * 用户状态枚举
 */
public enum UserStatusEnum implements BaseEnum<Integer> {
    
    INACTIVE(0, "未激活"),
    ACTIVE(1, "已激活"),
    LOCKED(2, "已锁定"),
    DELETED(9, "已删除");
    
    private final Integer value;
    private final String reasonPhrase;
    
    UserStatusEnum(Integer value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
    }
    
    @Override
    public Integer getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    /**
     * JSON 反序列化支持
     * 必须添加此方法以支持 @RequestBody 参数绑定
     */
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static UserStatusEnum parse(Integer value) {
        return BaseEnum.parse(value, UserStatusEnum.class);
    }
}

使用枚举

Controller 层

java
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    /**
     * 场景1:URL 查询参数
     * GET /api/user/list?status=1
     */
    @GetMapping("/list")
    public BaseVO list(
            @RequestParam(required = false) UserStatusEnum status) {
        // ✅ 框架自动将 "1" 转换为 UserStatusEnum.ACTIVE
        return R.ok(userService.findByStatus(status));
    }
    
    /**
     * 场景2:路径参数
     * GET /api/user/status/1
     */
    @GetMapping("/status/{status}")
    public BaseVO getByStatus(@PathVariable UserStatusEnum status) {
        // ✅ 框架自动转换
        return R.ok(userService.findByStatus(status));
    }
    
    /**
     * 场景3:请求体 JSON
     * POST /api/user/create
     * {"username":"test", "status":1}
     */
    @PostMapping("/create")
    public BaseVO create(@RequestBody UserCreateDTO dto) {
        // ✅ dto.getStatus() 已自动转换为枚举对象
        return R.ok(userService.create(dto));
    }
}

/**
 * 请求 DTO
 */
public class UserCreateDTO {
    private String username;
    private UserStatusEnum status;  // ✅ 自动转换
    
    // getter/setter...
}

JSON 序列化输出

java
public class UserVO {
    private Long id;
    private String username;
    private UserStatusEnum status;  // ✅ 输出枚举值而非枚举名称
    
    // getter/setter...
}

// 响应 JSON:
{
    "code": 200,
    "data": {
        "id": 1,
        "username": "test",
        "status": 1  // ✅ 输出 Integer 值,而不是 "ACTIVE"
    }
}

BaseEnum 接口详解

核心方法

java
public interface BaseEnum<T> {
    
    /**
     * 获取枚举值(必须实现)
     * @JsonValue 注解确保 JSON 序列化时输出此值
     */
    @JsonValue
    T getValue();
    
    /**
     * 获取枚举描述(必须实现)
     */
    String getReasonPhrase();
    
    /**
     * 附加单个值(可选)
     */
    default String additionalValue() {
        return null;
    }
    
    /**
     * 附加多个值(可选)
     */
    default Map<String, String> additionalMapValue() {
        return Collections.emptyMap();
    }
    
    /**
     * 解析枚举值(静态工具方法)
     */
    static <T, E extends BaseEnum<T>> E parse(
            T value, Class<E> enumClass) {
        // 智能解析,支持动态类型转换
    }
}

方法说明

1. getValue() - 枚举值

作用:返回枚举的实际值,用于 JSON 序列化和数据库存储。

支持的类型:

  • String - 字符串枚举值
  • Integer - 整数枚举值
  • Long - 长整数枚举值
  • Boolean - 布尔枚举值
  • BigDecimal - 大数枚举值
  • 其他数字类型

示例:

java
public enum GenderEnum implements BaseEnum<String> {
    MALE("M", "男性"),
    FEMALE("F", "女性");
    
    private final String value;
    private final String reasonPhrase;
    
    // 构造器
    
    @Override
    public String getValue() {
        return value;  // 返回 "M" 或 "F"
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static GenderEnum parse(String value) {
        return BaseEnum.parse(value, GenderEnum.class);
    }
}

2. getReasonPhrase() - 枚举描述

作用:提供枚举的人类可读描述。

使用场景:

  • 前端下拉框展示
  • 日志记录
  • 错误提示

示例:

java
public enum OrderStatusEnum implements BaseEnum<Integer> {
    PENDING(0, "待支付"),
    PAID(1, "已支付"),
    SHIPPED(2, "已发货"),
    COMPLETED(3, "已完成"),
    CANCELLED(9, "已取消");
    
    private final Integer value;
    private final String reasonPhrase;
    
    // 构造器、getter...
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static OrderStatusEnum parse(Integer value) {
        return BaseEnum.parse(value, OrderStatusEnum.class);
    }
}

// 使用
OrderStatusEnum status = OrderStatusEnum.PAID;
log.info("订单状态: {} - {}", status.getValue(), status.getReasonPhrase());
// 输出: 订单状态: 1 - 已支付

3. additionalValue() - 附加单个值

作用:扩展单个附加属性。

示例:

java
public enum ColorEnum implements BaseEnum<String> {
    RED("red", "红色", "#FF0000"),
    GREEN("green", "绿色", "#00FF00"),
    BLUE("blue", "蓝色", "#0000FF");
    
    private final String value;
    private final String reasonPhrase;
    private final String hexCode;  // 附加值:十六进制颜色代码
    
    ColorEnum(String value, String reasonPhrase, String hexCode) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
        this.hexCode = hexCode;
    }
    
    @Override
    public String getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    @Override
    public String additionalValue() {
        return hexCode;  // 返回十六进制代码
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static ColorEnum parse(String value) {
        return BaseEnum.parse(value, ColorEnum.class);
    }
}

// 使用
ColorEnum color = ColorEnum.RED;
String hex = color.additionalValue();  // "#FF0000"

4. additionalMapValue() - 附加多个值

作用:扩展多个附加属性,以 Map 形式返回。

示例:

java
public enum ProductTypeEnum implements BaseEnum<Integer> {
    PHYSICAL(1, "实物商品"),
    VIRTUAL(2, "虚拟商品"),
    SERVICE(3, "服务商品");
    
    private final Integer value;
    private final String reasonPhrase;
    private final Map<String, String> additionalMap;
    
    ProductTypeEnum(Integer value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
        
        // 初始化附加值
        this.additionalMap = new HashMap<>();
        switch (this) {
            case PHYSICAL:
                additionalMap.put("needShipping", "true");
                additionalMap.put("category", "goods");
                break;
            case VIRTUAL:
                additionalMap.put("needShipping", "false");
                additionalMap.put("category", "digital");
                break;
            case SERVICE:
                additionalMap.put("needShipping", "false");
                additionalMap.put("category", "service");
                break;
        }
    }
    
    @Override
    public Integer getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    @Override
    public Map<String, String> additionalMapValue() {
        return additionalMap;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static ProductTypeEnum parse(Integer value) {
        return BaseEnum.parse(value, ProductTypeEnum.class);
    }
}

// 使用
ProductTypeEnum type = ProductTypeEnum.PHYSICAL;
Map<String, String> additional = type.additionalMapValue();
String needShipping = additional.get("needShipping");  // "true"
String category = additional.get("category");  // "goods"

5. parse() - 智能解析

作用:将值解析为枚举对象,支持动态类型转换。

方法重载:

java
// 1. 基础解析(推荐)
static <T, E extends BaseEnum<T>> E parse(T value, Class<E> enumClass)

// 2. 严格解析(找不到时抛异常)
static <T, E extends BaseEnum<T>> E parseThrowIfNotFound(
    T value, Class<E> enumClass)

// 3. 完整解析(自定义行为)
static <T, E extends BaseEnum<T>> E parse(
    T value, Class<E> enumClass, 
    boolean isDynamicParse,      // 是否动态解析
    boolean isNotFoundThrow)     // 找不到时是否抛异常

动态解析特性:

java
public enum StatusEnum implements BaseEnum<Integer> {
    ACTIVE(1, "激活"),
    INACTIVE(0, "未激活");
    
    // ...
}

// ✅ 整数解析
StatusEnum s1 = BaseEnum.parse(1, StatusEnum.class);
// s1 = StatusEnum.ACTIVE

// ✅ 字符串自动转整数(动态解析)
StatusEnum s2 = BaseEnum.parse("1", StatusEnum.class, true, false);
// s2 = StatusEnum.ACTIVE

// ✅ 找不到返回 null
StatusEnum s3 = BaseEnum.parse(99, StatusEnum.class);
// s3 = null

// ❌ 找不到抛异常
StatusEnum s4 = BaseEnum.parseThrowIfNotFound(99, StatusEnum.class);
// 抛出: EnumIllegalArgumentException

枚举转换机制

URL 参数转换

原理:基于 Spring 的 ConverterFactory 机制。

工作流程:

HTTP 请求: GET /api/user/list?status=1

Spring MVC 参数解析

EnumConverterFactory.getConverter(UserStatusEnum.class)

检查缓存 CONVERTER_MAP
    ├─ 命中缓存 → 直接返回 Converter
    └─ 未命中 → 创建 Converter

    检查是否有 @EnumConvertValue 注解
        ├─ 有 → 使用 StringToAnnotationEnumConverter
        └─ 无 → 检查是否实现 BaseEnum
            ├─ 是 → 使用 StringToBaseEnumConverter
            └─ 否 → 返回 null(使用默认转换)

Converter.convert("1")

BaseEnum.parse(1, UserStatusEnum.class)

返回 UserStatusEnum.ACTIVE

核心代码:

java
public final class EnumConverterFactory 
        implements ConverterFactory<String, Enum<?>> {
    
    // 转换缓存(性能优化)
    private static final Map<Class<? extends Enum<?>>, 
        EnumConverterHolder> CONVERTER_MAP = new ConcurrentHashMap<>();
    
    @Override
    public <E extends Enum<?>> Converter<String, E> getConverter(
            Class<E> targetType) {
        // 从缓存获取或创建转换器
        EnumConverterHolder holder = CONVERTER_MAP.computeIfAbsent(
            targetType, EnumConverterHolder::create);
        
        // 优先使用 @EnumConvertValue 注解转换器
        return (Converter<String, E>) (
            holder.annotationConverter != null ? 
            holder.annotationConverter : 
            holder.baseEnumConverter
        );
    }
}

JSON 序列化

原理:基于 Jackson 的自定义序列化器 BaseEnumSerializer

序列化流程:

Java 对象: UserVO { status = UserStatusEnum.ACTIVE }

Jackson 序列化

BaseEnumSerializer.serialize(UserStatusEnum.ACTIVE, ...)

获取 getValue() 返回值 → 1 (Integer)

根据类型调用对应的 write 方法
    ├─ String → jsonGenerator.writeString()
    ├─ Integer → jsonGenerator.writeNumber()
    ├─ Long → jsonGenerator.writeNumber()
    ├─ Boolean → jsonGenerator.writeBoolean()
    └─ BigDecimal → jsonGenerator.writeNumber()

JSON 输出: {"status": 1}

核心代码:

java
public final class BaseEnumSerializer 
        extends StdSerializer<BaseEnum<Object>> {
    
    @Override
    public void serialize(
            BaseEnum<Object> object, 
            JsonGenerator jsonGenerator,
            SerializerProvider serializerProvider) throws IOException {
        
        if (Objects.isNull(object.getValue())) {
            jsonGenerator.writeNull();
            return;
        }
        
        Object value = object.getValue();
        
        // 根据值类型选择序列化方法
        if (value instanceof String) {
            jsonGenerator.writeString((String) value);
        } else if (value instanceof Integer) {
            jsonGenerator.writeNumber((Integer) value);
        } else if (value instanceof Long) {
            jsonGenerator.writeNumber((Long) value);
        }
        // ... 其他类型
    }
}

JSON 反序列化

原理:基于 @JsonCreator 注解的工厂方法。

反序列化流程:

JSON 输入: {"status": 1}

Jackson 反序列化

查找 @JsonCreator 方法

UserStatusEnum.parse(1)  // 必须在枚举类中定义

BaseEnum.parse(1, UserStatusEnum.class)

遍历枚举值匹配
    ├─ INACTIVE.getValue() = 0 → 不匹配
    ├─ ACTIVE.getValue() = 1 → ✅ 匹配
    └─ ...

返回 UserStatusEnum.ACTIVE

必须添加的代码:

java
public enum UserStatusEnum implements BaseEnum<Integer> {
    // ... 枚举常量
    
    /**
     * ⚠️ 必须添加此方法,否则 @RequestBody 无法反序列化
     */
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static UserStatusEnum parse(Integer value) {
        return BaseEnum.parse(value, UserStatusEnum.class);
    }
}

常见场景

场景1:用户状态枚举

java
public enum UserStatusEnum implements BaseEnum<Integer> {
    
    INACTIVE(0, "未激活"),
    ACTIVE(1, "已激活"),
    LOCKED(2, "已锁定"),
    DELETED(9, "已删除");
    
    private final Integer value;
    private final String reasonPhrase;
    
    UserStatusEnum(Integer value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
    }
    
    @Override
    public Integer getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    /**
     * 业务方法:是否可以登录
     */
    public boolean canLogin() {
        return this == ACTIVE;
    }
    
    /**
     * 业务方法:是否可以激活
     */
    public boolean canActivate() {
        return this == INACTIVE;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static UserStatusEnum parse(Integer value) {
        return BaseEnum.parse(value, UserStatusEnum.class);
    }
}

// Controller
@GetMapping("/list")
public BaseVO list(@RequestParam UserStatusEnum status) {
    // 业务逻辑中使用
    if (!status.canLogin()) {
        throw new ApplicationException(
            ErrorCodeEnum.FORBIDDEN,
            "该状态的用户不允许登录"
        );
    }
    return R.ok(userService.findByStatus(status));
}

场景2:订单状态流转

java
public enum OrderStatusEnum implements BaseEnum<Integer> {
    
    PENDING(0, "待支付"),
    PAID(1, "已支付"),
    SHIPPED(2, "已发货"),
    COMPLETED(3, "已完成"),
    CANCELLED(9, "已取消");
    
    private final Integer value;
    private final String reasonPhrase;
    
    OrderStatusEnum(Integer value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
    }
    
    @Override
    public Integer getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    /**
     * 获取下一个状态
     */
    public OrderStatusEnum getNextStatus() {
        return switch (this) {
            case PENDING -> PAID;
            case PAID -> SHIPPED;
            case SHIPPED -> COMPLETED;
            default -> throw new IllegalStateException(
                "状态 " + this.getReasonPhrase() + " 不能流转"
            );
        };
    }
    
    /**
     * 是否可以取消
     */
    public boolean canCancel() {
        return this == PENDING || this == PAID;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static OrderStatusEnum parse(Integer value) {
        return BaseEnum.parse(value, OrderStatusEnum.class);
    }
}

// Service
public void updateOrderStatus(Long orderId, OrderStatusEnum newStatus) {
    Order order = orderMapper.selectById(orderId);
    OrderStatusEnum currentStatus = order.getStatus();
    
    // 校验状态流转
    if (currentStatus.getNextStatus() != newStatus) {
        throw new ApplicationException(
            ErrorCodeEnum.INVALID_USER_INPUT,
            String.format("订单状态不能从 %s 直接变更为 %s",
                currentStatus.getReasonPhrase(),
                newStatus.getReasonPhrase())
        );
    }
    
    order.setStatus(newStatus);
    orderMapper.updateById(order);
}

场景3:字符串枚举值

java
public enum LanguageEnum implements BaseEnum<String> {
    
    ZH_CN("zh_CN", "简体中文"),
    ZH_TW("zh_TW", "繁体中文"),
    EN_US("en_US", "英语(美国)"),
    JA_JP("ja_JP", "日语");
    
    private final String value;
    private final String reasonPhrase;
    
    LanguageEnum(String value, String reasonPhrase) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
    }
    
    @Override
    public String getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static LanguageEnum parse(String value) {
        return BaseEnum.parse(value, LanguageEnum.class);
    }
}

// 请求: GET /api/content/get?lang=zh_CN
@GetMapping("/get")
public BaseVO getContent(@RequestParam LanguageEnum lang) {
    // ✅ lang = LanguageEnum.ZH_CN
    return R.ok(contentService.getByLanguage(lang));
}

场景4:带附加值的枚举

java
public enum PayMethodEnum implements BaseEnum<Integer> {
    
    ALIPAY(1, "支付宝", "https://www.alipay.com"),
    WECHAT(2, "微信支付", "https://pay.weixin.qq.com"),
    UNION(3, "银联支付", "https://www.unionpay.com");
    
    private final Integer value;
    private final String reasonPhrase;
    private final String payUrl;  // 附加值:支付网关地址
    
    PayMethodEnum(Integer value, String reasonPhrase, String payUrl) {
        this.value = value;
        this.reasonPhrase = reasonPhrase;
        this.payUrl = payUrl;
    }
    
    @Override
    public Integer getValue() {
        return value;
    }
    
    @Override
    public String getReasonPhrase() {
        return reasonPhrase;
    }
    
    @Override
    public String additionalValue() {
        return payUrl;
    }
    
    /**
     * 获取支付网关地址
     */
    public String getPayUrl() {
        return payUrl;
    }
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static PayMethodEnum parse(Integer value) {
        return BaseEnum.parse(value, PayMethodEnum.class);
    }
}

// Service
public String createPayment(PayMethodEnum payMethod, BigDecimal amount) {
    String payUrl = payMethod.getPayUrl();
    // 或者: String payUrl = payMethod.additionalValue();
    
    // 调用对应的支付网关
    return paymentService.create(payUrl, amount);
}

场景5:MyBatis-Plus 集成

java
// 实体类
@TableName("t_user")
public class User extends BaseDO {
    
    private String username;
    
    // ✅ MyBatis-Plus 会自动将数据库中的 Integer 值转换为枚举
    private UserStatusEnum status;
    
    // getter/setter...
}

// Mapper
public interface UserMapper extends BaseMapper<User> {
    
    // ✅ 自动转换
    @Select("SELECT * FROM t_user WHERE status = #{status}")
    List<User> selectByStatus(@Param("status") UserStatusEnum status);
}

// Service
public List<User> findActiveUsers() {
    // ✅ 查询条件自动转换
    return userMapper.selectByStatus(UserStatusEnum.ACTIVE);
}

@EnumConvertValue 注解(不推荐)

使用方式

注意:推荐使用 BaseEnum 接口,@EnumConvertValue 仅用于特殊场景。

java
public enum StatusEnum {
    
    ACTIVE,
    INACTIVE;
    
    /**
     * ⚠️ 使用 @EnumConvertValue 注解
     * 必须是静态方法,参数为 String,返回枚举类型
     */
    @EnumConvertValue
    public static StatusEnum convertValue(String value) {
        return switch (value.toUpperCase()) {
            case "1", "ACTIVE" -> ACTIVE;
            case "0", "INACTIVE" -> INACTIVE;
            default -> null;
        };
    }
}

// Controller
@GetMapping("/list")
public BaseVO list(@RequestParam StatusEnum status) {
    // ✅ 支持 "1", "ACTIVE" 等多种值转换
    return R.ok(userService.findByStatus(status));
}

限制条件

  1. ✅ 必须是静态方法
  2. ✅ 必须有一个 String 参数
  3. ✅ 返回值必须是枚举类型
  4. ✅ 只能标注在一个方法
  5. ❌ 不支持 JSON 序列化(无 getValue())
  6. ❌ 不支持 MyBatis-Plus 自动转换

为什么不推荐?

对比项BaseEnum@EnumConvertValue
URL 参数转换✅ 支持✅ 支持
JSON 序列化✅ 输出值❌ 输出枚举名
JSON 反序列化✅ 支持(@JsonCreator)❌ 不支持
MyBatis-Plus✅ 自动转换❌ 需手动处理
附加值✅ 支持❌ 不支持
类型安全✅ 泛型支持⚠️ 仅 String
代码简洁✅ 接口统一⚠️ 需自定义

结论:除非有特殊需求,否则始终使用 BaseEnum 接口。

@NumberToScaleString 注解

核心功能

解决问题:JavaScript 浮点数精度丢失。

原理:将数字类型序列化为带指定小数位的字符串。

使用方式

java
import host.springboot.framework.context.mvc.annotation.NumberToScaleString;
import java.math.RoundingMode;

public class ProductVO {
    
    private Long id;
    private String name;
    
    /**
     * ✅ 保留2位小数,四舍五入
     * 输入: 99.996
     * 输出: "100.00"
     */
    @NumberToScaleString(scale = 2, roundingMode = RoundingMode.HALF_UP)
    private BigDecimal price;
    
    /**
     * ✅ 保留4位小数,向下取整
     * 输入: 0.123456
     * 输出: "0.1234"
     */
    @NumberToScaleString(
        scale = 4, 
        roundingMode = RoundingMode.DOWN,
        toStringType = NumberToScaleString.ToStringType.TO_PLAIN_STRING
    )
    private BigDecimal rate;
    
    // getter/setter...
}

// 响应 JSON:
{
    "id": 1,
    "name": "商品名称",
    "price": "100.00",  // ✅ 字符串,保留2位小数
    "rate": "0.1234"    // ✅ 字符串,保留4位小数
}

注解参数

java
public @interface NumberToScaleString {
    
    /**
     * 保留小数位数
     * 默认: 2
     */
    int scale() default 2;
    
    /**
     * 舍入模式
     * 默认: HALF_UP(四舍五入)
     */
    RoundingMode roundingMode() default RoundingMode.HALF_UP;
    
    /**
     * 输出字符串类型
     * 默认: TO_PLAIN_STRING
     */
    ToStringType toStringType() default ToStringType.TO_PLAIN_STRING;
}

RoundingMode 说明

模式说明示例(scale=2)
UP向上取整1.234 → "1.24"
DOWN向下取整1.236 → "1.23"
CEILING向正无穷取整1.231 → "1.24"
FLOOR向负无穷取整1.239 → "1.23"
HALF_UP四舍五入1.235 → "1.24"
HALF_DOWN五舍六入1.235 → "1.23"
HALF_EVEN银行家舍入1.235 → "1.24"

ToStringType 说明

java
public enum ToStringType {
    
    /**
     * 默认 toString()
     * 可能使用科学计数法
     */
    DEFAULT,
    
    /**
     * toPlainString()
     * 不使用科学计数法(推荐)
     */
    TO_PLAIN_STRING,
    
    /**
     * toEngineeringString()
     * 工程计数法
     */
    TO_ENGINEERING_STRING
}

// 示例
BigDecimal value = new BigDecimal("0.00000123");

// DEFAULT: 可能输出 "1.23E-6"
// TO_PLAIN_STRING: 输出 "0.00000123"
// TO_ENGINEERING_STRING: 输出 "1.23E-6"

支持的类型

java
public class DTO {
    
    // ✅ BigDecimal
    @NumberToScaleString(scale = 2)
    private BigDecimal amount1;
    
    // ✅ BigInteger
    @NumberToScaleString(scale = 2)
    private BigInteger amount2;
    
    // ✅ Long
    @NumberToScaleString(scale = 2)
    private Long amount3;
    
    // ✅ Double
    @NumberToScaleString(scale = 2)
    private Double amount4;
    
    // ✅ Float
    @NumberToScaleString(scale = 2)
    private Float amount5;
    
    // ✅ Integer
    @NumberToScaleString(scale = 2)
    private Integer amount6;
    
    // ✅ String(数字字符串)
    @NumberToScaleString(scale = 2)
    private String amount7;  // "123.456" → "123.46"
    
    // ❌ 非数字类型会抛异常
    // @NumberToScaleString(scale = 2)
    // private String name;  // 错误!
}

使用场景

场景1:金额字段

java
public class OrderVO {
    
    /**
     * 订单金额(元)
     * 保留2位小数,四舍五入
     */
    @NumberToScaleString(scale = 2, roundingMode = RoundingMode.HALF_UP)
    private BigDecimal totalAmount;
    
    /**
     * 优惠金额(元)
     * 保留2位小数,向下取整(有利于商家)
     */
    @NumberToScaleString(scale = 2, roundingMode = RoundingMode.DOWN)
    private BigDecimal discountAmount;
    
    /**
     * 实付金额(元)
     * 保留2位小数,向上取整(避免少收)
     */
    @NumberToScaleString(scale = 2, roundingMode = RoundingMode.UP)
    private BigDecimal payAmount;
}

// JSON 输出:
{
    "totalAmount": "100.00",
    "discountAmount": "10.00",
    "payAmount": "90.00"
}

场景2:利率/汇率

java
public class ExchangeRateVO {
    
    /**
     * 汇率
     * 保留6位小数,银行家舍入
     */
    @NumberToScaleString(
        scale = 6, 
        roundingMode = RoundingMode.HALF_EVEN
    )
    private BigDecimal rate;
    
    /**
     * 年化收益率(%)
     * 保留4位小数
     */
    @NumberToScaleString(scale = 4)
    private BigDecimal annualYield;
}

// JSON 输出:
{
    "rate": "6.890123",      // 汇率保留6位
    "annualYield": "3.6500"  // 收益率保留4位
}

最佳实践

✅ 推荐做法

java
// 1. 所有枚举都实现 BaseEnum 接口
public enum StatusEnum implements BaseEnum<Integer> {
    // ✅ 统一规范
}

// 2. 必须添加 @JsonCreator 方法
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static StatusEnum parse(Integer value) {
    return BaseEnum.parse(value, StatusEnum.class);
}

// 3. 使用有意义的枚举值
public enum UserStatusEnum implements BaseEnum<Integer> {
    INACTIVE(0, "未激活"),  // ✅ 0 表示未激活
    ACTIVE(1, "已激活"),    // ✅ 1 表示已激活
    LOCKED(2, "已锁定"),    // ✅ 2 表示已锁定
    DELETED(9, "已删除");   // ✅ 9 表示删除状态
}

// 4. 添加业务方法增强枚举功能
public enum OrderStatusEnum implements BaseEnum<Integer> {
    // ...
    
    public boolean canCancel() {
        return this == PENDING || this == PAID;
    }
    
    public OrderStatusEnum getNextStatus() {
        return switch (this) {
            case PENDING -> PAID;
            case PAID -> SHIPPED;
            // ...
        };
    }
}

// 5. 金额字段使用 @NumberToScaleString
public class OrderVO {
    @NumberToScaleString(scale = 2, roundingMode = RoundingMode.HALF_UP)
    private BigDecimal amount;  // ✅ 解决精度问题
}

❌ 不推荐做法

java
// ❌ 1. 不实现 BaseEnum 接口
public enum StatusEnum {
    ACTIVE, INACTIVE;  // 无法自动转换
}

// ❌ 2. 忘记添加 @JsonCreator 方法
public enum StatusEnum implements BaseEnum<Integer> {
    ACTIVE(1, "激活");
    
    // 缺少 parse() 方法
    // @RequestBody 无法反序列化!
}

// ❌ 3. 使用无意义的枚举值
public enum StatusEnum implements BaseEnum<Integer> {
    ACTIVE(123, "激活"),     // ❌ 123 无意义
    INACTIVE(456, "未激活"); // ❌ 456 无意义
}

// ✅ 正确做法
public enum StatusEnum implements BaseEnum<Integer> {
    INACTIVE(0, "未激活"),
    ACTIVE(1, "激活");
}

// ❌ 4. 过度使用 @EnumConvertValue
public enum StatusEnum {
    ACTIVE, INACTIVE;
    
    @EnumConvertValue
    public static StatusEnum convertValue(String value) {
        // ❌ 不如直接实现 BaseEnum
    }
}

// ❌ 5. 金额字段不处理精度
public class OrderVO {
    private BigDecimal amount;  // ❌ 前端可能精度丢失
}

// ✅ 正确做法
public class OrderVO {
    @NumberToScaleString(scale = 2)
    private BigDecimal amount;  // ✅ 转为字符串
}

常见问题

Q: 为什么必须实现 BaseEnum 接口?

A: 框架基于 BaseEnum 接口提供了完整的转换和序列化支持:

  • URL 参数转换:通过 EnumConverterFactory
  • JSON 序列化:通过 BaseEnumSerializer
  • JSON 反序列化:通过 @JsonCreator + BaseEnum.parse()
  • MyBatis-Plus:自动类型转换
  • 附加值:支持扩展属性

不实现接口将失去这些自动化能力。

Q: 为什么必须添加 @JsonCreator 方法?

A: Jackson 反序列化 JSON 时需要知道如何将值转换为枚举:

java
// ❌ 没有 @JsonCreator
public enum StatusEnum implements BaseEnum<Integer> {
    ACTIVE(1, "激活");
    // 缺少 parse() 方法
}

// JSON: {"status": 1}
// 反序列化失败! Cannot construct instance of StatusEnum

// ✅ 有 @JsonCreator
public enum StatusEnum implements BaseEnum<Integer> {
    ACTIVE(1, "激活");
    
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static StatusEnum parse(Integer value) {
        return BaseEnum.parse(value, StatusEnum.class);
    }
}

// JSON: {"status": 1}
// ✅ 反序列化成功! status = StatusEnum.ACTIVE

Q: BaseEnum.parse() 和 parseThrowIfNotFound() 有什么区别?

A: 找不到匹配值时的行为不同:

java
// parse() - 找不到返回 null
StatusEnum s1 = BaseEnum.parse(99, StatusEnum.class);
// s1 = null,不抛异常

// parseThrowIfNotFound() - 找不到抛异常
StatusEnum s2 = BaseEnum.parseThrowIfNotFound(99, StatusEnum.class);
// 抛出: EnumIllegalArgumentException: 
// No matching [StatusEnum] for [99]

选择建议:

  • URL 参数:使用 parse()(允许 null)
  • JSON 反序列化:使用 parse()(允许 null)
  • 业务校验:使用 parseThrowIfNotFound()(严格校验)

Q: 如何处理枚举值为 null 的情况?

A: 框架已自动处理:

URL 参数:

java
// GET /api/user/list?status=
@GetMapping("/list")
public BaseVO list(@RequestParam(required = false) UserStatusEnum status) {
    // status = null(空字符串转换为 null)
    if (status == null) {
        // 查询所有状态
    }
}

JSON 序列化:

java
public class UserVO {
    private UserStatusEnum status;  // null
}

// JSON 输出:
{
    "status": null  // ✅ 输出 null
}

JSON 反序列化:

java
// JSON: {"status": null}
public class UserDTO {
    private UserStatusEnum status;  // null
}
// ✅ status = null

Q: @NumberToScaleString 会影响性能吗?

A: 性能影响极小:

  1. 仅在序列化时执行:不影响业务逻辑
  2. BigDecimal 计算:现代 JVM 优化良好
  3. 无额外内存:直接输出字符串

性能对比:

java
// 未使用注解
private BigDecimal price;  // 直接输出数字
// 序列化耗时: ~1μs

// 使用注解  
@NumberToScaleString(scale = 2)
private BigDecimal price;  // setScale + toString
// 序列化耗时: ~2μs

// 性能差异: 可忽略不计

Q: 枚举值可以重复吗?

A: 不建议,但技术上可以:

java
// ❌ 不推荐:枚举值重复
public enum StatusEnum implements BaseEnum<Integer> {
    INACTIVE(0, "未激活"),
    DELETED(0, "已删除");  // ❌ 值重复
}

// 解析时只会匹配到第一个
StatusEnum s = BaseEnum.parse(0, StatusEnum.class);
// s = StatusEnum.INACTIVE(始终返回第一个匹配)

// ✅ 推荐:使用不同的值
public enum StatusEnum implements BaseEnum<Integer> {
    INACTIVE(0, "未激活"),
    DELETED(9, "已删除");  // ✅ 值唯一
}

Q: 如何获取所有枚举值?

A: 使用标准 Java 方法:

java
// 获取所有枚举常量
UserStatusEnum[] values = UserStatusEnum.values();

// 转换为列表
List<UserStatusEnum> list = Arrays.asList(UserStatusEnum.values());

// 获取所有枚举值
List<Integer> valueList = Arrays.stream(UserStatusEnum.values())
    .map(UserStatusEnum::getValue)
    .collect(Collectors.toList());
// [0, 1, 2, 9]

// 构建前端下拉框数据
List<Map<String, Object>> options = Arrays.stream(UserStatusEnum.values())
    .map(e -> Map.of(
        "value", e.getValue(),
        "label", e.getReasonPhrase()
    ))
    .collect(Collectors.toList());
// [{"value":0,"label":"未激活"}, {"value":1,"label":"已激活"}, ...]

💡 提示:

  • 所有枚举必须实现 BaseEnum 接口
  • 必须添加 @JsonCreator 方法支持 JSON 反序列化
  • 使用有意义的枚举值(0/1/2/9 等)
  • 金额字段使用 @NumberToScaleString 避免精度丢失
  • 不推荐使用 @EnumConvertValue 注解

Released under the Apache-2.0 License