1. Конфиг в ReadLater Starter
Если вы писали только учебные консольные программы, очень легко поверить в наивную магию: «ну я же один раз написал String baseUrl = "...", значит так и будет всегда». Но backend-приложения живут в реальности, где одно и то же приложение приходится запускать в разных условиях: у вас дома, у тиммейта, на другом компьютере, с интернетом или без, на другом порту, в режиме mock, потому что внешний сервис «упал». И всё это должно решаться без правки исходников, иначе каждый запуск превращается в мини-ремонт.
Давайте начнём с честного признания: наш ReadLater Starter уже сейчас живёт не в одном режиме. У него есть client-фаза с командами catalog search и catalog details, и скоро появится server-фаза (server). Это значит, что у нас сразу возникают параметры, которые неизбежно отличаются от запуска к запуску: адрес внешнего API, таймауты, режим real/mock, порт сервера. И вот тут хардкод начинает вести себя как тот друг, который «точно придёт вовремя» — но только если вы всегда встречаетесь в одном и том же месте, в один и тот же день и никогда не меняете планы.
Хорошее рабочее правило на сегодня звучит так: код отвечает на вопрос “что делает приложение”, а конфигурация отвечает на вопрос “с какими значениями оно это делает”. Например, «делает HTTP-запрос к каталогу» — это логика. А «куда именно делает запрос» — это конфигурация.
Конфигурация - это отдельная ответственность
Понять конфигурацию проще всего через аналогию с рецептом. Рецепт говорит, что делать: смешать, запечь, подождать. Но он не должен быть зашит в «единственно возможные ингредиенты на планете Земля». Сегодня у вас молоко 2.5%, завтра — 3.2%, и это не повод переписывать рецепт и пересобирать кухню заново. В приложении то же самое: логика должна оставаться стабильной, а параметры — меняться без перекомпиляции.
В backend-мире конфигурация — это набор значений, которые могут отличаться между окружениями (у разных разработчиков, на разных машинах, в разных условиях сети) или между запусками (сегодня mock, завтра real). При этом конфигурация обычно не должна попадать в доменную модель и не должна расползаться по всему коду. Это отдельная ответственность: её удобно держать «рядом с точкой входа» и отдавать остальным частям приложения уже в понятном виде.
Здесь же возникает ещё одна важная мысль: конфигурация — это не «всё, что мне лень писать». Есть соблазн сделать конфигом каждую переменную, и тогда приложение превратится в казино настроек. Мы будем держаться здравого смысла: выносить наружу только то, что действительно должно меняться без правки Java-кода.
2. Конфигурация в ReadLater Starter
Сейчас мы аккуратно зафиксируем конкретно для нашего проекта: что является конфигурацией, а что является данными запуска (аргументами) или частью логики. Чтобы не превращать это в список “на память”, давайте оформим это в таблицу: так проще увидеть границу.
| Категория | Пример | Почему это именно оно |
|---|---|---|
| Конфигурация приложения | app.name=readlater | Имя не влияет на бизнес-логику, но может быть разным в разных окружениях и логах |
| Конфигурация каталожного клиента | catalog.api.base-url=https://openlibrary.org | Адрес внешнего сервиса может отличаться (real/mock, другой провайдер, другой стенд) |
| Конфигурация каталожного клиента | catalog.api.request-timeout-ms=2000 | Таймауты зависят от окружения и сети, а не от «логики поиска книги» |
| Конфигурация каталожного клиента | catalog.api.mode=real | Это переключатель поведения без изменения кода (особенно полезно для обучения) |
| Конфигурация server-mode | server.host=localhost, server.port=8080 | Порт может быть занят, хост может отличаться, а код сервера должен быть тем же |
| Данные конкретного запуска (args) | catalog search clean code | Это разовая команда: сегодня ищем “clean code”, завтра — “java concurrency” |
| Данные конкретного запуска (args) | catalog details OL12345M | Это разовый идентификатор книги для конкретного вызова |
| Часть прикладной логики | «как искать книги», «как нормализовать DTO» | Это поведение приложения, которое не должно меняться от окружения |
Обратите внимание на тонкий, но важный момент. Режим server — это не конфигурация, это команда. А server.port — это уже конфигурация для этой команды. Точно так же «искать книгу» — команда, а catalog.api.base-url — конфигурация, с которой команда работает.
Если сейчас вы не чувствуете разницу, это нормально. Её обычно начинают чувствовать, когда однажды меняют порт с 8080 на 8081 и внезапно делают коммит в Git «потому что иначе не запускается». И вот тут появляется вопрос: а почему вообще изменение порта потребовало изменения исходников?
3. Хардкод в main(): быстрый старт, медленная жизнь
Иногда хардкод выглядит невинно. Особенно в начале курса. Он даже кажется “честным”: вот значение, вот код, всё рядом. Проблема в том, что backend-проект — штука долгоживущая, и вот эта «невинность» быстро заканчивается. Вы захотите переключить real/mock, вы захотите увеличить таймаут, вы захотите поднять сервер на другом порту — и внезапно окажется, что каждое такое желание требует: открыть исходник, изменить строку, пересобрать, проверить, не сломал ли чего.
Посмотрим на «типичный стартовый хардкод», который в маленьких проектах возникает почти автоматически:
public class ReadLaterApplication {
public static void main(String[] args) {
// Конфигурационные значения окружения (сегодня одни, завтра другие)
// Проблема: они "зашиты" в исходник, значит требуют правки кода и пересборки.
String baseUrl = "https://openlibrary.org"; // адрес внешнего API (может быть real/mock или другой стенд)
int requestTimeoutMs = 2000; // сетевые таймауты зависят от сети и окружения
String apiMode = "real"; // переключатель поведения: "real" или "mock"
int serverPort = 8080; // порт может быть занят на конкретной машине
// Для демонстрации: выводим текущие значения, с которыми приложение стартовало
System.out.println("baseUrl = " + baseUrl); // baseUrl = https://openlibrary.org
System.out.println("requestTimeoutMs = " + requestTimeoutMs); // requestTimeoutMs = 2000
}
}
На уровне «я один запускаю у себя» это работает. Но теперь представьте реальность: вы в самолёте без интернета и хотите mock. Или у вас на машине порт 8080 занят (например, вы случайно оставили запущенным другой проект). Или внешний каталог стал медленным, и ваш 2000 ms внезапно превращается в “вечный таймаут”. И что вы делаете? Правильно: идёте править код. А потом забываете откатить. А потом коммитите. А потом тиммейт ругается, потому что у него теперь всё работает не так.
Самое неприятное здесь даже не то, что «надо переписать строчку». Самое неприятное — что меняется артефакт. У вас меняются исходники, а значит меняется “версия” приложения. И разница между «мне нужен другой порт» и «я изменил бизнес-логику» становится размыта. Это очень плохая привычка для backend-разработчика.
4. static final: лучше, но не конфиг
Следующая естественная реакция умного человека: «Ладно, я хотя бы вынесу литералы в константы». Это действительно делает код чище. Константы уменьшают дублирование и помогают не превращать проект в “кладбище магических чисел”. Но важно понять: это всё ещё не внешняя конфигурация. Вы просто переложили хардкод из одного места в другое место, но он не перестал быть частью исходников.
Например:
public class ReadLaterApplication {
// Константы улучшают читаемость и убирают дублирование "магических" значений.
// Но это всё равно значения в коде: чтобы поменять — нужно менять исходник и пересобирать.
private static final String BASE_URL = "https://openlibrary.org";
private static final int REQUEST_TIMEOUT_MS = 2000;
private static final String API_MODE = "real";
private static final int SERVER_PORT = 8080;
public static void main(String[] args) {
// Демонстрация: используем константу как "единственный источник" значения внутри кода
System.out.println("API mode = " + API_MODE); // API mode = real
}
}
С точки зрения читаемости стало лучше. Но если вы захотите поменять API_MODE на "mock", вы снова редактируете исходник. А значит снова пересобираете. И снова есть шанс забыть. И снова в Git появляется шум. Это похоже на ситуацию, когда вы написали на двери «вход справа», а потом магазин переехал — вы можете, конечно, купить новую дверь, но обычно проще повесить табличку, которую можно заменить без ремонта здания.
Поэтому давайте зафиксируем: константы — отличный инструмент против хаоса в коде. Но это не инструмент отделения кода от окружения. Они решают проблему “не хочу видеть магические значения по всему проекту”, но не решают проблему “хочу менять настройки без пересборки”.
5. Конфиг и args: не смешиваем
Здесь многие новички попадают в ловушку: раз у нас есть args, значит всё можно передавать аргументами. Технически — можно. Но если вы начнёте передавать абсолютно всё через args, то запуск приложения превратится в заклинание уровня «Гарри Поттер и Орден командной строки».
Нам нужна простая и предсказуемая модель:
- args выбирают сценарий: catalog search, catalog details, server;
- конфигурация задаёт параметры среды: baseUrl, таймауты, real/mock, host/port.
Чтобы это было видно не только в тексте, но и в голове, можно представить схему (на уровне идеи, без реализации):
flowchart TD
A[args] -->|выбирают| C[Launch mode]
B[конфигурация] -->|задаёт значения| D[Параметры окружения]
C --> E[Логика приложения]
D --> E
То есть catalog search clean code — это команда “что сделать сейчас”. А catalog.api.base-url — это “где находится каталог, к которому мы обращаемся”. Команда часто меняется от запуска к запуску, это нормально. Конфигурация тоже может меняться, но обычно реже и по другой причине: потому что окружение другое.
Давайте приземлим это на наши реальные команды запуска (которые мы уже использовали):
# Запускаем сценарий поиска (значения окружения не тащим в args)
./gradlew run --args="catalog search clean code"
# Запускаем сценарий деталей по конкретному идентификатору
./gradlew run --args="catalog details OL12345M"
# Запускаем сервер (порт/хост — это конфигурация, а не часть команды)
./gradlew run --args="server"
В этих командах уже видно правильное разделение. В args нет baseUrl, нет таймаутов, нет порта — и это хорошо. Иначе вы бы в каждом запуске таскали с собой «мешок настроек». Значения окружения должны жить отдельно, чтобы запуск оставался читаемым: вы сразу понимаете, что запускаете, а не расшифровываете “что означает восьмой аргумент”.
6. Мини-модель дальнейшей конфигурации
Сейчас мы не внедряем полноценную систему конфигурации (это следующие лекции дня), но важно заранее иметь “скелет мысли”, чтобы не потеряться. В рамках этого курса мы будем держаться простой, почти бытовой модели: есть базовый источник настроек (файл), есть внешний слой переопределений (переменные окружения), и есть команда запуска (args).
Выглядит это примерно так:
flowchart TD
P[application.properties] -->|база| X[Выбор значения]
E[Environment variables] -->|override| X
D[Defaults в коде] -->|fallback| X
X --> C[AppConfig]
A[args] --> L[LaunchCommand]
C --> R[Запуск логики]
L --> R
Обратите внимание: defaults в коде допустимы, но они должны быть осмысленными и локальными. «Если ничего не сказали, запускаемся на 8080» — нормальный дефолт. «Если ничего не сказали, идём на продовый платежный шлюз» — уже плохой дефолт (но мы сейчас и не про платежи, к счастью).
Ещё одно важное правило, которое мы проведём через весь проект: “источники значений” не должны быть размазаны по приложению. Если один класс читает System.getenv(), другой читает файл, третий парсит args — вы быстро получите конфигурационный хаос. Взрослый подход — собирать конфигурацию в одном месте (обычно в app/config слое рядом с точкой входа), а дальше передавать в остальные компоненты уже готовые значения.
7. Что не выносить в конфиг
Когда люди впервые слышат “конфигурация вне кода”, у них иногда появляется желание вынести наружу вообще всё. И тут важно остановиться и задать себе простой вопрос: «Это значение правда должно меняться без перекомпиляции? Или я просто боюсь принять решение в коде?»
Например, ключи JSON ("title", "author") в нашем DTO — это часть контракта. Если вы вынесете их в конфиг, вы получите систему, где поведение приложения зависит от случайного текста в файле настроек. Это не гибкость, это повышенная вероятность странных багов. То же самое касается “правил домена”. Если наш домен говорит «title не должен быть пустым», это не конфигурация. Это логика. Да, в больших системах некоторые правила могут быть настраиваемыми, но это совершенно другой уровень сложности, и в нашем курсе это было бы преждевременным усложнением.
В ReadLater Starter мы выносим наружу именно параметры окружения: адреса, порты, таймауты, режимы. Это те штуки, которые вам реально хочется менять без пересборки, и которые при этом не должны менять смысл бизнес-операций. Приложение по-прежнему остаётся тем же самым, просто “подключается” к миру другими проводами.
8. Типичные ошибки при выносе конфига
Ошибка №1: хардкодят значения в нескольких местах и потом героически ищут их по проекту.
Чаще всего так появляется зоопарк: где-то в ReadLaterApplication стоит 8080, где-то в ServerLauncher ещё один 8080, где-то в тестовом методе “временно” стоит 8081. Это неприятно тем, что вы не знаете, какое значение является “истинным”, и любое изменение превращается в квест “найди все вхождения”. Лекарство простое: договориться, что конфигурационные значения выбираются в одном месте, а дальше передаются как параметры.
Ошибка №2: считают, что static final — это уже внешняя конфигурация.
Константы действительно улучшают читаемость, и вы будете ими пользоваться. Но они всё ещё часть исходников, а значит изменение требует пересборки. Как только вы ловите себя на мысли «я поменяю порт и закоммичу, чтобы не забыть» — это почти стопроцентный сигнал, что вы путаете код и конфигурацию. Константы хороши для неизменяемых смыслов, конфиг — для окружения.
Ошибка №3: смешивают “команду запуска” и “настройки окружения”.
Если вы начинаете передавать baseUrl или порт через args, запуск быстро становится нечитаемым. И наоборот, если вы пытаетесь “в конфиге выбрать команду”, у вас получается приложение, которое трудно запускать явно: вы запускаете программу, а она “по настроению” решает быть сервером или клиентом. В нашем проекте разделение чёткое: args — это команда (server или catalog ...), конфиг — это параметры этой команды.
Ошибка №4: пытаются сделать конфигурацией каждую локальную переменную.
Иногда это выглядит так: “а давайте вынесем в конфиг строку ‘Searching…’ и сообщение об ошибке тоже, и длину отступа в консоли…”. Итог — огромный файл настроек, который никто не читает, и приложение, которое стало сложнее без реальной пользы. В этом курсе мы держимся прагматично: конфигурацией становится то, что реально меняется между окружениями и влияет на интеграции и запуск.
Ошибка №5: делают конфигурацию, но забывают назвать ключи по-человечески.
Ключи вроде x1, timeout2, modeFlag могут быть “понятны автору сегодня”, но завтра вы сами же будете проклинать этот файл. Нормальные ключи должны читаться как предложение: catalog.api.base-url, server.port. Это не эстетика — это часть инженерной дисциплины, потому что конфиг — такой же интерфейс, как публичный метод.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ