1. 概述
Spring提供了强大的自动配置功能,可用于绑定组件、配置Bean以及从属性源设置值。当需要避免硬编码值,而更倾向于通过属性文件或系统环境提供时,@Value
注解就显得尤为实用。
本教程将探讨如何利用Spring的自动配置机制,将属性值映射到Enum
实例。
2. Converters<F,T>
机制
Spring使用转换器将@Value
中的字符串值转换为所需类型。一个专门的BeanPostProcessor
会遍历所有组件,检查它们是否需要额外配置或注入。随后,系统会找到合适的转换器,将源数据转换为目标类型。 Spring内置了字符串到枚举的转换器,下面我们来分析它。
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);
}
}
但当无法将源数据映射到枚举时,它会尝试另一种方法:获取枚举和值的规范名称:
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. 宽松转换实践
以简单枚举类为例:
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());
}
}
}
这里未考虑枚举名非大写或混合大小写的情况。但这可解决,只需增加几行代码和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和属性注入。自定义转换器可进一步增强其能力,使其支持自定义类型或实现特定约定。
本文所有代码可在GitHub获取。