在 Spring Boot 中將不區分大小寫的 @Value 綁定到 Enum
1. 概述
Spring 為我們提供了自動配置功能,我們可以使用它來綁定元件、配置 bean 以及從屬性來源設定值。
當我們不想對值進行硬編碼並且更喜歡使用屬性檔案或系統環境提供它們時, @Value
註解非常有用。
在本教程中,我們將學習如何利用 Spring 自動配置將這些值對應到Enum
實例。
2. Converters<F,T>
Spring 使用轉換器將String
值從@Value
對應到所需的類型。專用的BeanPostPorcessor
會遍歷所有元件並檢查它們是否需要額外的配置,或者在我們的例子中是否需要注入。之後,找到合適的轉換器,並將來自來源轉換器的資料傳送到指定的目標。 Spring 提供了一個開箱即用的String
到Enum
轉換器,所以讓我們回顧一下它。
2.1. LenientToEnumConverter
顧名思義,該轉換器可以在轉換過程中非常自由地解釋資料。最初,它假設提供的值正確:
@Override
public E convert(T source) {
String value = source.toString().trim();
if (value.isEmpty()) {
return null;
}
try {
return (E) Enum.valueOf(this.enumType, value);
}
catch (Exception ex) {
return findEnum(value);
}
}
但是,如果無法將來源對應到Enum
,它會嘗試不同的方法。它取得Enum
和值的規範名稱:
private E findEnum(String value) {
String name = getCanonicalName(value);
List<String> aliases = ALIASES.getOrDefault(name, Collections.emptyList());
for (E candidate : (Set<E>) EnumSet.allOf(this.enumType)) {
String candidateName = getCanonicalName(candidate.name());
if (name.equals(candidateName) || aliases.contains(candidateName)) {
return candidate;
}
}
throw new IllegalArgumentException("No enum constant " + this.enumType.getCanonicalName() + "." + value);
}
getCanonicalName(String)
過濾掉所有特殊字元並將字串轉換為小寫:
private String getCanonicalName(String name) {
StringBuilder canonicalName = new StringBuilder(name.length());
name.chars()
.filter(Character::isLetterOrDigit)
.map(Character::toLowerCase)
.forEach((c) -> canonicalName.append((char) c));
return canonicalName.toString();
}
這個過程使得轉換器具有很強的適應性,因此如果不考慮的話可能會引入一些問題。同時,它免費為Enum
不區分大小寫匹配提供了極好的支持,無需任何額外的配置。
2.2.寬鬆的轉換
我們以一個簡單的Enum
類別為例:
public enum SimpleWeekDays {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
我們將使用@Value
註解將所有這些常數注入到專用的類別持有者中:
@Component
public class WeekDaysHolder {
@Value("${monday}")
private WeekDays monday;
@Value("${tuesday}")
private WeekDays tuesday;
@Value("${wednesday}")
private WeekDays wednesday;
@Value("${thursday}")
private WeekDays thursday;
@Value("${friday}")
private WeekDays friday;
@Value("${saturday}")
private WeekDays saturday;
@Value("${sunday}")
private WeekDays sunday;
// getters and setters
}
使用寬鬆的轉換,我們不僅可以使用不同的大小寫傳遞值,而且如之前所示,我們可以在這些值的周圍和內部添加特殊字符,並且轉換器仍然會映射它們:
@SpringBootTest(properties = {
"monday=Mon-Day!",
"tuesday=TuesDAY#",
"wednesday=Wednes@day",
"thursday=THURSday^",
"friday=Fri:Day_%",
"saturday=Satur_DAY*",
"sunday=Sun+Day",
}, classes = WeekDaysHolder.class)
class LenientStringToEnumConverterUnitTest {
@Autowired
private WeekDaysHolder propertyHolder;
@ParameterizedTest
@ArgumentsSource(WeekDayHolderArgumentsProvider.class)
void givenPropertiesWhenInjectEnumThenValueIsPresent(
Function<WeekDaysHolder, WeekDays> methodReference, WeekDays expected) {
WeekDays actual = methodReference.apply(propertyHolder);
assertThat(actual).isEqualTo(expected);
}
}
這不一定是一件好事,特別是如果它對開發人員隱藏的話。不正確的假設可能會產生難以識別的微妙問題。
2.3.極為寬鬆的轉換
同時,這種類型的轉換對雙方都有效,即使我們打破所有命名約定並使用以下內容也不會失敗:
public enum NonConventionalWeekDays {
Mon$Day, Tues$DAY_, Wednes$day, THURS$day_, Fri$Day$_$, Satur$DAY_, Sun$Day
}
這種情況的問題是它可能會產生正確的結果並將所有值映射到其專用枚舉:
@SpringBootTest(properties = {
"monday=Mon-Day!",
"tuesday=TuesDAY#",
"wednesday=Wednes@day",
"thursday=THURSday^",
"friday=Fri:Day_%",
"saturday=Satur_DAY*",
"sunday=Sun+Day",
}, classes = NonConventionalWeekDaysHolder.class)
class NonConventionalStringToEnumLenientConverterUnitTest {
@Autowired
private NonConventionalWeekDaysHolder holder;
@ParameterizedTest
@ArgumentsSource(NonConventionalWeekDayHolderArgumentsProvider.class)
void givenPropertiesWhenInjectEnumThenValueIsPresent(
Function<NonConventionalWeekDaysHolder, NonConventionalWeekDays> methodReference, NonConventionalWeekDays expected) {
NonConventionalWeekDays actual = methodReference.apply(holder);
assertThat(actual).isEqualTo(expected);
}
}
繪製“Mon-Day!”
沒有失敗的“Mon$Day”
可能會隱藏問題,並建議開發人員跳過既定的約定。儘管它適用於不區分大小寫的映射,但這些假設過於瑣碎。
3. 自訂轉換器
在映射過程中解決特定規則的最佳方法是建立Converter.
在見證了LenientToEnumConverter
的功能之後,讓我們退後幾步,創建一些更具限制性的東西。
3.1. StrictNullableWeekDayConverter
想像一下,只有當屬性正確識別其名稱時,我們才決定將值對應到枚舉。這可能會導致一些不遵守大寫約定的初始問題,但總的來說,這是一個萬無一失的解決方案:
public class StrictNullableWeekDayConverter implements Converter<String, WeekDays> {
@Override
public WeekDays convert(String source) {
try {
return WeekDays.valueOf(source.trim());
} catch (IllegalArgumentException e) {
return null;
}
}
}
此轉換器將對來源字串進行細微調整。在這裡,我們唯一要做的就是修剪值周圍的空白。另請注意,傳回 null 並不是最佳設計決策,因為它會允許建立處於錯誤狀態的上下文。但是,我們在這裡使用 null 來簡化測試:
@SpringBootTest(properties = {
"monday=monday",
"tuesday=tuesday",
"wednesday=wednesday",
"thursday=thursday",
"friday=friday",
"saturday=saturday",
"sunday=sunday",
}, classes = {WeekDaysHolder.class, WeekDayConverterConfiguration.class})
class StrictStringToEnumConverterNegativeUnitTest {
public static class WeekDayConverterConfiguration {
// configuration
}
@Autowired
private WeekDaysHolder holder;
@ParameterizedTest
@ArgumentsSource(WeekDayHolderArgumentsProvider.class)
void givenPropertiesWhenInjectEnumThenValueIsNull(
Function<WeekDaysHolder, WeekDays> methodReference, WeekDays ignored) {
WeekDays actual = methodReference.apply(holder);
assertThat(actual).isNull();
}
}
同時,如果我們提供大寫的值,則會注入正確的值。要使用這個轉換器,我們需要告訴 Spring:
public static class WeekDayConverterConfiguration {
@Bean
public ConversionService conversionService() {
DefaultConversionService defaultConversionService = new DefaultConversionService();
defaultConversionService.addConverter(new StrictNullableWeekDayConverter());
return defaultConversionService;
}
}
在某些 Spring Boot 版本或配置中,類似的轉換器可能是預設轉換器,這比LenientToEnumConverter.
3.2. CaseInsensitiveWeekDayConverter
讓我們找到一個愉快的中間立場,我們將能夠使用不區分大小寫的匹配,但同時不允許任何其他差異:
public class CaseInsensitiveWeekDayConverter implements Converter<String, WeekDays> {
@Override
public WeekDays convert(String source) {
try {
return WeekDays.valueOf(source.trim());
} catch (IllegalArgumentException exception) {
return WeekDays.valueOf(source.trim().toUpperCase());
}
}
}
我們不考慮Enum
名稱不是大寫或使用混合大小寫的情況。然而,這是一個可以解決的情況,並且只需要額外的幾行和 try-catch 區塊。我們可以為枚舉創建一個查找Enum
並緩存它,但讓我們這樣做吧。
測試看起來相似並且會正確映射值。為簡單起見,我們只檢查使用此轉換器正確映射的屬性:
@SpringBootTest(properties = {
"monday=monday",
"tuesday=tuesday",
"wednesday=wednesday",
"thursday=THURSDAY",
"friday=Friday",
"saturday=saturDAY",
"sunday=sUndAy",
}, classes = {WeekDaysHolder.class, WeekDayConverterConfiguration.class})
class CaseInsensitiveStringToEnumConverterUnitTest {
// ...
}
使用自訂轉換器,我們可以根據我們的需求或我們想要遵循的約定來調整映射過程。
4. 斯佩爾
SpEL 是一個強大的工具,幾乎可以做任何事情。在我們的問題中,在嘗試映射Enum
之前,我們將嘗試調整從屬性檔案收到的值。為了實現這一點,我們可以明確地將提供的值改為大寫:
@Component
public class SpELWeekDaysHolder {
@Value("#{'${monday}'.toUpperCase()}")
private WeekDays monday;
@Value("#{'${tuesday}'.toUpperCase()}")
private WeekDays tuesday;
@Value("#{'${wednesday}'.toUpperCase()}")
private WeekDays wednesday;
@Value("#{'${thursday}'.toUpperCase()}")
private WeekDays thursday;
@Value("#{'${friday}'.toUpperCase()}")
private WeekDays friday;
@Value("#{'${saturday}'.toUpperCase()}")
private WeekDays saturday;
@Value("#{'${sunday}'.toUpperCase()}")
private WeekDays sunday;
// getters and setters
}
要檢查值是否正確映射,我們可以使用先前建立的StrictNullableWeekDayConverter
:
@SpringBootTest(properties = {
"monday=monday",
"tuesday=tuesday",
"wednesday=wednesday",
"thursday=THURSDAY",
"friday=Friday",
"saturday=saturDAY",
"sunday=sUndAy",
}, classes = {SpELWeekDaysHolder.class, WeekDayConverterConfiguration.class})
class SpELCaseInsensitiveStringToEnumConverterUnitTest {
public static class WeekDayConverterConfiguration {
@Bean
public ConversionService conversionService() {
DefaultConversionService defaultConversionService = new DefaultConversionService();
defaultConversionService.addConverter(new StrictNullableWeekDayConverter());
return defaultConversionService;
}
}
@Autowired
private SpELWeekDaysHolder holder;
@ParameterizedTest
@ArgumentsSource(SpELWeekDayHolderArgumentsProvider.class)
void givenPropertiesWhenInjectEnumThenValueIsNull(
Function<SpELWeekDaysHolder, WeekDays> methodReference, WeekDays expected) {
WeekDays actual = methodReference.apply(holder);
assertThat(actual).isEqualTo(expected);
}
}
儘管轉換器僅理解大寫值,但透過使用 SpEL,我們可以將屬性轉換為正確的格式。該技術可能對簡單的轉換和映射很有幫助,因為它直接出現在@Value
註解中並且使用起來相對簡單。但是,請避免將大量複雜的邏輯放入 SpEL 中。
5. 結論
@Value
註解功能強大且靈活,支援SpEL和屬性注入。自訂轉換器可能會使其更加強大,允許我們將其與自訂類型一起使用或實現特定的約定。
與往常一樣,本教程中的所有程式碼都可以在 GitHub 上取得。