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
类带有一组已配置好的的 Converter
和 Formatter
。
在这些开箱即用的 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 中创建一个注解了 @InitBinder
的 initBinder
方法:
@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