Spring Boot 中的枚举(Enum)映射

1、概览

在本教程中,我们将学习如何在 Spring Boot 中实现不区分大小写的枚举映射。

2、Spring 默认的枚举映射

在处理请求参数时,Spring 依靠几个内置 Converter 来处理字符串转换。

通常情况下,将枚举作为请求参数时,默认会使用 StringToEnumConverterFactory 将传递的字符串转换为枚举。

该 Converter 会调用 Enum.valueOf(Class, String),这意味着给定的字符串必须要完全匹配枚举中的常量实例。

例如,让我们来看看 Level 枚举:

public enum Level {
    LOW, MEDIUM, HIGH
}

接下来,创建一个使用枚举作为参数的 Handler Method:

@RestController
@RequestMapping("enummapping")
public class EnumMappingController {

    @GetMapping("/get")
    public String getByLevel(@RequestParam(required = false) Level level){
        return level.name();
    }

}

使用 CURL 向 http://localhost:8080/enummapping/get?level=MEDIUM 发送一个请求:

curl http://localhost:8080/enummapping/get?level=MEDIUM

Handler Method 会返回 MEDIUM,即枚举实例 MEDIUM 的名称(name())。

现在,让我们传递 medium,看看会发生什么:

curl http://localhost:8080/enummapping/get?level=medium
{"timestamp":"2022-11-18T18:41:11.440+00:00","status":400,"error":"Bad Request","path":"/enummapping/get"}

如你所见,返回了无效请求异常:

Failed to convert value of type 'java.lang.String' to required type 'com.baeldung.enummapping.enums.Level'; 
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.baeldung.enummapping.enums.Level] for value 'medium'; 
...

查看异常堆栈,可以发现 Spring 抛出了 ConversionFailedException。它没有将 medium 识别为枚举实例。

3、不区分大小写的枚举映射

在映射枚举时,Spring 提供了几种方便的方法来解决大小写敏感性问题。

3.1、使用 ApplicationConversionService

ApplicationConversionService 类带有一组已配置好的的 ConverterFormatter

在这些开箱即用的 Converter 中,有一个 StringToEnumIgnoringCaseConverterFactory。顾名思义,它能以大小写不敏感的方式将字符串转换为枚举实例。

首先,添加并配置 ApplicationConversionService

@Configuration
public class EnumMappingConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        ApplicationConversionService.configure(registry);
    }
}

该类使用适合大多数 Spring Boot 应用的 Converter 来配置 FormatterRegistry

测试:

@RunWith(SpringRunner.class)
@WebMvcTest(EnumMappingController.class)
public class EnumMappingIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenPassingLowerCaseEnumConstant_thenConvert() throws Exception {
        mockMvc.perform(get("/enummapping/get?level=medium"))
            .andExpect(status().isOk())
            .andExpect(content().string(Level.MEDIUM.name()));
    }

}

我们可以看到,传递的 medium 参数 已成功转换为 MEDIUM

3.2、使用自定义 Converter

另一种解决方案是使用自定义 Converter。在这里,我们要使用 Apache Commons Lang 3 库。

首先,我们需要添加其依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

这里的基本思路是创建一个 Converter,将表示 Level 实例的字符串转换为实际的 Level 枚举实例:

public class StringToLevelConverter implements Converter<String, Level> {

    @Override
    public Level convert(String source) {
        if (StringUtils.isBlank(source)) {
            return null;
        }
        return EnumUtils.getEnum(Level.class, source.toUpperCase());
    }

}

从技术角度看,自定义 Converter 只是一个实现 Converter<S,T> 接口的简单类。

正如我们所见,我们将 String 对象转换为大写。然后,我们使用 Apache Commons Lang 3 库中的 EnumUtils 工具类从 Level 中获取枚举实例。

最后一步,需要告诉 Spring 我们自定义的 Converter。为此,我们将使用之前的 FormatterRegistry。它提供了 addConverter() 方法来注册自定义 Converter:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToLevelConverter());
}

现在,可以在 ConversionService 中使用 StringToLevelConverter 了。

像使用其他 Converter 一样使用它:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = EnumMappingMainApplication.class)
public class StringToLevelConverterIntegrationTest {

    @Autowired
    ConversionService conversionService;

    @Test
    public void whenConvertStringToLevelEnumUsingCustomConverter_thenSuccess() {
        assertThat(conversionService.convert("low", Level.class)).isEqualTo(Level.LOW);
    }

}

如上所示,测试中的字符串 low 已转换为 Level.LOW 枚举实例。

3.3、使用自定义 Property Editor

Spring 使用多个内置 Property Editor 来管理 String 值和 Java 对象之间的转换。

同样,我们可以创建一个自定义 Property Editor,将 String 对象映射为 Level 枚举实例。

例如,自定义 LevelEditor

public class LevelEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) {
        if (StringUtils.isBlank(text)) {
            setValue(null);
        } else {
            setValue(EnumUtils.getEnum(Level.class, text.toUpperCase()));
        }
    }
}

如上,我们需要继承 PropertyEditorSupport 类并覆写 setAsText() 方法。

覆写 setAsText() 方法的目的是将给定字符串转换为大写后,解析为 Level 枚举实例。

注意,PropertyEditorSupport 还提供了 getAsText() 方法。它在将 Java 对象序列化为字符串时被调用。因此,我们无需在此覆写它。

Spring 不会自动检测自定义的 Property Editor,我们需要手动注册 LevelEditor

在 Spring Controller 中创建一个注解了 @InitBinderinitBinder 方法:

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.registerCustomEditor(Level.class, new LevelEditor());
}

现在,我们把所有组件组装在一起,使用一个测试用例来确认我们的自定义 Property Editor LevelEditor 是否正常工作:

public class LevelEditorIntegrationTest {

    @Test
    public void whenConvertStringToLevelEnumUsingCustomPropertyEditor_thenSuccess() {
        LevelEditor levelEditor = new LevelEditor();
        levelEditor.setAsText("lOw");

        assertThat(levelEditor.getValue()).isEqualTo(Level.LOW);
    }
}

注意,EnumUtils.getEnum() 如果未找到枚举会返回 null 值。

因此,为了避免 NullPointerException,我们需要稍微修改一下 Handler Method:

public String getByLevel(@RequestParam(required = false) Level level) {
    if (level != null) {
        return level.name();
    }
    return "undefined";
}

现在,进行一下测试:

@Test
public void whenPassingUnknownEnumConstant_thenReturnUndefined() throws Exception {
    mockMvc.perform(get("/enummapping/get?level=unknown"))
        .andExpect(status().isOk())
        .andExpect(content().string("undefined"));
}

4、总结

在本文中,我们学习了如何使用内置的 ApplicationConversionService、以及自定义 Converter 或 Property Editor 来实现不区分大消息的枚举映射。


参考:https://www.baeldung.com/spring-boot-enum-mapping