如何实现Spring使用@ConfigurationProperties占位符未解析时启动失败?
很棒的问题!@ConfigurationProperties默认会把无法解析的占位符保留为字面量,这在你需要严格校验必填环境变量时确实很棘手。下面我会分享两种可靠的解决方案,让Spring在启动时如果遇到无法解析的占位符直接失败,不用再做容易出错的字符串检查。
方案一:全局强制占位符解析
如果你希望应用中所有占位符(包括@Value和@ConfigurationProperties中的)都必须能被解析,可以覆盖Spring默认的PropertySourcesPlaceholderConfigurer,拒绝无法解析的占位符。
创建一个配置类来定义这个Bean:
@Configuration public class StrictPlaceholderConfig { @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); // 禁用忽略无法解析的占位符 configurer.setIgnoreUnresolvablePlaceholders(false); return configurer; } }
工作原理:这个Bean会告诉Spring,只要有任何占位符(比如${MY_ENV_VAR})无法从环境变量、配置文件或其他属性源中解析,就立即抛出异常。应用会在完全初始化前启动失败,这正是我们想要的效果。
注意:这是全局设置——应用中所有无法解析的占位符都会触发启动失败。如果你只需要对特定属性做严格校验,建议使用第二种方案。
方案二:针对特定配置属性的自定义校验
如果你需要更精细的控制(只校验@ConfigurationProperties类中的某些属性),可以创建一个自定义校验注解,利用Spring的Environment来检查占位符是否可解析。
步骤1:定义自定义校验注解
import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = ResolvablePlaceholderValidator.class) public @interface ResolvablePlaceholder { String message() default "属性值中存在无法解析的占位符"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
步骤2:实现校验器
这个校验器会利用Spring的Environment尝试解析占位符,如果解析失败,校验就不通过。
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import org.springframework.core.env.Environment; public class ResolvablePlaceholderValidator implements ConstraintValidator<ResolvablePlaceholder, String> { private final Environment environment; // 通过构造方法注入Environment(Spring 4.3+支持校验器的构造方法注入) public ResolvablePlaceholderValidator(Environment environment) { this.environment = environment; } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; // 如果需要强制非空,可以单独添加@NotNull注解,这里根据需求调整 } try { // 使用resolveRequiredPlaceholders方法,若有任何占位符无法解析会抛出异常 environment.resolveRequiredPlaceholders(value); return true; } catch (IllegalArgumentException e) { // 占位符无法解析——校验失败 return false; } } }
步骤3:将注解应用到配置属性类
在你的属性类上添加@Validated,并在需要校验的字段上添加@ResolvablePlaceholder:
import jakarta.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @ConfigurationProperties(prefix = "example") @Validated public class AcmeProperties { @ResolvablePlaceholder @NotNull // 可选:如果需要强制属性非空,可添加此注解 public String key; // Getter、Setter、构造方法... }
处理字面量${}:如果你需要在属性值中包含字面量${...}(不是作为占位符),可以用反斜杠转义:\${my-literal-value}。Spring会将其解析为字面量${my-literal-value},而不会当作占位符处理,因此校验器不会标记它为错误。
为什么这比字符串检查更好:我们没有去检查值是否包含${...}(这种方式会误判合法的属性值),而是直接使用Spring自身的占位符解析逻辑来验证值是否能被完全解析。这更可靠,也和Spring处理属性的内部逻辑保持一致。
如何选择方案?
- 如果希望对整个应用的所有占位符都做严格校验,选择方案一。
- 如果只需要对
@ConfigurationProperties类中的特定属性强制要求占位符可解析,选择方案二。
两种方案都能确保Spring在启动时快速失败,而不是静默地将无法解析的占位符作为字面量使用。
内容的提问来源于stack exchange,提问作者Christian

