63

Is it possible to have immutable (final) fields with Spring Boot's @ConfigurationProperties annotation? Example below

@ConfigurationProperties(prefix = "example")
public final class MyProps {

  private final String neededProperty;

  public MyProps(String neededProperty) {
    this.neededProperty = neededProperty;
  }

  public String getNeededProperty() { .. }
}

Approaches I've tried so far:

  1. Creating a @Bean of the MyProps class with two constructors
    • Providing two constructors: empty and with neededProperty argument
    • The bean is created with new MyProps()
    • Results in the field being null
  2. Using @ComponentScan and @Component to provide the MyProps bean.
    • Results in BeanInstantiationException -> NoSuchMethodException: MyProps.<init>()

The only way I have got it working is by providing getter/setter for each non-final field.

RJo
  • 15,631
  • 5
  • 32
  • 63
  • 1
    To my knowledge, what you are trying to do will not work out of the box. – geoand Oct 01 '14 at 10:17
  • That's sad. Of course, I can always do it with plain Spring by using constructor parameters with `@Value` annotation. However, it would be nice if Spring Boot supported this, too. – RJo Oct 01 '14 at 11:05
  • I took a small peak at the source code, but it seams non-trivial to support something like what you are asking. Of course I am no expert on Spring internals so I might be missing something obvious – geoand Oct 01 '14 at 12:22
  • 5
    It's not exactly what you're looking for, but this existing Spring Boot issue may be of interest: https://github.com/spring-projects/spring-boot/issues/1254 – Andy Wilkinson Oct 01 '14 at 12:29
  • The solution proposed in the comments would solve my issue too. If the setters were not visible, the configuration properties would be unmodifiable without resorting to violence :) – RJo Oct 02 '14 at 04:45

9 Answers9

49

From Spring Boot 2.2, it is at last possible to define an immutable class decorated with @ConfigurationProperties.
The documentation shows an example.
You just need to declare a constructor with the fields to bind (instead of the setter way) and to add the @ConstructorBinding annotation at the class level to indicate that constructor binding should be used.
So your actual code without any setter is now fine :

@ConstructorBinding
@ConfigurationProperties(prefix = "example")
public final class MyProps {

  private final String neededProperty;

  public MyProps(String neededProperty) {
    this.neededProperty = neededProperty;
  }

  public String getNeededProperty() { .. }
}
gtiwari333
  • 24,554
  • 15
  • 75
  • 102
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • 9
    Note that you now have to use the `@ConstructorBinding` annotation to make this work. Before that (RC1) you had to use the `@ImmutableConfigurationProperties` in stead. For more information about why this annotation was chosen, you can refer to [issue 18563](https://github.com/spring-projects/spring-boot/issues/18563). – g00glen00b Oct 23 '19 at 08:37
  • @g00glen00b Thanks for your comment. I updated with the current way to do that. – davidxxx Apr 18 '20 at 10:56
  • It was very helpful, great answer. Thanks ! – Ravindra Ranwala Apr 05 '21 at 09:06
  • On Spring Boot 3.x you no longer should/can use `@ConstructorBinding` for a single constructor. https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0.0-M2-Release-Notes#constructingbinding-no-longer-needed-at-the-type-level – albertocavalcante Jan 19 '23 at 02:02
17

I have to resolve that problem very often and I use a bit different approach, which allows me to use final variables in a class.

First of all, I keep all my configuration in a single place (class), say, called ApplicationProperties. That class has @ConfigurationProperties annotation with a specific prefix. It is also listed in @EnableConfigurationProperties annotation against configuration class (or main class).

Then I provide my ApplicationProperties as a constructor argument and perform assignment to a final field inside a constructor.

Example:

Main class:

@SpringBootApplication
@EnableConfigurationProperties(ApplicationProperties.class)
public class Application {
    public static void main(String... args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}

ApplicationProperties class

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {

    private String someProperty;

    // ... other properties and getters

   public String getSomeProperty() {
       return someProperty;
   }
}

And a class with final properties

@Service
public class SomeImplementation implements SomeInterface {
    private final String someProperty;

    @Autowired
    public SomeImplementation(ApplicationProperties properties) {
        this.someProperty = properties.getSomeProperty();
    }

    // ... other methods / properties 
}

I prefer this approach for many different reasons e.g. if I have to setup more properties in a constructor, my list of constructor arguments is not "huge" as I always have one argument (ApplicationProperties in my case); if there is a need to add more final properties, my constructor stays the same (only one argument) - that may reduce number of changes elsewhere etc.

I hope that will help

Tom
  • 26,212
  • 21
  • 100
  • 111
2

Using similar approach to the one from https://stackoverflow.com/a/60442151/11770752

But instead of AllArgsConstructor you can use the RequiredArgsConstructor.

Consider following applications.properties

myprops.example.firstName=Peter
myprops.example.last-name=Pan
myprops.example.age=28

Note: Use consistency with your properties, i just wanted to show-case that both were correct (fistName and last-name).


Java Class pickping up the properties

@Getter
@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "myprops.example")
public class StageConfig
{
    private final String firstName;
    private final Integer lastName;
    private final Integer age;

    // ...
}


Additionally you have to add to your build-tool a dependency.

build.gradle

    annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')

or

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>${spring.boot.version}</version>
</dependency>

If you take it one step further to provide nice and precise descriptions for you configurations, consider creating a file additional-spring-configuration-metadata.json in directory src/main/resources/META-INF.

{
  "properties": [
    {
      "name": "myprops.example.firstName",
      "type": "java.lang.String",
      "description": "First name of the product owner from this web-service."
    },
    {
      "name": "myprops.example.lastName",
      "type": "java.lang.String",
      "description": "Last name of the product owner from this web-service."
    },
    {
      "name": "myprops.example.age",
      "type": "java.lang.Integer",
      "description": "Current age of this web-service, since development started."
    }
}

(clean & compile to take effect)


At least in IntelliJ, when you hover over the properties inside application.propoerties, you get a clear despriction of your custom properties. Very useful for other developers.

This is giving me a nice and concise structure of my properties, which i am using in my service with spring.

anjey
  • 93
  • 2
  • 11
2

Just an update on the latest support of more recent Spring-Boot versions:

If you're using a jdk version >= 14, you can use record type which does more or less the same as the Lombok version but without Lombok.

@ConfigurationProperties(prefix = "example")
public record MyProps(String neededProperty) {
}

You can also use record inside the MyProps record to manage nested properties. You can see an example here.

Another interesting post here which shows that the @ConstructorBinding annotation is even not necessary anymore if only one constructor is declared.

рüффп
  • 5,172
  • 34
  • 67
  • 113
1

In the end, if you want an immutable object you can also "hack" the setter that is

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {
    private String someProperty;

    // ... other properties and getters

    public String getSomeProperty() {
       return someProperty;
    }

    public String setSomeProperty(String someProperty) {
      if (someProperty == null) {
        this.someProperty = someProperty;
      }       
    }
}

Obviously if the property is not just a String, that is a mutable object, things are more complicated but that's another story.

Even better you can create a Configuration container

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {
   private final List<MyConfiguration> configurations  = new ArrayList<>();

   public List<MyConfiguration> getConfigurations() {
      return configurations
   }
}

where now the configuration is a clas without

public class MyConfiguration {
    private String someProperty;

    // ... other properties and getters

    public String getSomeProperty() {
       return someProperty;
    }

    public String setSomeProperty(String someProperty) {
      if (this.someProperty == null) {
        this.someProperty = someProperty;
      }       
    }
}

and application.yml as

myapp:
  configurations:
    - someProperty: one
    - someProperty: two
    - someProperty: other
user2688838
  • 761
  • 5
  • 11
  • I think you meant `if (this.someProperty == null) { this.someProperty = someProperty; }` – Rumid Oct 19 '18 at 09:09
  • Your design is not immuteable, it's just guarded against setting twice ie. at point A in time the properties could have a different state than at point B. – Patrick Jan 31 '19 at 14:37
  • [patrickf](https://stackoverflow.com/users/774398/patrickf) you are right, in fact I used the term "immutable" improperly. Thanks for the comment. – user2688838 Jan 31 '19 at 21:09
1

My idea is to encapsulate property groups via inner classes and expose interfaces with getters only.

Properties file:

myapp.security.token-duration=30m
myapp.security.expired-tokens-check-interval=5m

myapp.scheduler.pool-size=2

Code:

@Component
@ConfigurationProperties("myapp")
@Validated
public class ApplicationProperties
{
    private final Security security = new Security();
    private final Scheduler scheduler = new Scheduler();

    public interface SecurityProperties
    {
        Duration getTokenDuration();
        Duration getExpiredTokensCheckInterval();
    }

    public interface SchedulerProperties
    {
        int getPoolSize();
    }

    static private class Security implements SecurityProperties
    {
        @DurationUnit(ChronoUnit.MINUTES)
        private Duration tokenDuration = Duration.ofMinutes(30);

        @DurationUnit(ChronoUnit.MINUTES)
        private Duration expiredTokensCheckInterval = Duration.ofMinutes(10);

        @Override
        public Duration getTokenDuration()
        {
            return tokenDuration;
        }

        @Override
        public Duration getExpiredTokensCheckInterval()
        {
            return expiredTokensCheckInterval;
        }

        public void setTokenDuration(Duration duration)
        {
            this.tokenDuration = duration;
        }

        public void setExpiredTokensCheckInterval(Duration duration)
        {
            this.expiredTokensCheckInterval = duration;
        }

        @Override
        public String toString()
        {
            final StringBuffer sb = new StringBuffer("{ ");
            sb.append("tokenDuration=").append(tokenDuration);
            sb.append(", expiredTokensCheckInterval=").append(expiredTokensCheckInterval);
            sb.append(" }");
            return sb.toString();
        }
    }

    static private class Scheduler implements SchedulerProperties
    {
        @Min(1)
        @Max(5)
        private int poolSize = 1;

        @Override
        public int getPoolSize()
        {
            return poolSize;
        }

        public void setPoolSize(int poolSize)
        {
            this.poolSize = poolSize;
        }

        @Override
        public String toString()
        {
            final StringBuilder sb = new StringBuilder("{ ");
            sb.append("poolSize=").append(poolSize);
            sb.append(" }");
            return sb.toString();
        }
    }

    public SecurityProperties getSecurity()     { return security; }
    public SchedulerProperties getScheduler()   { return scheduler; }

    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder("{ ");
        sb.append("security=").append(security);
        sb.append(", scheduler=").append(scheduler);
        sb.append(" }");
        return sb.toString();
    }
}
dshvets1
  • 119
  • 1
  • 3
0

Using Lombok annotations the code would looks like this:

@ConfigurationProperties(prefix = "example")
@AllArgsConstructor
@Getter
@ConstructorBinding
public final class MyProps {

  private final String neededProperty;

}

Additionally if you want to Autowire this property class directly and not using @Configuration class and @EnableConfigurationProperties, you need to add @ConfigurationPropertiesScan to main application class that is annotated with @SpringBootApplication.

See related documentation here: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-constructor-binding

Radouxca
  • 144
  • 1
  • 7
  • Not bad in term of clarity except you rely on the Lombok external dependency, IMHO the new `record` feature of Java 14 is a better solution now. – рüффп Jun 23 '22 at 20:59
0

If you want to slice test your properties in your application without loading the whole spring boot context use the @EnableConfigurationProperties in your test.

Example:

src/main/resources/application.yml

myApp:
  enabled: true
  name: "test" 
@Getter
@AllArgsConstructor
@Configuration
@ConfigurationProperties(prefix = "myApp")
public class MyApplicationProperties {

    boolean enabled;
    String name;

}

// this will only load MyApplicationProperties.class in spring boot context making it fast
@SpringBootTest( classes = MyApplicationProperties.class})
@EnableConfigurationProperties
class MyApplicationPropertiesTest {

    @Autowired
    MyApplicationProperties myApplicationProperties ;

    @Test
    void test_myApplicationProperties () {
        assertThat(myApplicationProperties.getEnabled()).isTrue();
        assertThat(myApplicationProperties.getName()).isEqualTo("test");

}
Flavio Oliva
  • 401
  • 4
  • 15
-2

You can set the field values through @Value annotations. These can be placed directly on the fields and don't require any setters:

@Component
public final class MyProps {

  @Value("${example.neededProperty}")
  private final String neededProperty;

  public String getNeededProperty() { .. }
}

The downside of this approach is:

  • You'll need to specified the fully-qualified property name on each field.
  • Validation doesn't work (cf. this question)
oberlies
  • 11,503
  • 4
  • 63
  • 110
  • 2
    This won't work. You'll get a `needProperty might have not been initialized` error.. One solution would be to use a constructor with `@Value("${example.neededProperty}") String neededProperty` as a parameter and then initialize neededProperty – RobOhRob Aug 02 '19 at 14:57