Опирается на правила:
R-VLD-CFG-1…R-VLD-CFG-4иR-VLD-CFG-X1…R-VLD-CFG-X2из Validation Style Guide → раздел 7. Конфигурация.
Важно знать
- Каждый
@ConfigurationProperties-класс обязан иметь@Validatedна классе. Невалидный конфиг →BeanCreationExceptionна старте, не «сервис поднялся с битым флагом».@NotBlankдля String-полей (URL, пути, ключи).@NotNullдля object-полей (Duration, кастомные объекты).@Min/@Maxдля числовых лимитов (maxConcurrent,poolSize).- Nested —
@Validна поле для рекурсивной валидации (AppSettings.database,AppSettings.messaging).- Spring валидирует
Duration/DataSizeпо типу сам. Дополнительные@DurationMin/@DurationMax— только когда нужен бизнес-предел (@DurationMax(value = 60, unit = SECONDS)для timeout).@Value("${prop}")для required-конфига — антипаттерн.@Valueне валидируется. Используем@ConfigurationProperties(typed + validated) даже для одного поля.- Optional-поля — без
@NotNull/@NotBlank. Null = «не задано». ТипString(неOptional<String>).
Конфиг — единственное место, где «упасть на старте» лучше, чем «работать с битыми значениями». Невалидный URL в DTO даёт 400 одному клиенту; невалидный URL в client.sber.base-url ломает весь сервис на первом платеже. Fail-fast через @Validated ловит проблему до того, как трафик начнёт приходить. Раскрытие раздела 7 гайда.
@Validated на каждом @ConfigurationProperties
R-VLD-CFG-1: правило простое — нет @Validated — нет валидации.
// ХОРОШО
@ConfigurationProperties("client.sber")
@Validated
public record SberClientSettings(
@NotBlank String baseUrl,
@NotNull Duration connectTimeout,
@NotNull Duration readTimeout,
@Min(1) @Max(100) int maxConcurrent,
String apiKey
) {}
# application.yml
client:
sber:
base-url: https://api.sber.example
connect-timeout: 2s
read-timeout: 10s
max-concurrent: 20
api-key: ${SBER_API_KEY:}
Что произойдёт с пустым SBER_API_KEY:
api-key— без@NotBlank, optional. Spring подставит пустую строку. Бин создастся, валидация пройдёт.- При первом запросе к Sber клиент может упасть с 401 (пустой ключ → отказ от сервера). Это уже runtime — но не «битый конфиг», а «вызов с пустым ключом», что более понятная ситуация.
С опечаткой base-rl: вместо base-url::
- Spring передаст
nullвbaseUrl. @NotBlankупадёт на старте:BeanCreationException: Failed to bind properties under 'client.sber.base-url': must not be blank.- Сервис не стартует. Healthcheck красный, deploy откатывается.
Это и есть fail-fast. Без @Validated сервис стартует, healthcheck зелёный, первый платёж падает с непонятной ошибкой через час.
Required-поля: @NotBlank vs @NotNull
R-VLD-CFG-2: правильный выбор аннотации по типу.
| Тип поля | Аннотация | Замечания |
|---|---|---|
String (URL, путь, токен) | @NotBlank | Защищает от null, "", " " |
Duration, DataSize | @NotNull | Spring парсит 2s, 100MB сам; null = не задано |
| Custom record/class | @NotNull + @Valid (если нужна рекурсивная) | См. ниже про nested |
int, long, boolean | @Min/@Max или без аннотации | Примитивы не могут быть null |
Integer, Long, Boolean | @NotNull + @Min/@Max | Обёртки могут быть null |
@ConfigurationProperties("app.messaging")
@Validated
public record MessagingSettings(
@NotBlank String bootstrapServers, // ← String, обязательно
@NotBlank String consumerGroupId,
@NotNull Duration pollTimeout, // ← Duration, обязательно
@NotNull Duration sessionTimeout,
@Min(1) @Max(1000) int batchSize, // ← примитив, ноль и отрицательные недопустимы
@NotNull @Min(1) Integer maxRetries, // ← Integer (обёртка) + NotNull + Min
boolean enableMetrics, // ← boolean без аннотаций, default false ок
String sslTruststorePath // ← optional, может быть null
) {}
Duration валидируется Spring-ом по типу
R-VLD-CFG-3: для Duration / DataSize дополнительные аннотации обычно не нужны.
client:
sber:
connect-timeout: 2s # парсится в Duration
read-timeout: 10s
upload-size-limit: 100MB # парсится в DataSize
@ConfigurationProperties("client.sber")
@Validated
public record SberClientSettings(
@NotNull Duration connectTimeout,
@NotNull Duration readTimeout,
@NotNull DataSize uploadSizeLimit
) {}
Что валидируется Spring-ом автоматически:
- Формат строки.
2s,100ms,1m30s,PT10S— все парсятся вDuration.2x— ошибка парсинга, бин не создаётся. - Тип.
connect-timeout: "two seconds"— не парсится, ошибка. - null vs unset.
connect-timeout:без значения —null;@NotNullловит.
Когда нужны дополнительные аннотации — бизнес-предел:
@NotNull @DurationMin(value = 100, unit = ChronoUnit.MILLIS)
@DurationMax(value = 60, unit = ChronoUnit.SECONDS)
Duration connectTimeout
Это говорит: «timeout не может быть < 100 ms (бессмысленно) и > 60 s (слишком долго для нашей системы)». Не валидация формата (Spring справится сам), а business invariant конфига.
@DataSizeMin/@DataSizeMax — для DataSize (@DataSizeMax("500MB")).
Nested — @Valid обязателен
R-VLD-CFG-4: вложенные структуры не валидируются автоматически.
// ХОРОШО — @Valid на nested
@ConfigurationProperties("app")
@Validated
public record AppSettings(
@Valid @NotNull DatabaseSettings database,
@Valid @NotNull MessagingSettings messaging,
@Valid @NotNull List<NotificationChannelSettings> notificationChannels
) {}
public record DatabaseSettings(
@NotBlank String url,
@NotBlank String username,
@NotBlank String password,
@Min(1) @Max(200) int maxPoolSize
) {}
app:
database:
url: jdbc:postgresql://localhost/order
username: order_user
password: ${DB_PASSWORD}
max-pool-size: 20
messaging:
bootstrap-servers: kafka:9092
consumer-group-id: order-service
# ...
notification-channels:
- type: SMS
provider: sms-aero
- type: EMAIL
provider: mailgun
Без @Valid на database:
@NotNullпроверит, чтоdatabaseне null.- Внутрь
DatabaseSettingsJakarta не пойдёт.@NotBlank urlне сработает. - Конфиг с пустым url стартует, падает на первом коннекте.
С @Valid — рекурсия. Каждое поле DatabaseSettings проверяется по своим аннотациям.
То же для List:
@Valid @NotNull List<NotificationChannelSettings> notificationChannels
@Valid заходит в каждый элемент списка. Без него — только проверка, что сам список не null.
@Value для required-конфига — антипаттерн
R-VLD-CFG-X2: @Value("${...}") не проходит через Jakarta Validation.
// ПЛОХО
@Service
class SberClient {
@Value("${client.sber.base-url}")
private String baseUrl; // ← не валидируется
@Value("${client.sber.connect-timeout:2s}")
private Duration connectTimeout;
}
Что не так:
- Нет валидации. Опечатка в
application.yml(base-rl:) →baseUrl == null, сервис стартует. - Default через
:замаскирует отсутствие значения.${client.sber.api-key:}→ пустая строка, не понятно «не задан» или «явно пустой». - Не typed-конфиг. Каждое поле в отдельном
@Value— разбросано по сервису. С@ConfigurationPropertiesвсё в одном классе.
Правильно — даже для одного поля:
@ConfigurationProperties("client.sber")
@Validated
public record SberClientSettings(@NotBlank String baseUrl) {}
@Service
@RequiredArgsConstructor
class SberClient {
private final SberClientSettings settings; // injected
// settings.baseUrl() — гарантированно не null/blank
}
@Value остаётся пригодным для тестов (где быстро подменить одно значение) и для тривиальных не-required конфигов (@Value("${app.name:default}") для одной строки). Для production-конфига сервиса — всегда @ConfigurationProperties.
Optional-поля без @NotNull
Поля, которые могут быть не заданы, остаются без @NotNull/@NotBlank:
@ConfigurationProperties("client.sber")
@Validated
public record SberClientSettings(
@NotBlank String baseUrl,
@NotNull Duration connectTimeout,
String apiKeyOverride, // ← optional, в проде из секрет-менеджера
Duration retryDelayOverride // ← optional, по дефолту берётся из общего конфига
) {}
null — это валидное значение «не задано». Код, который использует, проверяет:
Duration delay = settings.retryDelayOverride() != null
? settings.retryDelayOverride()
: defaultRetryDelay;
Альтернатива — default в YAML (retry-delay-override: 5s), но это лишает смысла optionality.
Тип Optional<String> для конфига не используем — это семантика возвращаемых значений, не полей. Конструктор record-а с Optional<String> — некрасиво. null достаточно.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
@ConfigurationProperties без @Validated | R-VLD-CFG-X1 | @Validated на классе |
@Value("${prop}") для required-конфига | R-VLD-CFG-X2 | @ConfigurationProperties typed + validated |
@NotNull String (вместо @NotBlank) | R-VLD-STD-1 | @NotBlank для String |
Nested без @Valid | R-VLD-CFG-4 | @Valid @NotNull NestedSettings field |
@DurationMax без бизнес-причины | R-VLD-CFG-3 | Spring парсит Duration сам, @NotNull достаточно |
Optional<String> в @ConfigurationProperties | — | String field (null = не задано) |
Куда дальше
- Validation → раздел 7. Конфигурация — нормативные формулировки
R-VLD-CFG-*. - Где валидировать — общая картина: контроллер, конфиг, домен.
- Стандартные constraints — что использовать на каких типах.
- Spring Bootstrap Style Guide —
@ConfigurationPropertiesв layout сервиса (BS-*). - Resilience Style Guide — таймауты в
*ClientSettings(R-RES-*).