不用 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. 1. 通用性: 无需为每个枚举都写一个转换器,一个 Starter 解决所有问题。

  2. 2. 约定驱动: 只要枚举遵循一个简单的约定(实现一个通用接口),就能被自动识别和转换。

  3. 3. 自动注册: 引入 Starter 依赖后,转换逻辑自动在 Spring MVC 中生效。

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

实现流程:

  1. 1. 定义一个通用接口,如 BaseEnum,它包含一个 getCode() 方法。

  2. 2. 所有需要被自动转换的枚举都实现 BaseEnum 接口。

  3. 3. 创建一个 StringToEnumConverterFactory,它会为所有实现了 BaseEnum 接口的枚举,生成一个能根据 getCode() 的值进行匹配的转换器。

  4. 4. 通过 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,是提升代码质量和“开发幸福感”的一大利器,是每一个追求代码洁癖的团队都值得拥有的基础组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值