1. 概述
本教程將探討在 Spring Boot 中實現不區分大小寫的枚舉映射的不同方法。
首先,我們將瞭解枚舉在 Spring 中默認是如何映射的。然後,我們將學習如何解決大小寫敏感問題。
2. Spring 中默認枚舉映射
Spring 依賴於內置轉換器來處理請求參數中的字符串轉換。
通常,當我們通過請求參數傳遞一個枚舉時,它會使用 <a href="https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java">StringToEnumConverterFactory</a> 在其底層進行字符串的轉換到枚舉。
通過設計,這個轉換器被稱為 <em>Enum.valueOf(Class, String)</em>, 這意味着提供的字符串必須與聲明的枚舉常量完全匹配。
例如,讓我們考慮 <em>Level</em> 枚舉:
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
<em >ApplicationConversionService</em > 類自帶了一組配置轉換器和格式器。
在這些內置轉換器中,我們找到 `StringToEnumIgnoringCaseConverterFactory。正如其名稱所示,它以不區分大小寫的方式將字符串轉換為枚舉。
首先,我們需要添加和配置 <em >ApplicationConversionService</em >:
@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>基本思路是創建一個轉換器,將字符串形式的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 Lang 3 庫中的 EnumUtils 工具類,從大寫字符串中獲取 Level 常量。
現在,讓我們添加拼圖的最後一塊。我們需要告訴 Spring 關於我們新的自定義轉換器。為此,我們將使用之前相同的 FormatterRegistry。它提供了 addConverter() 方法,用於註冊自定義轉換器:
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToLevelConverter());
}<p>這就是全部。我們的 <em >StringToLevelConverter</em> 現在已在 <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/ConversionService.html"><em >ConversionService</em></a > 中可用。</p>
<p>現在,我們可以像使用任何其他轉換器一樣使用它:</p>
@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);
}
}如上所示,測試用例確認了 “低” 值被轉換為 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 不會自動檢測自定義 property editor。為此,我們需要在我們的 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 中不區分大小寫的枚舉映射的方法。
沿途,我們探討了使用內置和自定義轉換器來實現這些方法。此外,我們還看到了如何使用自定義屬性編輯器來實現相同的目標。