Inmutable @ConfigurationProperties

8 minutos de lectura

¿Es posible tener campos inmutables (finales) con Spring Boot? @ConfigurationProperties ¿anotación? Ejemplo a continuación

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

Enfoques que he probado hasta ahora:

  1. Creando un @Bean del MyProps clase con dos constructores
    • Proporcionar dos constructores: vacío y con neededProperty argumento
    • El frijol se crea con new MyProps()
    • Resultados en el campo siendo null
  2. Usando @ComponentScan y @Component para proporcionar el MyProps frijol.
    • Resultados en BeanInstantiationException -> NoSuchMethodException: MyProps.<init>()

La única forma en que lo tengo funcionando es proporcionando getter/setter para cada campo no final.

  • Que yo sepa, lo que está tratando de hacer no funcionará de inmediato.

    – geoand

    1 oct 2014 a las 10:17

  • Eso es triste. Por supuesto, siempre puedo hacerlo con Spring simple usando parámetros de constructor con @Value anotación. Sin embargo, sería bueno que Spring Boot también admitiera esto.

    – RJo

    1 oct 2014 a las 11:05

  • Tomé un pequeño pico en el código fuente, pero no parece trivial para respaldar algo como lo que está preguntando. Por supuesto, no soy un experto en los componentes internos de Spring, por lo que podría estar perdiéndome algo obvio.

    – geoand

    1 oct 2014 a las 12:22

  • No es exactamente lo que está buscando, pero este problema existente de Spring Boot puede ser de su interés: github.com/spring-projects/spring-boot/issues/1254

    –Andy Wilkinson

    1 oct 2014 a las 12:29

  • La solución propuesta en los comentarios también resolvería mi problema. Si los setters no estuvieran visibles, las propiedades de configuración no se podrían modificar sin recurrir a la violencia 🙂

    – RJo

    2 de octubre de 2014 a las 4:45

avatar de usuario de davidxxx
davidxxx

Desde Spring Boot 2.2, por fin es posible definir una clase inmutable decorada con @ConfigurationProperties.
La documentación muestra un ejemplo.
Solo necesita declarar un constructor con los campos para vincular (en lugar de la forma de establecimiento) y agregar el @ConstructorBinding anotación en el nivel de clase para indicar que se debe usar el enlace del constructor.
Entonces, su código real sin ningún setter ahora está bien:

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

  • Tenga en cuenta que ahora tiene que usar el @ConstructorBinding anotación para hacer este trabajo. Antes de eso (RC1) tenías que usar el @ImmutableConfigurationProperties en cambio. Para obtener más información acerca de por qué se eligió esta anotación, puede consultar número 18563.

    – g00glen00b

    23 de octubre de 2019 a las 8:37


  • @g00glen00b Gracias por tu comentario. Actualicé con la forma actual de hacerlo.

    – davidxxx

    18 de abril de 2020 a las 10:56

  • Fue muy útil, gran respuesta. Gracias !

    –Ravindra Ranwala

    5 de abril de 2021 a las 9:06

Tengo que resolver ese problema muy a menudo y uso un enfoque un poco diferente, lo que me permite usar final variables en una clase.

En primer lugar, mantengo toda mi configuración en un solo lugar (clase), digamos, llamado ApplicationProperties. esa clase tiene @ConfigurationProperties anotación con un prefijo específico. También figura en @EnableConfigurationProperties anotación contra la clase de configuración (o clase principal).

Luego proporciono mi ApplicationProperties como un argumento constructor y realizar la asignación a un final campo dentro de un constructor.

Ejemplo:

Principal clase:

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

ApplicationProperties clase

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

    private String someProperty;

    // ... other properties and getters

   public String getSomeProperty() {
       return someProperty;
   }
}

Y una clase con propiedades finales.

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

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

    // ... other methods / properties 
}

Prefiero este enfoque por muchas razones diferentes, por ejemplo, si tengo que configurar más propiedades en un constructor, mi lista de argumentos del constructor no es “enorme”, ya que siempre tengo un argumento (ApplicationProperties en mi caso); si es necesario agregar más final properties, mi constructor permanece igual (solo un argumento), lo que puede reducir la cantidad de cambios en otros lugares, etc.

Espero que le ayudará

  • Eso es mucha placa de caldera frente a solo usar @Value

    –Peter Davis

    9 de noviembre de 2016 a las 19:56

  • Esto es Java. Más repetitivo significa mejor código

    – Clijsters

    6 de febrero de 2018 a las 14:55


  • @Clijsters Honestamente, no puedo decir si estás bromeando, pero quiero decir, no es del todo correcto, ¡pero tampoco está lejos!

    – Dylan Watson

    12 de febrero de 2018 a las 0:34

  • ¡Sí! Estaba destinado a ser gracioso (pero a menudo hay algo real en una broma).

    – Clijsters

    12 de febrero de 2018 a las 7:35

avatar de usuario de user2688838
usuario2688838

Al final, si quieres un objeto inmutable, también puedes “hackear” el setter que está

@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;
      }       
    }
}

Obviamente, si la propiedad no es solo una cadena, es un objeto mutable, las cosas son más complicadas, pero esa es otra historia.

Aún mejor, puede crear un contenedor de configuración

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

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

donde ahora la configuración es una clase sin

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;
      }       
    }
}

y aplicación.yml como

myapp:
  configurations:
    - someProperty: one
    - someProperty: two
    - someProperty: other

  • creo que quisiste decir if (this.someProperty == null) { this.someProperty = someProperty; }

    – Rúmido

    19 oct 2018 a las 9:09

  • Su diseño no es inmutable, solo está protegido contra la configuración dos veces, es decir. en el punto A en el tiempo las propiedades podrían tener un estado diferente que en el punto B.

    – Patrick Favre

    31 de enero de 2019 a las 14:37

  • patrickf tienes razón, de hecho utilicé el término “inmutable” incorrectamente. Gracias por el comentario.

    – usuario2688838

    31 de enero de 2019 a las 21:09

Mi idea es encapsular grupos de propiedades a través de clases internas y exponer interfaces solo con captadores.

Archivo de propiedades:

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

myapp.scheduler.pool-size=2

Código:

@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();
    }
}

Usando un enfoque similar al de https://stackoverflow.com/a/60442151/11770752

pero en lugar de AllArgsConstructor puedes usar el RequiredArgsConstructor.

Considere seguir applications.properties

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

Nota: Use la coherencia con sus propiedades, solo quería mostrar el caso de que ambas eran correctas (fistName y last-name).


Java Class recogiendo las propiedades

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

    // ...
}


Además, debe agregar una dependencia a su herramienta de compilación.

construir.gradle

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

o

pom.xml

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

Si va un paso más allá para proporcionar descripciones agradables y precisas para sus configuraciones, considere crear un archivo additional-spring-configuration-metadata.json en directorio src/main/resources/META-INF.

{ "propiedades": [
    {
      "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.

рüффп's user avatar
рüффп

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.

Radouxca's user avatar
Radouxca

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

  • 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 at 20:59

¿Ha sido útil esta solución?