1. 概述
Spring 提供了自動配置功能,我們可以利用它來綁定組件、配置 Bean 以及從屬性源設置值。
<em @Value</em>> 註解在我們需要避免硬編碼值,並更傾向於使用屬性文件或系統環境變量提供值時非常有用。
在本教程中,我們將學習如何利用 Spring 的自動配置功能將這些值映射到 <em Enum</em>> 實例中。
2. 轉換器<F,T>
Spring 使用轉換器將 String 值從 @Value 映射到所需的類型。一個專門的 BeanPostProcessor 會遍歷所有組件並檢查它們是否需要額外的配置或,在我們的案例中,注入。 之後,會找到合適的轉換器,並將源轉換器中的數據發送到指定的目標。 Spring 提供了一個內置的 String 到 Enum 轉換器,讓我們來回顧一下。
2.1. <em >LenientToEnumConverter</em>
作為名稱所示,這個轉換器在轉換過程中對數據解釋非常寬鬆。最初,它假設值已正確提供。
@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();
}這個過程使轉換器具有很強的適應性,因此如果未加以考慮,可能會引入一些問題。 此外,它還提供對大小寫不敏感匹配的卓越支持,適用於 枚舉,無需進行任何額外的配置。
2.2. 寬鬆轉換 (Lenient Conversion)
讓我們以一個簡單的 枚舉類 (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. 自定義轉換器
使用自定義轉換器是解決映射過程中特定規則的最佳方法。通過創建自定義轉換器實現,可以靈活地處理映射規則。在瞭解了 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. <em >CaseInsensitiveWeekDayConverter</em>
讓我們找到一個平衡點,既能使用不區分大小寫的匹配,又能避免其他任何差異:
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());
}
}
}
我們沒有考慮當枚舉名稱未採用大寫或使用混合大小寫的情況。 然而,這是一個可解決的問題,只需要增加幾行代碼和 try-catch 塊即可。我們可以為枚舉創建查找映射並將其緩存,但我們現在不做這件事。
測試結果將類似,並且會正確地映射值。為了簡單起見,讓我們只檢查使用此轉換器正確映射的屬性:
@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
SpEL 是一種強大的工具,可以做幾乎任何事情。 在解決我們問題時,我們將嘗試調整從屬性文件中接收到的值,然後再嘗試映射 枚舉。為了實現這一點,我們可以顯式地將提供的值更改為大寫:
@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和屬性注入。自定義轉換器可能會使其更強大,允許我們使用它與自定義類型或實現特定約定。