Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to protect @ConfigurationProperties classes from changes?

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?

like image 775
Cherry Avatar asked Jun 24 '17 04:06

Cherry


2 Answers

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() { ... }

    }

}
like image 117
davidxxx Avatar answered Sep 18 '22 16:09

davidxxx


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.

like image 26
jihor Avatar answered Sep 20 '22 16:09

jihor