51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot 中的枚举(Enum)映射

1、概览 {#1概览}

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

2、Spring 默认的枚举映射 {#2spring-默认的枚举映射}

在处理请求参数时,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、不区分大小写的枚举映射 {#3不区分大小写的枚举映射}

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

3.1、使用 ApplicationConversionService {#31使用-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 {#32使用自定义-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 {#33使用自定义-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、总结 {#4总结}

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


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

赞(0)
未经允许不得转载:工具盒子 » Spring Boot 中的枚举(Enum)映射