1. Конфигурация после сборки jar
Когда приложение живёт в IDE, мозг быстро привыкает к иллюзии, что «проект = приложение». Внутри IDE рядом лежит папка src/main/resources, профили включаются галочкой в Run Configuration, а нужные параметры могут случайно сохраниться в настройках запуска. Но jar-файл — это уже «чемодан с вещами»: вы его отдали — и всё, дальше у вас нет права подложить туда ещё один носок незаметно.
И тут внезапно выясняется, что конфигурация — это не декоративный YAML «для красоты», а часть контракта запуска. Для Boot-сервиса нормальная модель такая: один и тот же jar запускается в разных средах (local/dev/prod), а различия задаются профилями, переменными окружения, аргументами командной строки и внешними конфигами. Если вы это умеете — вы реально умеете Spring Boot. Если нет — вы умеете нажимать зелёный треугольник (и это тоже навык, просто… чуть менее монетизируемый).
В этой лекции будем считать, что jar уже собран и лежит, например, здесь:
build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Если у вас имя другое — это нормально: главное, чтобы вы запускали тот файл, который вы только что собрали.
2. Откуда Spring Boot берёт свойства при запуске jar
Перед тем как переопределять профили и свойства, полезно навести порядок в голове: Boot сначала собирает набор источников свойств (property sources), а потом применяет правило приоритета. Мы эту модель уже обсуждали в модуле про externalized configuration, но сейчас она становится особенно практической: вы буквально «крутите ручки», а приложение либо слушается, либо нет.
Ниже — упрощённая карта источников, без попытки превратить лекцию в справочник. Важно понять принцип: в этой схеме приоритет растёт сверху вниз. То, что стоит ниже, сильнее override'ит то, что было задано выше. SPRING_APPLICATION_JSON пока не включаем в диаграмму, потому что отдельно разберём его чуть ниже.
flowchart TB
A["Низкий приоритет: внутри jar — application.yaml и application-{profile}.yaml"] --> B["Внешние config files: ./config, дополнительные location/import"]
B --> C["Environment variables"]
C --> D["Java system properties -D..."]
D --> E["Высокий приоритет: command-line args --key=value"]
E --> F["Итоговая конфигурация в Environment"]
Самая частая неожиданность у новичка в packaged-run: он меняет application.yaml в проекте и ожидает, что это влияет на уже собранный jar. Но jar — это «фото на паспорт»: оно не меняется от того, что вы постриглись. Если вы правите ресурсы — jar надо пересобрать. Если хотите менять поведение без пересборки — используйте runtime overrides (env vars, -D, --..., внешние конфиги).
Для catalog-service (по ТЗ курса) базовые конфиги обычно такие:
application.yaml — общий.
application-local.yaml, application-dev.yaml, application-prod.yaml — профильные.
catalog-data.yaml — импортируемый файл со списком курсов (через spring.config.import).
И всё это может жить внутри jar, а поверх — у вас могут быть внешние файлы (например, ./config/catalog-extra.yaml) или вообще отдельная папка конфигурации, которая лежит рядом с jar на сервере.
3. Включаем профиль при java -jar: CLI args, env vars и -D
Профиль — это не «режим работы приложения для красоты», а способ включить нужную конфигурацию и (иногда) нужные бины. В нашем курсе профили — один из ключевых инструментов: local для удобной разработки, dev для более «реалистичной» диагностики, prod для минимально безопасного exposure и более строгих настроек.
Технически активировать профиль можно несколькими путями. Отличие не в магии Spring, а в том, как именно вы передаёте параметр процессу. И тут полезно мыслить так: «мне нужен активный профиль — я выбираю канал доставки».
Самый прямой и наглядный способ — аргумент командной строки:
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=dev
Если вы любите переменные окружения (а они обычно отлично вписываются в запуск сервисов), то это будет так:
SPRING_PROFILES_ACTIVE=prod \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Третий вариант — Java system properties через -D. Это чуть «олдскульнее», но по-прежнему рабочий и распространённый путь:
java -Dspring.profiles.active=local \
-jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Что выбрать? В учебном проекте — любой, лишь бы вы могли воспроизвести запуск без IDE. В реальной жизни часто побеждают env vars (они хорошо дружат с процессными менеджерами) или CLI args (они максимально явные). Главное — не держать профиль «только в IDE», потому что тогда jar-запуск превращается в лотерею.
4. Переопределяем прикладные свойства при запуске jar: --, env vars и -D
Профиль — это только начало. Настоящая сила externalized configuration — в том, что вы можете менять поведение приложения точечно: порт, флаги поведения, лимиты, диагностические настройки. И всё это — без пересборки jar, просто за счёт параметров запуска.
CLI args: переопределяем прямо в запуске
CLI args в Boot выглядят как --key=value. Для jar-запуска это особенно удобно, потому что у вас всегда есть одна строка, которую можно скопировать, сохранить в README или передать коллеге.
Например, запустим catalog-service в профиле dev и на другом порту:
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=dev \
--server.port=9090
Теперь добавим переопределение прикладного свойства. По ТЗ у нас есть namespace app.catalog.*, и, допустим, мы хотим увеличить лимит featured-курсов:
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=dev \
--app.catalog.max-featured-count=6
Если у вас в коде есть поведение, завязанное на maintenance-mode, можно включить его ровно так же:
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=local \
--app.catalog.maintenance-mode=true
Обратите внимание на маленькую, но важную вещь: мы передаём свойства в canonical форме, той же, что в YAML. Boot дальше сам разберётся с naming conventions и binding.
Env vars: переопределяем через окружение
Переменные окружения — классный способ, когда строка запуска не должна раздуваться или когда вы запускаете сервис через какой-нибудь менеджер, который удобно подставляет env vars.
Для server.port это выглядит так:
SPRING_PROFILES_ACTIVE=dev \
SERVER_PORT=9090 \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
А вот для app.catalog.max-featured-count важно сразу держать одну каноническую форму, чтобы не плодить конкурирующие варианты именования. В env vars Boot опирается на canonical property name: точки превращаются в _, дефисы из kebab-case убираются, всё переводится в верхний регистр. Поэтому app.catalog.max-featured-count превращается в APP_CATALOG_MAXFEATUREDCOUNT.
SPRING_PROFILES_ACTIVE=dev \
APP_CATALOG_MAXFEATUREDCOUNT=6 \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Небольшая табличка-шпаргалка (именно для наших свойств проекта):
| Canonical property (YAML/CLI) | Пример env var |
|---|---|
| spring.profiles.active | SPRING_PROFILES_ACTIVE |
| server.port | SERVER_PORT |
| app.catalog.title | APP_CATALOG_TITLE |
| app.catalog.max-featured-count | APP_CATALOG_MAXFEATUREDCOUNT |
| app.catalog.maintenance-mode | APP_CATALOG_MAINTENANCEMODE |
System properties -D: переопределяем в JVM
System properties полезны, когда вы управляете запуском JVM через одну «JVM-строку». Например:
# В этом варианте все overrides передаются как свойства JVM
java \
-Dspring.profiles.active=prod \
-Dserver.port=8085 \
-Dapp.catalog.max-featured-count=4 \
-jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Это не лучше и не хуже, чем env vars или CLI args — просто другой канал доставки. Но помнить про него полезно: вы будете видеть -D... в реальных проектах довольно часто.
5. SPRING_APPLICATION_JSON
Иногда хочется передать не одно свойство, а сразу набор, и делать для этого десять env vars — грустно. В такие моменты Spring Boot даёт механизм SPRING_APPLICATION_JSON: вы передаёте JSON-строку, внутри которой лежит «кусок конфигурации», и Boot воспринимает это как ещё один property source.
Это похоже на ситуацию «вот вам мини-application.yaml, только в JSON-виде». Пример (обратите внимание: JSON-кавычки — ваш новый источник приключений, особенно в разных оболочках):
# Один env var, внутри которого «кусок конфигурации»
# Важно: кавычки/экранирование зависят от вашей оболочки (bash/zsh/powershell и т.д.)
SPRING_APPLICATION_JSON='{
"spring": { "profiles": { "active": "dev" } },
"server": { "port": 9090 },
"app": { "catalog": { "max-featured-count": 6 } }
}' \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
Смысл здесь простой: мы одной переменной окружения включили профиль, поменяли порт и переопределили лимит featured-курсов.
Почему этот механизм полезен именно в контексте packaged-run? Потому что вы начинаете мыслить как инженер, который «перевозит» приложение между окружениями: jar один, настройки меняются снаружи. SPRING_APPLICATION_JSON — один из способов положить часть настроек «рядом с запуском», не создавая отдельный файл.
И да, если вы сейчас подумали «звучит круто, но я точно где-то забуду кавычку», — это абсолютно нормальное чувство. Даже опытные разработчики иногда ловят такие ошибки, потому что JSON в переменной окружения — это как собрать IKEA без инструкции, но с уверенностью в себе.
6. Внешние конфиги: additional-location и location
До этого момента мы меняли поведение через параметры запуска (env vars, -D, CLI args). Но часто есть ещё более «операторский» сценарий: конфиг должен жить в отдельном файле, рядом с jar или в отдельной директории, чтобы его можно было версионировать, менять, подкладывать в разные машины, не трогая сборку.
В нашем проекте по ТЗ даже предусмотрена папка ./config/ и опциональный внешний файл ./config/catalog-extra.yaml. Теперь вопрос: как сказать Boot «посмотри ещё и туда»?
spring.config.additional-location: аккуратно расширяем стандартный поиск
spring.config.additional-location добавляет ещё одну локацию к стандартному поиску. Это значит: Boot как обычно прочитает конфиги из jar и стандартных внешних мест, а потом дополнительно заглянет в указанную локацию.
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=dev \
--spring.config.additional-location=optional:file:./config/
Здесь важны три детали.
Во-первых, file: — это явный указатель, что мы читаем из файловой системы, а не из classpath.
Во-вторых, ./config/ — относительный путь. Он будет разрешаться относительно текущей директории процесса, а не относительно расположения jar. Это мы ещё отдельно разберём ниже, потому что это один из самых частых «почему оно не работает?!».
В-третьих, optional: означает «если папки/файлов нет — не падай». Это полезно, когда внешний конфиг не обязателен. В учебном проекте это часто удобно: можно запускать и без ./config, и с ней.
Как Boot будет искать файлы в этой директории? Обычно он ищет стандартные имена вроде application.yaml, application-dev.yaml и т.п. То есть если вы положите в ./config/ файл application-dev.yaml, он может переопределить то, что лежит внутри jar, не требуя пересборки.
spring.config.location: полностью заменяем стандартный поиск
spring.config.location — это другой инструмент. Он не «добавляет ещё одно место», а заменяет стандартный поиск конфигурации на то, что вы указали. Это сильное и иногда опасное действие: можно случайно «отключить» загрузку внутренних ресурсов, к которым вы привыкли.
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.config.location=file:./config/
Когда это уместно? Когда вы хотите жёстко сказать: «Запуск без внешнего конфига запрещён». Это бывает в реальных системах, где конфиг обязан приезжать отдельно (например, вместе с секретами), и запуск «на дефолтах» считается ошибкой.
Но в учебном проекте и в большинстве первых сервисов я бы начинал именно с additional-location, потому что он мягче и проще для дебага: вы добавили внешний файл — увидели эффект; убрали — вернулись к конфигу внутри jar.
7. Проверяем, что профиль и переопределения применились
Когда конфигурация задаётся «снаружи», всегда есть риск самообмана: вы уверены, что передали нужный параметр, а приложение продолжает жить по старому. И здесь важно не гадать, а проверять. К счастью, мы уже построили для catalog-service хороший diagnostic baseline: логи и Actuator.
Самый простой и «бедный, но честный» способ — логировать важные параметры при старте. В нашем проекте для этого и существует StartupSummaryRunner: он должен сообщать, с какими профилями и какими ключевыми настройками стартанул сервис. Это особенно важно в jar-режиме, потому что там IDE больше не подсвечивает вам активный профиль красивой галочкой.
Небольшой пример того, как может выглядеть фрагмент startup-лога (идея, а не единственный верный формат):
Active profiles: dev
Server port: 9090
Catalog title: Spring+ Catalog
Max featured count: 6
Если вы хотите сделать этот лог более надёжным и привязать его к нашей typed configuration, вот компактный пример (не «полная версия класса», а кусок, который показывает мысль). Обратите внимание на imports: я специально пишу их явно, чтобы вы видели, откуда берутся типы.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component // Регистрируем класс как bean, чтобы он создался при старте контекста
public class StartupConfigLog {
private static final Logger log = LoggerFactory.getLogger(StartupConfigLog.class);
public StartupConfigLog(CatalogProperties props, Environment env) {
// Environment — «финальный» слой, где видны все источники свойств с учётом приоритетов
log.info("Active profiles: {}", String.join(",", env.getActiveProfiles()));
// Typed configuration (CatalogProperties) показывает, что биндинг реально сработал
log.info("Max featured count: {}", props.maxFeaturedCount()); // например: 6
}
}
Да, этот пример слегка «читерский»: он логирует в конструкторе. В реальном коде вы бы делали это через ApplicationRunner, чтобы логировать уже после поднятия контекста в предсказуемой точке lifecycle (это мы делали раньше). Но как иллюстрация связи Environment + CatalogProperties + лог он хорошо работает и занимает ровно столько строк, сколько нужно, чтобы не утонуть.
Второй способ — Actuator. Если в local/dev профиле у вас открыт /actuator/env и /actuator/configprops, вы можете проверить фактическое значение прямо из работающего процесса. Например (условный запрос, формат зависит от того, как вы смотрите endpoint — через браузер, curl или HTTP-файл):
GET http://localhost:9090/actuator/env
GET http://localhost:9090/actuator/configprops
Смысл этих проверок простой: если вы передали --app.catalog.max-featured-count=6, то вы должны увидеть, что в Environment действительно финальное значение равно 6, и что CatalogProperties тоже связано с этим значением.
8. Относительные пути и working directory процесса
Есть одна коварная штука, которая ломает запуск внешних конфигов чаще, чем Spring, Gradle и космическая радиация вместе взятые. Это текущая директория процесса (working directory). Когда вы пишете file:./config/, вы на самом деле говорите: «возьми папку config относительно того места, откуда я запустил команду».
А место, откуда вы запускаете команду, может быть разным. Например, вы можете:
- находиться в корне проекта и запускать java -jar build/libs/...;
- находиться внутри build/libs и запускать java -jar catalog-service-...;
- запускать jar вообще из другой папки, куда вы его скопировали.
И во всех трёх случаях ./config/ — это разные пути.
Пример, который выглядит одинаково «по смыслу», но отличается по working directory:
# Вариант 1: запускаем из корня проекта
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
--spring.config.additional-location=optional:file:./config/
# Вариант 2: сначала зашли в build/libs, и теперь ./config смотрит уже туда
cd build/libs
java -jar catalog-service-0.0.1-SNAPSHOT.jar \
--spring.config.additional-location=optional:file:./config/
Во втором варианте Boot будет искать build/libs/config, а не <project-root>/config. Если вы об этом не думали — вы будете уверены, что «Spring Boot сломался». А он просто честно делает то, что вы написали.
Как лечится? Самый простой подход — запускать процесс из «стабильной» директории (там, где вы ожидаете конфиг), либо задавать путь явно (например, абсолютный), либо держать внешний конфиг рядом с jar и запускать из этой же папки.
9. Типичные ошибки при запуске jar
Ошибка №1: запускать jar без профиля и удивляться, почему “в dev у меня было иначе”.
Когда вы запускали приложение из IDE, профиль мог быть включён настройкой IDE или переменной окружения, которую IDE подставляет. В jar-запуске этого «сервиса по угадыванию ваших мыслей» уже нет. Поэтому вы запускаете без профиля и получаете дефолтное поведение. Лечится просто: профиль должен быть частью команды запуска или окружения, а не частью памяти IDE.
Ошибка №2: менять application.yaml в проекте и не пересобирать jar.
Это классика жанра. Вы правите ресурсы, запускаете старый jar и думаете, что изменения «не работают». Но jar — это отдельный файл, он не смотрит на вашу папку src. Если вы хотите, чтобы изменения внутри ресурсов попали в артефакт, нужно собрать новый jar. Если вы хотите менять поведение без сборки — используйте runtime overrides или внешний конфиг.
Ошибка №3: путать spring.config.additional-location и spring.config.location.
additional-location расширяет поиск и обычно «прощает» отсутствие внешних файлов, особенно с optional:. location заменяет стандартный поиск и может внезапно отключить часть привычного конфигурационного поведения. Если вы не уверены, что вам нужен жёсткий запрет запуска без внешнего конфига — начинайте с additional-location.
Ошибка №4: неверно назвать env var для прикладного свойства.
Когда вы переходите от --app.catalog.max-featured-count=6 к env vars, легко ошибиться в имени и получить ощущение «переменная есть, а свойство не применилось». Помогает дисциплина: держитесь одной канонической формы вроде APP_CATALOG_MAXFEATUREDCOUNT, а затем проверяйте фактическое значение через startup-логи или Actuator (env/configprops).
Ошибка №5: не учитывать текущую директорию процесса и терять внешний конфиг.
file:./config/ — это не «рядом с jar», а «рядом с working directory». Если вы запускаете jar из другой папки, относительный путь меняет смысл. Это не баг Spring Boot и не повод писать “ну тогда я захардкожу путь в коде” (пожалуйста, не надо). Это повод сделать запуск воспроизводимым и осознанным: либо фиксировать директорию запуска, либо указывать путь явно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ