1. Когда JSON удобнее переменных окружения
Представьте, что вы запускаете catalog-service не «как обычно», а в маленьком демонстрационном режиме. Хочется быстро поменять заголовок каталога, включить технический режим (maintenance), уменьшить лимит featured-курсов и, например, подвинуть порт, чтобы не конфликтовать с другим сервисом на вашем ноутбуке. Технически это всё — обычные свойства. Практически — руками печатать их по одному становится утомительно.
Три launch-time канала уже понятны: env vars, -D и --key=value. Но как только нужно перекинуть не одно свойство, а пачку связанных значений, передавать их по одному становится неудобно. Отсюда и появляется JSON-блок: не новая модель конфигурации, а более компактный override поверх уже знакомой карты.
Чтобы почувствовать боль, достаточно посмотреть на такой запуск. Он рабочий, но если вы ошиблись в одном символе, приложение просто спокойно проигнорирует неправильный ключ и сделает вид, что так и было задумано (а вы потом два часа спорите с реальностью).
# Длинный запуск: много отдельных переменных окружения
APP_CATALOG_TITLE="Demo Catalog" \
APP_CATALOG_MAINTENANCE_MODE=true \
APP_CATALOG_MAX_FEATURED_COUNT=1 \
SERVER_PORT=9095 \
./gradlew bootRun
И вот здесь возникает простая инженерная мысль: «А можно я это передам одним блоком, чтобы оно было атомарно и в одном месте?» Spring Boot отвечает: «Можно. Держи SPRING_APPLICATION_JSON».
2. Что такое SPRING_APPLICATION_JSON
Важно сразу правильно понять идею: SPRING_APPLICATION_JSON — это не «JSON-конфиг вместо YAML». Это способ передать несколько свойств одной строкой, чаще всего через переменную окружения. Spring Boot умеет взять эту строку, распарсить JSON и добавить получившиеся значения в Environment как отдельный источник свойств.
На практике у механизма есть два «входа», которые полезно знать по именам, чтобы не путаться в документации и в чужих примерах:
- SPRING_APPLICATION_JSON — имя переменной окружения (environment variable), куда вы кладёте JSON.
- spring.application.json — то же самое по смыслу, но как обычное имя свойства (его можно передать и как -Dspring.application.json=..., и как --spring.application.json=...).
Фокус в том, что Boot не просто читает это значение «как строку». Он превращает его в отдельный property source (условно: «JSON-источник») и размещает его высоко в цепочке приоритетов.
Чтобы было видно, куда JSON-источник встраивается в уже знакомую лестницу, держите простую схему precedence, удобную именно для этой темы:
flowchart TD
A["command-line args --key=value"] --> B["SPRING_APPLICATION_JSON spring.application.json"]
B --> C["Java system properties -Dkey=value"]
C --> D["OS environment variables"]
D --> E["external config files"]
E --> F["packaged application.yaml"]
Эта диаграмма не про «все возможные источники на свете», а про практичную карту: где дефолт, где override, и почему одно значение побеждает другое.
3. Расплющивание JSON в ключи
С точки зрения человека JSON — это дерево (вложенные объекты). С точки зрения Environment — это плоские ключи вида app.catalog.title. Поэтому Boot делает понятную вещь: берёт вложенность и расплющивает её в dot-separated ключи.
Например, вот такой JSON:
{
"app": {
"catalog": {
"title": "JSON title",
"maintenance-mode": true
}
}
}
превратится в два обычных свойства:
- app.catalog.title = JSON title
- app.catalog.maintenance-mode = true
Можно смотреть на это как на YAML, только без красивых отступов и без комментариев. (Да, это минус. JSON не терпит комментарии. Он строгий как преподаватель на зачёте — «лишняя запятая? до свидания».)
Хороший способ закрепить понимание — маленькая табличка соответствий:
| JSON-фрагмент | Во что превращается в Environment |
|---|---|
|
server.port=9095 |
|
app.catalog.title=Demo |
|
app.catalog.maintenance-mode=true |
Отдельно обратите внимание на типы. В JSON true — это булево значение, а "true" — это строка. Для Environment в большинстве случаев это всё равно будет выглядеть как строка, но при попытке получить значение с приведением типов разница может стать заметной. И, что важнее, разница заметна вам как человеку: "true" выглядит как «я не уверен, что делаю, но пусть будет в кавычках на всякий случай». Кавычки в конфигурации редко добавляют уверенности.
4. Передача JSON: env, -D, --
На уровне повседневной работы у SPRING_APPLICATION_JSON есть три «транспорта». Новой precedence-системы тут не появляется: меняется только способ доставки одной и той же строки до Boot. И да, здесь начинается самое весёлое: экранирование кавычек. Считайте это мини-налогом за компактность.
Через env var SPRING_APPLICATION_JSON
В Unix-подобных оболочках обычно проще всего использовать одинарные кавычки снаружи, а внутри JSON оставить двойные:
# Один JSON-блок вместо набора отдельных переменных окружения
SPRING_APPLICATION_JSON='{"app":{"catalog":{"title":"Demo Catalog","maintenance-mode":true}}}' \
./gradlew bootRun
Если у вас Windows PowerShell, подход похожий, только синтаксис присваивания другой:
# PowerShell: задаём переменную окружения и запускаем приложение
$env:SPRING_APPLICATION_JSON = '{"app":{"catalog":{"title":"Demo Catalog","maintenance-mode":true}}}'
./gradlew bootRun
Главная идея: мы передаём одну переменную, но внутри неё сразу несколько связанных настроек.
Через -Dspring.application.json=...
Иногда удобнее передать это как JVM-параметр, особенно если вы запускаете приложение как обычную Java-команду и хотите, чтобы настройка жила именно на уровне JVM (или так проще настроить в IDE).
Пример выглядит так:
# Важно: -D... — это параметр JVM, он должен стоять до -jar
java -Dspring.application.json='{"server":{"port":9095}}' -jar app.jar
Здесь важный момент из серии «ошибка на миллион»: -D... — это параметр JVM, и он должен стоять до -jar. Если поставить после, JVM его не увидит. Она не обидится, она просто проигнорирует — и вы снова будете спорить с реальностью.
Через --spring.application.json=...
Boot умеет принимать это и как аргумент приложения. На вид это вообще выглядит как обычный --key=value, только значение — JSON:
# Gradle: аргументы приложения передаются через --args (и тут обычно начинаются кавычки)
./gradlew bootRun --args='--spring.application.json={"app":{"catalog":{"title":"CLI JSON title"}}}'
Это работает, но чаще всего становится самым «кавычкозависимым» вариантом. Если вы не любите экранировать кавычки и спорить с shell’ом, env var обычно спокойнее.
5. Приоритеты и null
SPRING_APPLICATION_JSON легко переоценить: задали APP_CATALOG_TITLE, не сработало, а потом оказывается, что сверху был JSON-блок. Поэтому здесь полезно зафиксировать не всю карту заново, а только место этого источника: command-line args обычно сильнее, затем идёт JSON-блок, затем system properties и env vars, а потом уже файлы.
Давайте разберём один ключ app.catalog.title в пяти местах, чтобы увидеть поведение глазами:
1) В application.yaml лежит дефолт:
app:
catalog:
# Дефолтное значение из файла конфигурации
title: "Title from file"
2) Вы добавили env var:
APP_CATALOG_TITLE="Title from env"
3) Вы добавили JSON-блок:
SPRING_APPLICATION_JSON='{"app":{"catalog":{"title":"Title from JSON"}}}'
4) И сверху ещё указали command-line:
--app.catalog.title="Title from CLI"
Итоговое значение будет "Title from CLI", потому что аргументы командной строки победят всех.
Теперь важный нюанс про null. Новички часто ожидают, что так можно «сбросить» свойство:
# Важно: null тут не работает как «сброс/стирание», а скорее как «ключа нет»
SPRING_APPLICATION_JSON='{"app":{"catalog":{"title":null}}}'
Ожидание: «ну я же задал null, значит оно пустое». Реальность: для этого механизма null не работает как «стирание». В итоге у вас останется значение из более слабого источника (например, из application.yaml), потому что ключ с null считается по сути отсутствующим. То есть null — это не кнопка Reset.
Если нужно вернуть дефолт, самый надёжный путь — просто не задавать override. Конфигурация в Boot вообще любит простую философию: «если не знаешь, что делать — убери лишнее, и станет понятнее».
6. Мини-диагностика через Environment
Когда начинается конфликт источников, мозг пытается «вспомнить», что вы запускали, что было в терминале, что IDE подсовывает в Run Configuration, и где вообще истинная правда. В этот момент полезнее всего не гадать, а спросить у приложения напрямую: «что ты видишь в Environment?».
Ниже — тот же временный probe-runner, только с ключами, которые удобно проверить именно для JSON-override. Не держите его постоянно в проекте: это просто фонарик, чтобы быстро увидеть, кто победил в precedence-цепочке.
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
// Учебный хелпер: печатаем итоговые значения из Environment, чтобы понять precedence источников
@Bean
ApplicationRunner configEchoRunner(Environment env) {
return args -> {
// getProperty(...) обычно возвращает строку или null, если ключ не задан ни в одном источнике
System.out.println("app.catalog.title = " + env.getProperty("app.catalog.title"));
// Здесь ключ с дефисом — это нормально: он был «расплющен» из JSON/YAML в app.catalog.maintenance-mode
System.out.println("maintenance = " + env.getProperty("app.catalog.maintenance-mode"));
// Удобный маркер для проверки, какой именно источник переопределил server.port
System.out.println("server.port = " + env.getProperty("server.port"));
};
}
Если вы запустите приложение с таким JSON:
SPRING_APPLICATION_JSON='{"app":{"catalog":{"title":"JSON title","maintenance-mode":true}},"server":{"port":9095}}' \
./gradlew bootRun
то вывод будет примерно таким (значения — для примера):
app.catalog.title = JSON title // app.catalog.title = JSON title
maintenance = true // maintenance = true
server.port = 9095 // server.port = 9095
Смысл здесь в том, что приложение перестаёт быть «чёрным ящиком». Вы больше не спорите с YAML-файлом на диске. Вы спрашиваете у Environment, и он показывает итоговую правду.
7. Типичные ошибки при использовании SPRING_APPLICATION_JSON
Эта тема кажется маленькой и «как будто очевидной», пока вы не словили первый запуск, где JSON не распарсился, кавычки съел shell, а приложение тихо стартовало с дефолтами — и вы уверены, что Boot вас игнорирует из вредности. На самом деле почти все проблемы тут типовые и решаются внимательностью к синтаксису и приоритетам. Давайте разберём самые частые грабли.
Ошибка №1: JSON невалидный (лишняя запятая, неэкранированная кавычка, комментарий).
YAML умеет быть «человечнее» и иногда прощает мелкие огрехи, а JSON — нет. Особенно часто ломают конфиг trailing comma ({"a":1,}) и попытка вставить комментарий. Лечится скучно: проверкой JSON на валидность и привычкой держать его максимально маленьким.
Ошибка №2: shell “съел” кавычки, и до Boot дошла каша.
На Bash и PowerShell правила кавычек разные, на Windows CMD — ещё веселее. Если вы видите, что значение будто не применяется, первое, что стоит сделать — вывести переменную окружения в том же терминале и убедиться, что в ней действительно лежит корректная строка JSON, а не обрезанный фрагмент.
Ошибка №3: попытка хранить большой конфиг одной JSON-строкой.
SPRING_APPLICATION_JSON хорош, когда вы переопределяете 2–5 связанных значений. Когда вы пытаетесь засунуть туда половину application.yaml, у вас пропадает читаемость, растёт риск ошибок, и любые изменения превращаются в «редактирование минированной строки». Для постоянной конфигурации лучше остаются файлы.
Ошибка №4: ожидание, что null “сбросит” значение из файла.
Интуитивно кажется, что null — это «пусто», значит override должен победить. Но для этого механизма null не работает как стирание ключа. В итоге вы думаете, что “обнулили”, а приложение берёт значение снизу по precedence. Если нужно убрать override — убирайте сам override.
Ошибка №5: перепутаны приоритеты источников (особенно CLI args поверх JSON).
Очень частый сценарий: вы задали JSON-блок и уверены, что он главный, а потом в IDE или в команде запуска где-то спрятался --app.catalog.title=.... И он победил. В такие моменты помогает либо печать итоговых значений через Environment, либо просто дисциплина: в конфликтных ситуациях всегда сначала смотрим на фактическую команду запуска.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ