Spring Boot中的枚举映射

2023/05/12

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(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-12-26T12: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 'cn.tuyucheng.taketoday.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 cn.tuyucheng.taketoday.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);
    }
}

此类使用适用于大多数Spring Boot应用程序的现成转换器配置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 使用自定义转换器

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

首先,我们需要添加它的[依赖](https://search.maven.org/classic/#search ga 1 g%3A”org.apache.commons” AND a%3A”commons-lang3”):
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

这里的基本思想是创建一个转换器,将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<S,T>接口的简单类

如我们所见,我们将String对象转换为大写;然后,我们使用Apache Commons Lang3库中的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);
    }
}

如上所示,测试用例确认“low”值已转换为Level.LOW。

3.3 使用自定义属性编辑器

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

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

我们需要注册我们的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上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章