13

How can I check, if ${service.property} is not an empty string and if so, throw some kind of readable exception? It must happen during Bean creation.

@Component
public class Service {

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

I am looking for the easiest way (least written code). Would be great if by using annotations.

My current solution is to perform "handwritten" validation inside setter for the property, but is a little too much code for such easy thing.

Hint: I looked for some way to use the SpEL, since I use it already inside @Value, but as far I have found out, it would not be that easy/clean. But could have overlooked something.

Clarification: Expected behaviour is, that the application will not start up. The goal is to assure, that all properties are set, and especially, that string properties are not empty. Error should say clearily, what is missing. I don't want to set any defaults! User must set it all.

Tomasz
  • 988
  • 1
  • 9
  • 23

3 Answers3

4

What you have there will work. If you don't include the property in your properties file you will receive a org.springframework.beans.factory.BeanCreationException exception on server start up.

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)

The alternative would be to use an initProperty to handle or set the value, here is where you could throw some kind of readable exception.

@Component
public class Service {

    private String property;

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

It really depends if you want the your application to start up regardless if the property is set and if not, throw a readable exception to the log or console then set it with a default value or if you want the error to be thrown at server start-up and bean creation.

I guess the third option would be to just set the value if none was given by using the default setter.

@Component
public class Service {

    @Value("${service.property:'This is my default setter string'}")
    private String property;
}
Shaggy
  • 1,444
  • 1
  • 23
  • 34
  • The initProperty solution is just like setter solution - too much code. I don't want to set any defaults (for example for environment dependent properties). Can I somehow in @Value instead of default value throw an exception? – Tomasz Apr 23 '15 at 08:34
  • You don't have to set the default value, if you use your code `@Value("${service.property}")` and the property is not set it will throw the `org.springframework.beans.factory.BeanCreationException` exception I've shown above. It will clearly state `Could not resolve placeholder 'service.property' in string value "${service.property}"` – Shaggy Apr 23 '15 at 12:07
  • I have added a clarification above: Expected behaviour is, that the application will not start up. The goal is to assure, that all properties are set, and especially, that string properties are not empty. Error should say clearily, what is missing. I don't want to set any defaults! User must set it all. – Tomasz Apr 23 '15 at 17:04
  • I am not stuck on it, I'm looking for best solution - in case someone knows something great. I will now post my solution, not perfect, but maybe helpful for somebody – Tomasz Apr 24 '15 at 07:26
4

You can use the component as a property placeholder itself. And then you may use any validation that you want.

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

    @NotBlank
    private String username;

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

    ...
}

And your my.properties file will look like this:

my.username=felipe
Felipe Desiderati
  • 2,414
  • 3
  • 24
  • 42
0

Here my solution, just put that class in your code (just fix the "my.package" String):

/**
 * 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.");
        }
    }
}

And here the test for it:

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

}
Tomasz
  • 988
  • 1
  • 9
  • 23