JavaRush /Курси /Java Server /Конфігурація поза кодом

Конфігурація поза кодом

Java Server
Рівень 20 , Лекція 0
Відкрита

1. Конфіг у ReadLater Starter

Якщо ви писали лише навчальні консольні програми, дуже легко повірити в наївну магію: «Ну, я ж один раз написав String baseUrl = "...", отже так і буде завжди». Але бекенд-застосунки живуть у реальності, де один і той самий застосунок доводиться запускати в різних умовах: у вас удома, у напарника, на іншому комп’ютері, з інтернетом або без, на іншому порту, у режимі mock, якщо зовнішній сервіс «упав». І все це має вирішуватися без правки вихідного коду, інакше кожен запуск перетворюється на мініремонт.

Почнімо з чесного визнання: наш ReadLater Starter уже зараз живе не в одному режимі. У нього є клієнтська фаза з командами catalog search і catalog details, і незабаром з’явиться серверна фаза (server). Це означає, що в нас одразу виникають параметри, які неминуче відрізняються від запуску до запуску: адреса зовнішнього API, таймаути, режим real/mock, порт сервера. І саме тут хардкод починає поводитися як той знайомий, який «точно прийде вчасно» — але тільки якщо ви завжди зустрічаєтеся в одному й тому самому місці, в один і той самий день і ніколи не змінюєте плани.

Гарне робоче правило на сьогодні звучить так: код відповідає на запитання «що робить застосунок», а конфігурація — на запитання «з якими значеннями він це робить». Наприклад, «робить HTTP-запит до каталогу» — це логіка. А «куди саме робить запит» — це конфігурація.

Конфігурація — це окрема відповідальність

Найпростіше зрозуміти конфігурацію через аналогію з рецептом. Рецепт говорить, що робити: змішати, запекти, почекати. Але він не має бути зашитий в «єдино можливі інгредієнти на планеті Земля». Сьогодні у вас молоко 2,5 %, завтра — 3,2 %, і це не привід переписувати рецепт і перескладати кухню заново. У застосунку все так само: логіка має залишатися стабільною, а параметри — змінюватися без перекомпіляції.

У бекенд-світі конфігурація — це набір значень, які можуть відрізнятися між середовищами (у різних розробників, на різних машинах, у різних умовах мережі) або між запусками (сьогодні 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(): швидкий старт, повільне життя

Іноді хардкод виглядає невинно. Особливо на початку курсу. Він навіть здається «чесним»: ось значення, ось код, усе поруч. Проблема в тому, що бекенд-проєкт — річ довгоживуча, і ця «невинність» швидко закінчується. Ви захочете перемкнути 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 раптом перетворюється на «вічний таймаут». І що ви робите? Правильно: ідете правити код. Потім забуваєте відкотити. Потім комітите. А потім напарник свариться, бо в нього тепер усе працює не так.

Найнеприємніше тут навіть не те, що «треба переписати рядок». Найнеприємніше — що змінюється артефакт. У вас змінюється вихідний код, а отже змінюється «версія» застосунку. І різниця між «мені потрібен інший порт» і «я змінив бізнес-логіку» стає розмитою. Це дуже погана звичка для бекенд-розробника.

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 = " + API_MODE); // Режим API = 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[Режим запуску]
    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] -->|перевизначення| X
    D[Типові значення в коді] -->|резервний варіант| X
    X --> C[AppConfig]
    A[args] --> L[LaunchCommand]
    C --> R[Запуск логіки]
    L --> R

Зверніть увагу: типові значення в коді допустимі, але вони мають бути осмисленими й локальними. «Якщо нічого не сказали, запускаємося на 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. Це не естетика — це частина інженерної дисципліни, тому що конфіг — такий самий інтерфейс, як публічний метод.

1
Задача
Java Server, 20 рівень, 0 лекція
Недоступна
Централізоване зведення налаштувань
Централізоване зведення налаштувань
1
Задача
Java Server, 20 рівень, 0 лекція
Недоступна
Розділення налаштувань і даних запуску
Розділення налаштувань і даних запуску
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ