Immutable @ConfigurationProperties

后端 未结 7 1901
误落风尘
误落风尘 2020-12-08 13:55

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

@ConfigurationProperties(         


        
相关标签:
7条回答
  • 2020-12-08 13:55

    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
    
    0 讨论(0)
  • 2020-12-08 14:01

    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

    0 讨论(0)
  • 2020-12-08 14:11

    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();
        }
    }
    
    0 讨论(0)
  • 2020-12-08 14:12

    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() { .. }
    }
    
    0 讨论(0)
  • 2020-12-08 14:14

    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.

    0 讨论(0)
  • 2020-12-08 14:17

    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

    0 讨论(0)
提交回复
热议问题