Неизменный @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() { .. }
}
Подходы, которые я пробовал до сих пор:
- Создание
@Bean
изMyProps
класс с двумя конструкторами- Предоставление двух конструкторов: пустой и с
neededProperty
аргумент - Боб создан с
new MyProps()
- Результаты в этой области
null
- Предоставление двух конструкторов: пустой и с
- С помощью
@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() { .. }
}
Недостатком этого подхода является:
- Вам нужно будет указать полное имя свойства в каждом поле.
- Проверка не работает (см. Этот вопрос)