1、概览 {#1概览}
Spring 提供了自动配置功能,可以用它来绑定组件、配置 Bean 以及从属性源中设置值。
当我们不想硬编码值,而希望使用 properties 文件或系统环境提供值时,@Value
注解就非常有用。
本文将带你了解如何通过 Spring 自动配置将属性值映射到 Enum
实例,不区分大小写。
2、Converter<F,T>
{#2converterft}
Spring 使用 Converter
将 @Value
注解中的 String
值映射到所需的类型。一个专用的 BeanPostProcessor
会遍历所有组件,并检查它们是否需要额外的配置或注入。然后,找到一个合适的 Converter
,并将源 Converter
中的数据绑定目标。Spring 提供了一个内置的 String
到 Enum
类型的 Converter
,让我们来详细了解一下。
2.1、LenientToEnumConverter {#21lenienttoenumconverter}
顾名思义,该 Converter
在转换过程中可以自由解释数据。最初,它假定提供的数值是正确的:
@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
和 value
的规范名称:
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();
}
这一过程使 Converter
具有很强的适应性,但如果没有考虑到一些问题,可能会引入一些问题。与此同时,它还提供了对大小写不敏感的 Enum
匹配的出色支持,无需任何额外配置。
2.2、宽松的转换 {#22宽松的转换}
以一个简单的 Enum
类为例:
public enum SimpleWeekDays {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
使用 @Value
注解把所有这些常量注入到一个专用的 holder 类中:
@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;
// get、Set 方法省略
}
使用宽松转换,不仅可以使用不同的大小写传递值,而且如前所述,还可以在这些值的周围和内部添加特殊字符,Converter
仍会对其进行映射:
@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、极其宽松的转换 {#23极其宽松的转换}
同时,这种转换方式对两边都有效,即使打破所有命名约定,使用类似如下这样的方式也不会失败:
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 {#3自定义-converter}
在映射过程中处理特定规则的最佳方法是创建自己定义的 Converter 实现。在了解了 LenientToEnumConverter
的功能后,让我们来创建一个限制性更强的 Converter。
3.1、StrictNullableWeekDayConverter {#31strictnullableweekdayconverter}
只有当属性正确标识其名称时,才将值映射到枚举类型。这可能会导致一些最初的问题,因为它没有遵守大写字母约定,但总体而言,这是一个十分可靠的解决方案:
public class StrictNullableWeekDayConverter implements Converter<String, WeekDays> {
@Override
public WeekDays convert(String source) {
try {
return WeekDays.valueOf(source.trim());
} catch (IllegalArgumentException e) {
return null;
}
}
}
该 Converter
会对源字符串进行细微调整。在这里,唯一做的就是去除值周围的空白。另外,注意,返回 null
值并不是最佳的设计决策,因为这会允许在不正确的状态下创建 Context。在这里使用 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 {
}
@Autowired
private WeekDaysHolder holder;
@ParameterizedTest
@ArgumentsSource(WeekDayHolderArgumentsProvider.class)
void givenPropertiesWhenInjectEnumThenValueIsNull(
Function<WeekDaysHolder, WeekDays> methodReference, WeekDays ignored) {
WeekDays actual = methodReference.apply(holder);
assertThat(actual).isNull();
}
}
此时,如果以大写字母提供值,就会注入正确的值。要使用这个 Converter,需要在 Spring 中注册:
public static class WeekDayConverterConfiguration {
@Bean
public ConversionService conversionService() {
DefaultConversionService defaultConversionService = new DefaultConversionService();
// 添加自定义转换器
defaultConversionService.addConverter(new StrictNullableWeekDayConverter());
return defaultConversionService;
}
}
在某些 Spring Boot 版本或配置中,类似的 Converter 可能是默认 Converter,这比 LenientToEnumConverter
更合理。
3.2、CaseInsensitiveWeekDayConverter {#32caseinsensitiveweekdayconverter}
一个折中的方法,既能够进行不区分大小写的匹配,又不允许其他任何差异:
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
块即可。可以为枚举创建一个查找 Map
并将其缓存起来,但是文本不采用。
测试结果看起来很相似,也能正确映射值。为简单起见,这里只检查使用此 Converter
能够正确映射的属性:
@SpringBootTest(properties = {
"monday=monday",
"tuesday=tuesday",
"wednesday=wednesday",
"thursday=THURSDAY",
"friday=Friday",
"saturday=saturDAY",
"sunday=sUndAy",
}, classes = {WeekDaysHolder.class, WeekDayConverterConfiguration.class})
class CaseInsensitiveStringToEnumConverterUnitTest {
// ...
}
使用自定义 Converter,可以根据自己的需求或想要遵循的惯例调整映射过程。
4、SpEL {#4spel}
SpEL 是一个功能强大的工具,几乎无所不能。可以在映射 Enum
之前,调整从 Properties 文件接收到的值,显式地将所提供的值改为大写:
@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;
// Get、Set
}
要检查值是否正确映射,可以使用之前创建的 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);
}
}
尽管 Converter 只能理解大写值,但通过使用 SpEL,可以将属性转换为正确的格式。这种技术对于简单的转换和映射可能很有帮助,因为它直接存在于 @Value
注解中,使用起来相对简单。不过,需要避免在 SpEL 中加入大量复杂的逻辑。
5、总结 {#5总结}
@Value
注解强大而灵活,支持 SpEL 和属性注入。通过自定义 Converter
还可以实现更细粒度的转换控制。
Ref:https://www.baeldung.com/spring-boot-enum-bind-case-insensitive-value