web-dev-qa-db-fra.com

Immuable @ConfigurationProperties

Est-il possible d'avoir des champs immuables (finaux) avec l'annotation @ConfigurationProperties de Spring Boot? Exemple ci-dessous

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

Approches que j'ai essayées jusqu'à présent:

  1. Création d'un @Bean de la classe MyProps avec deux constructeurs
    • Fournir deux constructeurs: vide et avec l'argument neededProperty
    • Le bean est créé avec new MyProps()
    • Résultats dans le champ étant null
  2. Utiliser @ComponentScan et @Component pour fournir le bean MyProps .
    • Résultats en BeanInstantiationException -> NoSuchMethodException: MyProps.<init>()

Le seul moyen de le faire fonctionner est de fournir un getter/setter pour chaque champ non final.

33
RJo

Je dois résoudre ce problème très souvent et j'utilise une approche légèrement différente, ce qui me permet d'utiliser des variables final dans une classe.

Tout d’abord, je garde toute ma configuration dans un seul endroit (classe), disons, appelé ApplicationProperties. Cette classe a une annotation @ConfigurationProperties avec un préfixe spécifique. Il est également répertorié dans l'annotation @EnableConfigurationProperties par rapport à la classe de configuration (ou classe principale).

Ensuite, je fournis mon ApplicationProperties en tant qu'argument de constructeur et j'effectue une affectation à un champ final à l'intérieur d'un constructeur. 

Exemple:

Principale classe:

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

ApplicationPropertiesclass

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

    private String someProperty;

    // ... other properties and getters

   public String getSomeProperty() {
       return someProperty;
   }
}

Et une classe avec les propriétés finales

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

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

    // ... other methods / properties 
}

Je préfère cette approche pour différentes raisons, par exemple. si je dois configurer plus de propriétés dans un constructeur, ma liste d'arguments de constructeur n'est pas "énorme" car j'ai toujours un argument (ApplicationProperties dans mon cas); s'il est nécessaire d'ajouter plus de propriétés final, mon constructeur reste le même (un seul argument) - cela peut réduire le nombre de modifications ailleurs, etc.

J'espère que cela aidera

12
Tom

Au final, si vous voulez un objet immuable, vous pouvez aussi "pirater" le setter qui 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;
      }       
    }
}

Évidemment, si la propriété n'est pas simplement une chaîne, c'est un objet mutable, les choses sont plus compliquées, mais c'est une autre histoire.

Encore mieux, vous pouvez créer un conteneur de configuration

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

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

où maintenant la configuration est une clas sans 

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

et application.yml as

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

Mon idée est d'encapsuler des groupes de propriétés via des classes internes et d'exposer des interfaces avec des accesseurs uniquement.

Fichier de propriétés:

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
dshvets1

Vous pouvez définir les valeurs de champ par le biais d'annotations @Value. Ceux-ci peuvent être placés directement sur les champs et ne nécessitent aucun ajusteur:

@Component
public final class MyProps {

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

  public String getNeededProperty() { .. }
}

Les inconvénients de cette approche sont les suivants:

  • Vous devrez spécifier le nom de propriété qualifié complet dans chaque champ.
  • La validation ne fonctionne pas (cf. cette question )
0
oberlies