不用 if-else,Spring Boot 怎么知道 ?status=10 是哪个枚举?

几乎在每个项目中,我们都会定义大量的枚举(Enum)来表示状态、类型等。一个常见的实践是为枚举赋予一个数值或字符串 code,以便在数据库和前后端交互中使用,例如:

复制代码
public enum OrderStatusEnum {
    PENDING_PAYMENT(10, "待支付"),
    PROCESSING(20, "处理中"),
    SHIPPED(30, "已发货");
    
    private final int code;
    private final String description;
    // ...
}

但问题来了:当后端 Controller 接收前端传来的参数(如 ?status=10)时,Spring MVC 默认并不知道如何将 10 这个 Integer 自动转换为 OrderStatusEnum.PENDING_PAYMENT。于是,我们的 Controller 代码常常会变成这样:

复制代码
@GetMapping("/orders")
public List<Order> getOrders(@RequestParam Integer status) {
    OrderStatusEnum statusEnum = OrderStatusEnum.fromCode(status); // 手动转换
    if (statusEnum == null) {
        throw new IllegalArgumentException("Invalid status code");
    }
    // ...
}

这种手动转换的代码充满了 if-else 和重复的校验逻辑,非常丑陋。本文将带你构建一个通用的枚举转换 Starter,让你的 Controller 可以直接、优雅地接收枚举类型,彻底告别这些样板代码。

1. 项目设计与核心思路

我们的 enum-converter-starter 目标如下:

    1. 通用性: 无需为每个枚举都写一个转换器,一个 Starter 解决所有问题。
    1. 约定驱动: 只要枚举遵循一个简单的约定(实现一个通用接口),就能被自动识别和转换。
    1. 自动注册: 引入 Starter 依赖后,转换逻辑自动在 Spring MVC 中生效。

核心实现机制:ConverterFactory

Spring 框架提供了一个 ConverterFactory<S, R> 接口。它是一个能创建 Converter<S, T extends R> 实例的工厂。我们可以创建一个 ConverterFactory<String, Enum>,它能为任何 Enum 类型的子类 T 创建一个从 StringT 的转换器。

实现流程:

    1. 定义一个通用接口,如 BaseEnum,它包含一个 getCode() 方法。
    1. 所有需要被自动转换的枚举都实现 BaseEnum 接口。
    1. 创建一个 StringToEnumConverterFactory,它会为所有实现了 BaseEnum 接口的枚举,生成一个能根据 getCode() 的值进行匹配的转换器。
    1. 通过 WebMvcConfigurer 将这个 ConverterFactory 注册到 Spring 的格式化服务中。

2. 创建 Starter 项目与核心组件

我们采用 autoconfigure + starter 的双模块结构。

步骤 2.1: 依赖 (autoconfigure 模块)

这个 Starter 非常轻量,核心依赖只需要 spring-boot-starter-web

复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    </dependencies>
步骤 2.2: 定义约定接口和通用转换工厂

BaseEnum (约定接口):

复制代码
package com.example.converter.autoconfigure.core;

public interface BaseEnum {
    /**
     * 获取枚举的代码值
     * @return code 值 (可以是 Integer, String 等)
     */
    Object getCode();
}

StringToEnumConverterFactory (核心转换逻辑):

复制代码
package com.example.converter.autoconfigure.core;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {

    @Override
    public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
        // 我们只处理实现了 BaseEnum 接口的枚举
        if (!BaseEnum.class.isAssignableFrom(targetType)) {
            // 对于未实现接口的枚举,使用 Spring 默认的转换器 (按名称匹配)
            return new StringToEnumConverter(targetType);
        }
        return new StringToBaseEnumConverter<>(targetType);
    }

    // 内部类,负责将 String 转换为实现了 BaseEnum 的枚举
    private static class StringToBaseEnumConverter<T extends Enum<?>> implements Converter<String, T> {
        private final Class<T> enumType;

        StringToBaseEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                return null;
            }
            for (T enumConstant : enumType.getEnumConstants()) {
                if (enumConstant instanceof BaseEnum) {
                    // 使用 getCode() 的值进行比较
                    if (String.valueOf(((BaseEnum) enumConstant).getCode()).equals(source)) {
                        return enumConstant;
                    }
                }
            }
            return null; // or throw exception
        }
    }
    
    // 内部类,用于兼容 Spring 默认的按名称转换
    private static class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
        private final Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                return null;
            }
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

3. 自动装配的魔法 (EnumConverterAutoConfiguration)

步骤 3.1: 配置属性类
复制代码
@ConfigurationProperties(prefix = "enum.converter")
public class EnumConverterProperties {
    private boolean enabled = true; // 默认开启
    // Getters and Setters...
}
步骤 3.2: 自动配置主类

这个类负责将我们的 ConverterFactory 注册到 Spring MVC。

复制代码
package com.example.converter.autoconfigure;

import com.example.converter.autoconfigure.core.StringToEnumConverterFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableConfigurationProperties(EnumConverterProperties.class)
@ConditionalOnProperty(prefix = "enum.converter", name = "enabled", havingValue = "true", matchIfMissing = true)
public class EnumConverterAutoConfiguration implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 将我们的通用转换工厂注册进去
        registry.addConverterFactory(new StringToEnumConverterFactory());
    }
}
步骤 3.3: 注册自动配置

autoconfigure 模块的 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中添加:

复制代码
com.example.converter.autoconfigure.EnumConverterAutoConfiguration

4. 如何使用我们的 Starter

步骤 4.1: 引入 Starter 依赖

复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>enum-converter-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

步骤 4.2: 让你的枚举实现约定接口

复制代码
import com.example.converter.autoconfigure.core.BaseEnum;

public enum OrderStatusEnum implements BaseEnum {
    PENDING_PAYMENT(10, "待支付"),
    PROCESSING(20, "处理中"),
    SHIPPED(30, "已发货");
    
    private final Integer code;
    private final String description;
    
    OrderStatusEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }
}

步骤 4.3: 在 Controller 中直接接收枚举类型

现在,你的 Controller 可以写得无比清爽:

改造前 (丑陋):

复制代码
// @GetMapping("/orders")
// public List<Order> getOrdersByStatusCode(@RequestParam Integer status) {
//     OrderStatusEnum statusEnum = // ... 手动 if-else 或 switch 转换
//     return orderService.findByStatus(statusEnum);
// }

改造后 (优雅):

复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @GetMapping("/orders")
    public String getOrdersByStatus(@RequestParam OrderStatusEnum status) {
        // Spring MVC 已经自动将请求参数 "10" 转换为了 OrderStatusEnum.PENDING_PAYMENT
        System.out.println("查询状态为: " + status.name());
        return "查询成功,状态为: " + status;
    }
}

验证:

  • • 访问 http://localhost:8080/orders?status=20

  • • 控制台将打印 查询状态为: PROCESSING

  • • 浏览器将收到 查询成功,状态为: PROCESSING

总结

通过自定义一个 Spring Boot Starter 和巧妙地利用 ConverterFactory,我们将繁琐、重复的枚举转换逻辑从业务代码中彻底剥离。这不仅让 Controller 层代码变得更加简洁、类型安全,还通过一个统一的 BaseEnum 接口,在团队内部推行了一套优雅的枚举设计规范。

这个看似小巧的 Starter,是提升代码质量和"开发幸福感"的一大利器,是每一个追求代码洁癖的团队都值得拥有的基础组件。