Bind Case Insensitive @Value to Enum in Spring Boot – 在 Spring Boot 中将大小写不敏感的 @Value 绑定到枚举

最后修改: 2024年 1月 1日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring provides us with autoconfiguration features that we can use to bind components, configure beans, and set values from a property source.

Spring 为我们提供了自动配置功能,我们可以用它来绑定组件、配置 Bean 以及从属性源中设置值。

@Value annotation is useful when we don’t want to cannot hardcode the values and prefer to provide them using property files or the system environment.

@Value 注解在我们不想硬编码值,而希望使用属性文件或系统环境提供值时非常有用。

In this tutorial, we’ll learn how to leverage Spring autoconfiguration to map these values to Enum instances.

在本教程中,我们将学习如何利用 Spring 自动配置将这些值映射到 Enum 实例。

2. Converters<F,T>

2.转换器<F,T></em

Spring uses converters to map the String values from @Value to the required type. A dedicated BeanPostPorcessor goes through all the components and checks if they require additional configuration or, in our case, injection. After that, a suitable converter is found, and the data from the source converter is sent to the specified target. Spring provides a String to Enum converter out of the box, so let’s review it.

Spring 使用转换器将 @Value 中的 String 值映射到所需的类型。一个专用的 BeanPostPorcessor 会检查所有组件,并检查它们是否需要额外的配置,或者在我们的案例中,是否需要注入。之后,将找到一个合适的转换器,并将源转换器中的数据发送到指定的目标。Spring 提供了一个从 StringEnum 的转换器,让我们来回顾一下。

2.1. LenientToEnumConverter

2.1. LenientToEnumConverter.

As the name suggests, this converter is quite free to interpret the data during conversion. Initially, it assumes that the values are provided correctly:

顾名思义,该转换器在转换过程中可以自由解释数据。最初,它假定提供的数值是正确的:

@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);
    }
}

However, it tries a different approach if it cannot map the source to an Enum. It gets the canonical names for both Enum and the 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);
}

The getCanonicalName(String) filters out all special characters and converts the string to lowercase:

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();
}

This process makes the converter quite adaptive, so it might introduce some problems if not considered. At the same time, it provides excellent support for case-insensitive matching for Enum for free, without any additional configuration required.

这一过程使转换器具有很强的适应性,因此如果不加以考虑,可能会带来一些问题。与此同时,它免费为 Enum 提供了对大小写不敏感匹配的出色支持,无需任何额外配置。

2.2. Lenient Conversion

2.2.宽松转换

Let’s take a simple Enum class as an example:

让我们以一个简单的 Enum 类为例:

public enum SimpleWeekDays {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

We’ll inject all these constants into a dedicated class-holder using @Value annotation:

我们将使用 @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
}

Using lenient conversion, we can not only pass the values using a different case, but as was shown previously, we can add special characters around and inside these values, and the converter will still map them:

使用宽松转换,我们不仅可以使用不同的大小写传递值,而且如前所述,我们还可以在这些值的周围和内部添加特殊字符,转换器仍会对其进行映射:

@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);
    }
}

It’s not necessarily a good thing to do, especially if it’s hidden from developers. Incorrect assumptions can create subtle problems that are hard to identify.

这并不一定是件好事,尤其是在对开发人员隐瞒的情况下。不正确的假设会产生难以识别的微妙问题。

2.3. Extremely Lenient Conversion

2.3.极其宽松的转换

At the same time, this type of conversion works for both sides and won’t fail even if we break all the naming conventions and use something like this:

同时,这种转换方式对双方都有效,即使我们打破所有命名约定,使用类似这样的方式也不会失败:

public enum NonConventionalWeekDays {
    Mon$Day, Tues$DAY_, Wednes$day, THURS$day_, Fri$Day$_$, Satur$DAY_, Sun$Day
}

The issue with this case is that it might yield the correct result and map all the values to their dedicated enums:

这种情况的问题在于,它可能会产生正确的结果,并将所有值映射到其专用的枚举中:

@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);
    }
}

Mapping “Mon-Day!” to “Mon$Day” without failing might hide issues and suggest developers skip the established conventions. Although it works with case-insensitive mapping, the assumptions are too frivolous.

“Mon-Day!”映射为“Mon$Day”而不失败可能会隐藏问题,并建议开发人员跳过既定的惯例。

3. Custom Converters

3.定制转换器

The best way to address specific rules during mappings is to create our implementation of a Converter. After witnessing what LenientToEnumConverter is capable of, let’s take a few steps back and create something more restrictive.

在映射过程中处理特定规则的最佳方法是创建我们的 Converter 实现。LenientToEnumConverter 的功能后,让我们退后几步,创建一个限制性更强的功能。

3.1. StrictNullableWeekDayConverter

3.1.StrictNullableWeekDayConverter</em

Imagine that we decided to map values to the enums only if the properties correctly identify their names. This might cause some initial problems with not respecting the uppercase convention, but overall, this is a bulletproof solution:

设想一下,我们决定只有在属性正确标识了枚举名称的情况下,才将值映射到枚举中。这可能会造成一些不遵守大写约定的初期问题,但总体而言,这是一个防不胜防的解决方案:

public class StrictNullableWeekDayConverter implements Converter<String, WeekDays> {
    @Override
    public WeekDays convert(String source) {
        try {
            return WeekDays.valueOf(source.trim());
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
}

This converter will make minor adjustments to the source string. Here, the only thing we do is trim whitespace around the values. Also, note that returning null isn’t the best design decision, as it would allow the creation of a context in an incorrect state. However, we’re using nulls here to simplify testing:

该转换器会对源字符串进行细微调整。在这里,我们唯一要做的就是修剪值周围的空白。此外,请注意返回空值并不是最佳的设计决策,因为它将允许在不正确的状态下创建上下文。不过,我们在这里使用空值是为了简化测试:

@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();
    }
}

At the same time, if we provide the values in uppercase, the correct values would be injected. To use this converter, we need to tell Spring about it:

同时,如果我们以大写字母提供值,就会注入正确的值。要使用这个转换器,我们需要告诉 Spring 有关它的信息:

public static class WeekDayConverterConfiguration {
    @Bean
    public ConversionService conversionService() {
        DefaultConversionService defaultConversionService = new DefaultConversionService();
        defaultConversionService.addConverter(new StrictNullableWeekDayConverter());
        return defaultConversionService;
    }
}

In some Spring Boot versions or configurations, a similar converter may be a default one, which makes more sense than LenientToEnumConverter.

在某些 Spring Boot 版本或配置中,类似的转换器可能是默认转换器,这比 LenientToEnumConverter. 更合理。

3.2. CaseInsensitiveWeekDayConverter

3.2 大小写敏感周日转换器</em

Let’s find a happy middle ground where we’ll be able to use case-insensitive matching but at the same time won’t allow any other differences:

让我们找到一个快乐的中间点,既能使用不区分大小写的匹配,又不允许任何其他差异:

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());
        }
    }
}

We’re not considering the situation when Enum names aren’t in uppercase or using mixed case. However, this would be a solvable situation and would require only an additional couple of lines and try-catch blocks. We could create a lookup map for the Enum and cache it, but let’s do it.

我们没有考虑 Enum 名称不是大写字母或使用混合大小写的情况。不过,这种情况是可以解决的,只需增加几行代码和 try-catch 块即可。我们可以为 Enum 创建一个查找映射,并将其缓存起来,不过我们还是这样做吧。

The tests would look similar and would correctly map the values. For simplicity, let’s check only the properties that would be correctly mapped using this converter:

测试结果看起来很相似,也能正确映射数值。为简单起见,我们只检查使用此转换器可以正确映射的属性:

@SpringBootTest(properties = {
    "monday=monday",
    "tuesday=tuesday",
    "wednesday=wednesday",
    "thursday=THURSDAY",
    "friday=Friday",
    "saturday=saturDAY",
    "sunday=sUndAy",
}, classes = {WeekDaysHolder.class, WeekDayConverterConfiguration.class})
class CaseInsensitiveStringToEnumConverterUnitTest {
    // ...
}

Using custom converters, we can adjust the mapping process based on our needs or conventions we want to follow.

使用自定义转换器,我们可以根据自己的需求或想要遵循的惯例调整映射过程。

4. SpEL

4.播放

SpEL is a powerful tool that can do almost anything. In the context of our problem, we’ll try to adjust the values we receive from a property file before we try to map Enum. To achieve this, we can explicitly change the provided values to upper-case:

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
}

To check that the values are mapped correctly, we can use the StrictNullableWeekDayConverter we created before:

要检查值是否正确映射,我们可以使用之前创建的 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);
    }
}

Although the converter understands only upper-case values, by using SpEL, we convert the properties to the correct format. This technique might be helpful for simple translations and mappings, as it’s present directly in the @Value annotation and is relatively straightforward to use. However, avoid putting a lot of complex logic into SpEL.

虽然转换器只能理解大写值,但通过使用 SpEL,我们可以将属性转换为正确的格式。这种技术可能有助于简单的转换和映射,因为它直接存在于 @Value 注解中,使用起来相对简单。

5. Conclusion

5.结论

@Value annotation is powerful and flexible, supporting SpEL and property injection. Custom converters might make it even more powerful, allowing us to use it with custom types or implement specific conventions.

@Value 注解强大而灵活,支持 SpEL 和属性注入。自定义转换器可能会使其功能更加强大,使我们可以将其用于自定义类型或实现特定的约定。

As usual, all the code in this tutorial is available over on GitHub.

与往常一样,本教程中的所有代码均可在 GitHub 上获取。