Неизменный @ConfigurationProperties

Возможно ли иметь неизменяемые (финальные) поля с Spring Boot's @ConfigurationProperties аннотаций? Пример ниже

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

Подходы, которые я пробовал до сих пор:

  1. Создание @Bean из MyProps класс с двумя конструкторами
    • Предоставление двух конструкторов: пустой и с neededProperty аргумент
    • Боб создан с new MyProps()
    • Результаты в этой области null
  2. С помощью @ComponentScan а также @Component обеспечить MyProps боб.
    • Результаты в BeanInstantiationException -> NoSuchMethodException: MyProps.<init>()

Единственный способ заставить его работать, это предоставить getter/setter для каждого нефинального поля.

9 ответов

Решение

Я должен решить эту проблему очень часто, и я использую немного другой подход, который позволяет мне использовать final переменные в классе.

Прежде всего, я храню всю свою конфигурацию в одном месте (классе), скажем, под названием ApplicationProperties, Этот класс имеет @ConfigurationProperties аннотация с конкретным префиксом. Это также указано в @EnableConfigurationProperties аннотация против класса конфигурации (или основного класса).

Тогда я предоставляю ApplicationProperties в качестве аргумента конструктора и выполнить присваивание final поле внутри конструктора.

Пример:

Основной класс:

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

ApplicationProperties учебный класс

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

    private String someProperty;

    // ... other properties and getters

   public String getSomeProperty() {
       return someProperty;
   }
}

И класс с конечными свойствами

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

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

    // ... other methods / properties 
}

Я предпочитаю этот подход по многим различным причинам, например, если мне нужно настроить больше свойств в конструкторе, мой список аргументов конструктора не является "огромным", поскольку у меня всегда есть один аргумент (ApplicationProperties в моем случае); если есть необходимость добавить больше final свойства, мой конструктор остается неизменным (только один аргумент) - это может уменьшить количество изменений в других местах и ​​т. д.

Я надеюсь, что это поможет

Начиная с Spring Boot 2.2, наконец, можно определить неизменяемый класс, украшенный @ConfigurationProperties.
В документации показан пример.
Вам просто нужно объявить конструктор с полями для привязки (вместо способа установки) и добавить @ConstructorBinding аннотация на уровне класса, чтобы указать, что следует использовать привязку конструктора.
Итак, ваш фактический код без какого-либо сеттера теперь в порядке:

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

Просто обновление о последней поддержке более поздних версий Spring-Boot:

Если вы используете версию jdk >= 14, вы можете использоватьrecordтип, который делает более или менее то же самое, что и версия Lombok, но без Lombok.

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

Вы также можете использовать запись внутриMyPropsзапись для управления вложенными свойствами. Вы можете увидеть пример здесь .

Еще один интересный пост здесь , который показывает, что@ConstructorBindingаннотация больше не нужна, если объявлен только один конструктор.

Используя подход, аналогичный подходу из /questions/38592769/neizmennyij-configurationproperties/55193848#55193848

Но вместо AllArgsConstructor вы можете использовать RequiredArgsConstructor.

Рассмотрим следующие applications.properties

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

Примечание: используйте согласованность со своими свойствами, я просто хотел показать, что оба верны (fistName и last-name).


Класс Java подбирает свойства

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

    // ...
}


Кроме того, вы должны добавить в свой инструмент сборки зависимость.

build.gradle

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

или же

pom.xml

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

Если вы сделаете еще один шаг, чтобы предоставить красивое и точное описание для ваших конфигураций, подумайте о создании файла additional-spring-configuration-metadata.json в каталоге 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."
    }
}

(очистите и скомпилируйте, чтобы вступили в силу)


По крайней мере, в IntelliJ, когда вы наводите курсор на свойства внутри application.propoerties, вы получите четкое описание ваших настраиваемых свойств. Очень полезно для других разработчиков.

Это дает мне красивую и лаконичную структуру моих свойств, которые я использую в своем сервисе с Spring.

В конце концов, если вы хотите неизменный объект, вы также можете "взломать" установщик, который

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

Очевидно, что если свойство не просто строка, то есть изменяемый объект, все будет сложнее, но это уже другая история.

Еще лучше вы можете создать контейнер конфигурации

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

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

где сейчас конфигурация класс без

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

и application.yml как

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

Моя идея заключается в том, чтобы инкапсулировать группы свойств через внутренние классы и предоставлять интерфейсы только с помощью методов получения.

Файл свойств:

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

myapp.scheduler.pool-size=2

Код:

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

Если вы хотите протестировать свои свойства в приложении без загрузки всего контекста весенней загрузки, используйте@EnableConfigurationPropertiesв вашем тесте.

Пример:

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");

}

Используя аннотации Lombok, код будет выглядеть так:

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

  private final String neededProperty;

}

Кроме того, если вы хотите напрямую подключить этот класс свойств, не используя @Configuration класс и @EnableConfigurationProperties, вам нужно добавить @ConfigurationPropertiesScan в основной класс приложения, помеченный @SpringBootApplication.

См. Соответствующую документацию здесь: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html

Вы можете установить значения поля через @Value аннотаций. Они могут быть размещены непосредственно на полях и не требуют установки:

@Component
public final class MyProps {

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

  public String getNeededProperty() { .. }
}

Недостатком этого подхода является:

  • Вам нужно будет указать полное имя свойства в каждом поле.
  • Проверка не работает (см. Этот вопрос)
Другие вопросы по тегам