知識庫 / Spring / Spring Boot RSS 訂閱

Spring Boot 中忽略大小寫綁定枚舉值

Spring Boot
HongKong
5
11:21 AM · Dec 06 ,2025

1. 概述

Spring 提供了自動配置功能,我們可以利用它來綁定組件、配置 Bean 以及從屬性源設置值。

<em @Value</em>> 註解在我們需要避免硬編碼值,並更傾向於使用屬性文件或系統環境變量提供值時非常有用。

在本教程中,我們將學習如何利用 Spring 的自動配置功能將這些值映射到 <em Enum</em>> 實例中。

2. 轉換器<F,T>

Spring 使用轉換器將 String 值從 @Value 映射到所需的類型。一個專門的 BeanPostProcessor 會遍歷所有組件並檢查它們是否需要額外的配置或,在我們的案例中,注入。 之後,會找到合適的轉換器,並將源轉換器中的數據發送到指定的目標。 Spring 提供了一個內置的 StringEnum 轉換器,讓我們來回顧一下。

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和屬性注入。自定義轉換器可能會使其更強大,允許我們使用它與自定義類型或實現特定約定。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.