Lucas
Lucas

Reputation: 2642

Spring Boot Autowired Configuration Only Null When Validator Bean Used

I have a Spring Boot Configuration class that looks like the following:

package foo.bar;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Configuration @ConfigurationProperties(prefix = "myConfig") public class MyConfig
{

    private String foo;
    private int myValue;

    @NotNull private String requiredString;

    @Min(0) @Max(100) private int smallPositiveInt;

    public void setFoo(String foo) { this.foo = foo; }

    public void setMyValue(int myValue) { this.myValue = myValue; }

    public void setRequiredString(String requiredString) { this.requiredString = requiredString; }

    public void setSmallPositiveInt(int smallPositiveInt)
    {
        this.smallPositiveInt = smallPositiveInt;
    }

    public String getRequiredString() { return requiredString; }

    public int getSmallPositiveInt() { return smallPositiveInt; }

    public String getFoo() { return foo; }

    public int getMyValue() { return myValue; }
}

And a YAML config file that looks like:

server:
  port: 0

myConfig:
  myValue: 9876543
  foo: Goodbye
  requiredString: Here
---

And my SpringApplication code looks like the following:

package foo.bar;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication public class MyService implements ApplicationRunner
{

    // Define the logger object for this class
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired private MyConfig myConfig;

    // myConfig is null when I uncomment these lines:
    //    @Bean
    //    public Validator configurationPropertiesValidator()
    //    {
    //        return new MyConfigValidator();
    //    }

    @Autowired ApplicationContext applicationContext;

    @Override public void run(ApplicationArguments applicationArguments) throws Exception
    {
        log.info("Running application...");

        log.info("MyConfig values:");
        log.info("foo=" + myConfig.getFoo());
        log.info("myValue=" + myConfig.getMyValue());
        log.info("smallPositiveInt=" + myConfig.getSmallPositiveInt());

        log.warn("Example warning log message.");
        log.error("Example error log message.");
        log.debug("Example debug log message.");
    }

    public static void main(String[] args) { SpringApplication.run(MyService.class, args); }
}

When I uncomment the validator the autowired config is null but it works fine when this is commented out. Any ideas what could be happening with the dependency injection?

MyConfigValidator is now completely blank and just implements Validator with no actual functionality I can post it if someone believes that may be the issue.

Update: When I debug the code I can see that validate() is being called in MyConfigValidator with a MyConfig object that has the correct values from the YAML file. Is it possible that Spring injects this object into MyConfigValidator over the Spring Boot Application when it is included in the code as a Bean?

Update 2: After looking at the Spring Boot property validation example I was able to get this to work by creating a static class that implements ApplicationRunner. However, I don't see why this is necessary and would like to avoid doing this.

Upvotes: 1

Views: 1368

Answers (2)

Lucas
Lucas

Reputation: 2642

The approach that seems to work the best for my project is to embed the validator inside the configuration class like so:

@Component 
@ConfigurationProperties(prefix = "myConfig") 
public class MyConfig implements Validator
{

private String foo;
private int myValue;
private String requiredString;
private int smallPositiveInt;

private final Logger log = LoggerFactory.getLogger(this.getClass());

// This means that this validator only supports validating configs of type "MyConfig".
@Override public boolean supports(Class<?> type) { return type == MyConfig.class; }

@Override public void validate(Object o, Errors errors)
{
    MyConfig c = (MyConfig)o;
    log.info("Validating: " + c.toString());
    if(c.getSmallPositiveInt() == 60)
    {
        errors.rejectValue("smallPositiveInt", "error", "Cannot be 60!");
    }
}
}

Please let me know of any downsides to using this approach, it appears to be the best solution when using multiple configuration objects since they seem to be validated whenever they are created via Spring dependency injection.

Upvotes: 1

Phil Webb
Phil Webb

Reputation: 8622

I think the reason that myConfig is null is because configurationPropertiesValidator gets instantiated early. This is causing MyService to get instantiated before AutowiredAnnotationBeanPostProcessor is available to inject fields.

If you make the configurationPropertiesValidator() method static, things should work fine:

@SpringBootApplication 
public class MyService implements ApplicationRunner {

    // ...

    @Bean
    public static Validator configurationPropertiesValidator() {
        return new MyConfigValidator();
    }

    // ...

}

Upvotes: 2

Related Questions