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:
@Bean
de la classe MyProps
avec deux constructeurs neededProperty
new MyProps()
null
@ComponentScan
et @Component
pour fournir le bean MyProps
.BeanInstantiationException
-> NoSuchMethodException: MyProps.<init>()
Le seul moyen de le faire fonctionner est de fournir un getter/setter pour chaque champ non final.
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);
}
}
ApplicationProperties
class
@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
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
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();
}
}
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: