How to protect @ConfigurationProperties classes from changes?

7.4k views Asked by At

To use @ConfigurationProperties annotation one must create a class with getters and setters like that:

@ConfigurationProperties(prefix = "some")
public class PropertiesConfig {
    private boolean debug;

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }
}

But this leads to the situation when somebody is tempted to modify this value by calling:

@Autowire
private PropertiesConfig config;        
//....

config.setDebug(true);

Is there a way to create @ConfigurationProperties annotated classes without setters and external parser/reader classes?

4

There are 4 answers

1
Strelok On

Not possible out of of the box. @ConfigurationProperties beans have to have standard getters and setters. You might want to consider the approach described in this answer: Immutable @ConfigurationProperties

Or something like this:

@Component
public class ApplicationProperties {

  private final String property1;
  private final String property2;

  public ApplicationProperties(
    @Value("${some.property1"}) String property1,
    @Value("${some.other.property2}) String property2) {
    this.property1 = property1;
    this.property2 = property1;
  }

  //
  // ... getters only ...
  //

}
0
jihor On

One approach with as little boilerplate code as possible would be using an interface with getters only

public interface AppProps {
    String getNeededProperty();
}

and getting rid of boilerplate getters and setters in the implementation with the help of Lombok's @Getter and @Setter annotations :

@ConfigurationProperties(prefix = "props")
@Getter
@Setter
public class AppPropsImpl implements AppProps {
    private String neededProperty;
}

Then, for the bean bo be accessible to other beans only by interface, one can, instead of marking it as @Component or using @EnableConfigurationProperties(AppPropsImpl.class) on the main application class, consider putting it into a configuration which will expose it by interface:

@Configuration
@EnableConfigurationProperties
public class PropsConfiguration  {
    @Bean
    public AppProps appProps(){
        return new AppPropsImpl();
    }
}

Now this bean can be injected only by using an interface, and this makes the setters not available to other beans:

public class ApplicationLogicBean {
    @Autowired
    AppProps props;

    public void method(){
        log.info("Got " + props.getNeededProperty());
    }
}

Tested with Spring Boot 1.5.3 and Lombok 1.16.16.

1
Tomek Wermiński On

Something like this works fine

@Configuration
class MyAppPropertiesConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "some")
    public PropertiesConfig propertiesConfig () {
        return new PropertiesConfigImpl();
    }

    public interface PropertiesConfig {
            public boolean isDebug();
    }

    private static class PropertiesConfigImpl implements PropertiesConfig {
        private boolean debug;

        @Override
        public boolean isDebug() {
            return debug;
        }

        public void setDebug(boolean debug) {
            this.debug = debug;
        }
    }
}

and then

@Autowired PropertiesConfig properties;
0
davidxxx On

From Spring Boot 2.2, it is at last possible to define immutable class decorated with @ConfigurationProperties. Really thanks to Spring developers to improve continuously their framework.
The documentation shows an example.
You just need to declare a constructor with the fields to bind (instead of the setter way) :

@ConstructorBinding
@ConfigurationProperties(prefix = "some")
public class PropertiesConfig {
    private boolean debug;

    public AcmeProperties(boolean enabled) {
        this.enabled = enabled;   
    }

    public boolean isDebug() {
        return debug;
    }

}

Note 1 : you have to define one and only one single constructor with the parameters to bind :

In this setup one, and only one constructor must be defined with the list of properties that you wish to bind and not other properties than the ones in the constructor are bound.

Note 2 : a @DefaultValue was introduced to define default values of an immutable property binding.

Default values can be specified using @DefaultValue and the same conversion service will be applied to coerce the String value to the target type of a missing property.

Here is a more detailed example get from the official documentation :

import java.net.InetAddress;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DefaultValue;
import org.springframework.boot.context.properties.ConstructorBinding;

@ConstructorBinding
@ConfigurationProperties("acme")
public class AcmeProperties {

    private final boolean enabled;

    private final InetAddress remoteAddress;

    private final Security security;

    public AcmeProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }

    public boolean isEnabled() { ... }

    public InetAddress getRemoteAddress() { ... }

    public Security getSecurity() { ... }

    public static class Security {

        private final String username;

        private final String password;

        private final List<String> roles;

        public Security(String username, String password,
                @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }

        public String getUsername() { ... }

        public String getPassword() { ... }

        public List<String> getRoles() { ... }

    }

}