使用 Spring @Value 时如何进行简单的 属性 验证

How to make simple property validation when using Spring @Value

我如何检查 ${service.property} 是否不是空字符串,如果是,抛出某种可读异常?它必须在 Bean 创建期间发生。

@Component
public class Service {

  @Value("${service.property}")
  private String property;
}

我正在寻找最简单 的方法(最少编写的代码)。如果使用注释会很棒。

我目前的解决方案是在 setter 中为 属性 执行 "handwritten" 验证,但是对于这么简单的事情来说代码有点太多了。

提示:我正在寻找使用 SpEL 的方法,因为我已经在 @Value 中使用它,但据我所知,它不会是 easy/clean。但可能忽略了一些东西。

澄清:预期的行为是,应用程序将不会启动。目标是确保所有属性都已设置,尤其是 字符串属性不为空 。错误应该说清楚,缺少什么。我不想设置任何默认值!用户必须全部设置。

你所拥有的一切都会奏效。如果您不在属性文件中包含 属性,您将在服务器启动时收到 org.springframework.beans.factory.BeanCreationException 异常。

Apr 22, 2015 9:47:37 AM org.apache.catalina.core.ApplicationContext log
SEVERE: StandardWrapper.Throwable
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'service': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'service': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private java.lang.String com.util.Service.property; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'service.property' in string value "${service.property}"
    at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:306)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1146)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:519)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)

另一种方法是使用 initProperty 来处理或设置值,在这里您可以抛出某种可读异常。

@Component
public class Service {

    private String property;

    @Autowired
    public void initProperty(@Value("${service.property}") String property) {
        if(property == null) {
            // Error handling here
        }
    }
}

这真的取决于您是否希望您的应用程序启动而不管 属性 是否设置,如果没有,向日志或控制台抛出一个可读的异常,然后将其设置为默认值,或者如果您希望在服务器启动和 bean 创建时抛出错误。

我猜第三个选项是如果 none 是通过使用默认 setter 给出的值。

@Component
public class Service {

    @Value("${service.property:'This is my default setter string'}")
    private String property;
}

这是我的解决方案,只需将 class 放入您的代码中(只需修复 "my.package" 字符串):

/**
 * Validates the environment-dependent properties during application start. Finds all spring beans, which classes are in
 * defined package, validates them and in case of error tries to log the property name (not class field name), taken
 * from {@link Value} annotation.
 * 
 * @author Tomasz
 */
@Component
public class ConfigurationChecker implements ApplicationListener<ContextRefreshedEvent> {
    private static final Logger LOG = LoggerFactory.getLogger(ConfigurationChecker.class);

    // this is a property, that is set in XML, so we bind it here to be found by checker. For properties wired directly in Beans using @Value just add validation constraints
    @Value("${authorization.ldap.url}")
    @NotBlank
    private String ldapUrl;

    private static final String FAIL_FAST_PROPERTY = "hibernate.validator.fail_fast";
    private Validator validator = Validation.byDefaultProvider().configure().addProperty(FAIL_FAST_PROPERTY, "false")
            .buildValidatorFactory().getValidator();

    /**
     * Performs the validation and writes all errors to the log.
     */
    @SneakyThrows
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {

        LOG.info("Validating properties");

        Set<ConstraintViolation<Object>> allViolations = new HashSet<>();

        // Find all spring managed beans (including ConfigurationChecker)...
        for (String beanName : event.getApplicationContext().getBeanDefinitionNames()) {
            Object bean = event.getApplicationContext().getBean(beanName);

            // ...but validate only ours.
            if (bean.getClass().getCanonicalName().startsWith("my.package")) {
                Set<ConstraintViolation<Object>> viol = this.validator.validate(bean);
                LOG.info("Bean '" + beanName + "': " + (viol.isEmpty() ? " OK" : viol.size() + " errors found"));
                allViolations.addAll(viol);
            } else {
                continue;
            }

        }

        // if any error found...
        if (allViolations.size() > 0) {

            for (ConstraintViolation<Object> violation : allViolations) {
                // ...extract "property.name" from field annotation like @Value("${property.name}")
                String propertyName = violation.getLeafBean().getClass()
                        .getDeclaredField(violation.getPropertyPath().toString()).getAnnotation(Value.class).value();
                propertyName = StringUtils.substring(propertyName, 2, -1);

                // .. log it ..
                LOG.error(propertyName + " " + violation.getMessage());
            }

            // ... and do not let the app start up.
            throw new IllegalArgumentException("Invalid configuration detected. Please check the log for details.");
        }
    }
}

这里是测试:

@RunWith(EasyMockRunner.class)
public class ConfigurationCheckerTest extends EasyMockSupport {

    @TestSubject
    private ConfigurationChecker checker = new ConfigurationChecker();

    @Mock
    private ContextRefreshedEvent event;
    @Mock
    private ApplicationContext applicationContext;

    @Test(expected = IllegalArgumentException.class)
    public void test() {

        expect(this.event.getApplicationContext()).andReturn(this.applicationContext).anyTimes();
        expect(this.applicationContext.getBeanDefinitionNames()).andReturn(new String[] { "configurationChecker" });
        expect(this.applicationContext.getBean("configurationChecker")).andReturn(this.checker);

        replayAll();
        this.checker.onApplicationEvent(this.event);

        verifyAll();
    }

}

您可以将组件用作 属性 占位符本身。然后您可以使用任何您想要的验证。

@Component
@Validated
@PropertySource("classpath:my.properties")
@ConfigurationProperties(prefix = "my")
public class MyService {

    @NotBlank
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }

    ...
}

您的 my.properties 文件将如下所示:

my.username=felipe