1.概述

在这个教程中,我们将探讨在Spring Boot中实现不区分大小写的枚举映射的不同方法。

首先,我们会了解Spring默认如何处理枚举映射。然后,我们将学习如何解决大小写敏感性问题。

2. Spring默认的枚举映射

Spring依赖于内置的转换器来处理请求参数的字符串转换。

通常,当我们作为请求参数传递枚举时,它会使用底层的StringToEnumConverterFactory将传递的字符串转换为枚举。

按照设计,这个转换器使用Enum.valueOf(Class, String),这意味着给定的字符串必须精确匹配声明的枚举常量中的一个。

例如,考虑Level枚举:

public enum Level {
    LOW, MEDIUM, HIGH
}

接下来,我们创建一个接受枚举作为参数的处理器方法

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

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

}

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

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

处理器方法返回的是MEDIUM,即枚举常量MEDIUM的名称。

现在,让我们用medium代替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类包含一组预配置的转换器和格式化程序。

这些现成的转换器中,有一个名为StringToEnumIgnoringCaseConverterFactory的,顾名思义,它以不区分大小写的方式将字符串转换为枚举。

首先,我们需要添加并配置ApplicationConversionService

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

这个类配置了FormatterRegistry,为大多数Spring Boot应用提供现成的转换器。

现在,让我们通过测试用例确认一切按预期工作:

@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. 使用自定义转换器

另一种解决方案是使用自定义转换器。这里,我们将使用Apache Commons Lang 3库。

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

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

基本思想是创建一个将枚举常量字符串表示转换为实际枚举常量的转换器:

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<S,T>接口的类。

如上所述,我们先将字符串对象转换为大写,然后使用Apache Commons Lang 3库的EnumUtils工具类从大写字符串获取Level常量。

现在,我们需要完成最后一步。我们需要告诉Spring关于我们的新自定义转换器。为此,我们将使用之前提到的FormatterRegistry。它提供了addConverter()方法来注册自定义转换器:

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

就这样,我们的StringToLevelConverter现在在ConversionService中可用:

@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);
    }

}

现在,我们可以像使用其他转换器一样使用它:

@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. 使用自定义属性编辑器

Spring在后台使用多个内置的属性编辑器来管理字符串值和Java对象之间的转换。

同样,我们可以创建一个自定义属性编辑器来将字符串对象映射为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对象为字符串时会被调用。因此,这里不需要重写它。

我们需要注册我们的LevelEditor,因为Spring不会自动检测自定义属性编辑器。要做到这一点,我们需要在我们的Spring控制器中创建一个带有@InitBinder注解的方法:

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

现在,当我们把所有部分组合在一起,让我们通过测试用例确认我们的自定义属性编辑器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,我们需要稍微修改一下处理器方法:

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. 总结

在这篇文章中,我们学习了在Spring中实现不区分大小写的枚举映射的各种方法。

过程中,我们了解了使用内置和自定义转换器来做到这一点的不同方式。然后,我们看到了如何通过自定义属性编辑器实现相同的目标。

如往常一样,本文中使用的代码可以在GitHub上的教程项目中找到。