JavaRush /Курси /Java Server /Запуск режиму server

Запуск режиму server

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

1. Старт сервера та конфігурація

Коли ви вперше запускаєте локальний HTTP-сервер, дуже хочеться зробити все нашвидкуруч: написати 8080 прямо в коді, запустити — і радіти життю. І так, перші пʼять хвилин життя проєкту справді хочеться прожити саме так. Проблема в тому, що бекенд-код зазвичай живе довше за пʼять хвилин, а порт 8080 у світі розробників — це як розетка біля холодильника: здається, що вона вільна рівно доти, доки хтось не підʼєднає туди чайник, мікрохвильовку й зарядку від ноутбука одночасно.

Для server-режиму нам потрібно лише кілька стартових параметрів. Але ці параметри — не «частина логіки застосунку», а «частина середовища, у якому застосунок запущено». Сьогодні це ваш ноутбук, завтра — ноутбук ментора, післязавтра — CI, а потім ви просто відкриєте другий проєкт, який теж захоче 8080 (і, звісно ж, переможе в бійці).

Уже є набір об’єктів JDK: ми розуміємо, що HttpServer живе довго, а HttpExchange приходить на кожен запит. Тепер із цих цеглинок треба зібрати живий процес, який реально привʼязується до host і port.

Тому старт сервера має виглядати як чітка послідовність: ми отримуємо ServerConfig із загальної конфігураційної моделі проєкту, збираємо InetSocketAddress, створюємо HttpServer, викликаємо start() і одразу ж пишемо зрозумілий лог: яка адреса, який порт, яке імʼя застосунку. Це не «занудство заради занудства» — це базова звичка, яка відрізняє «код на вечір для себе» від «код, який ви зможете пояснити й підтримувати».

Щоб було простіше орієнтуватися, ось мінімальний фрагмент application.properties, який нам потрібен саме для server-mode:

Файл: src/main/resources/application.properties

app.name=readlater-starter
server.host=localhost
server.port=8080

Ми не повторюємо тут механіку читання properties (це було в дні про конфігурацію), але тримаємо в голові важливу думку: сервер стартує на тому, що прийшло з конфігурації, а не на тому, що випадково набралося в класі LocalApiServer.

2. ServerConfig: record замість параметрів

Коли ви запускаєте сервер, у код легко просочується «пара зайвих аргументів». Спочатку потрібні host і port. Потім раптово хочеться додати appName (і це логічно — ми хочемо повертати його в /health). Потім — ще щось. Якщо ці значення ганяти окремими параметрами, ваш код швидко перетворюється на «паровозик з аргументів», де легко переплутати порядок, типи й сенс.

Для таких випадків дуже зручно виділяти маленький конфіг-об’єкт. У Java 25 ідеальний формат для цього — record: він короткий, зрозумілий і за змістом чудово підходить для контейнера налаштувань.

Ось мінімальна форма ServerConfig, якої вистачить на сьогоднішній запуск:

package com.example.readlater.config;

/**
 * Конфігурація запуску HTTP-сервера.
 * Тут лише те, що стосується середовища (де й як стартуємо), а не бізнес-логіки.
 */
public record ServerConfig(
        String host,   // Хост/інтерфейс для привʼязування (наприклад, localhost)
        int port,      // Порт, який слухатиме сервер
        String appName // Імʼя застосунку для логів/health тощо
) {
}

Зверніть увагу на дрібницю: ми кладемо ServerConfig у пакет config. Це чесно відображає роль об’єкта: він не «серверна логіка», а налаштування для серверного режиму.

Тепер питання: звідки взяти ServerConfig? В ідеалі — з вашої загальної конфігураційної моделі (умовно AppConfig). Назва класів у вас може бути іншою, але ідея однакова: конфіг застосунку зберігає appName, server.host, server.port. Далі ми або зберігаємо ServerConfig як частину AppConfig, або збираємо його на місці.

Приклад збирання — коротко й без заглиблення в читання properties:

import com.example.readlater.config.ServerConfig;

// У прикладі значення "захардкожені" лише для демонстрації.
// У реальному застосунку їх потрібно брати з уже завантаженої конфігурації (properties/env/etc).
ServerConfig serverConfig = new ServerConfig(
        "localhost",
        8080,
        "readlater-starter"
);

У реальному проєкті замість літералів будуть значення з уже завантаженого AppConfig. І головний плюс у тому, що далі весь код запуску сервера приймає один об’єкт. Не три параметри, не пʼять, не «а ще один прапорець», а один компактний ServerConfig.

3. LocalApiServer: окремий клас запуску

Є давня легенда: якщо довго писати все в main(), то одного дня main() почне писати код замість вас. Легенда, звісно, неправдива. Але те, що main() роздується до сотень рядків і стане найстрашнішим місцем проєкту, — це цілком реальна доля.

ReadLaterApplication (або як у вас називається точка входу) має робити одну річ: обирати режим і збирати залежності. А ось запускати сервер, будувати адресу, ловити IOException, писати лог запуску — це окрема відповідальність. Саме тому ми виносимо запуск в окремий клас, наприклад LocalApiServer.

Він буде дуже маленьким. І це добре. Це «пульт керування сервером», а не «серверний фреймворк імені нас». Він уміє запускати сервер на адресі з ServerConfig і логувати результат.

На цьому кроці LocalApiServer відповідає лише за старт і привʼязку до адреси. Цього достатньо, щоб довести: процес став сервером. Коли зʼявиться перший зовнішній шлях, той самий клас просто отримає реєстрацію /health і JSON-відповідь.

Мінімальна заготовка:

package com.example.readlater.app.server;

import com.example.readlater.config.ServerConfig;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public class LocalApiServer {

    public HttpServer start(ServerConfig config) throws IOException {
        // Перетворюємо host/port із конфігурації на "мережеву адресу", на якій слухатиме сервер
        InetSocketAddress address =
                new InetSocketAddress(config.host(), config.port());

        // backlog = 0 => використовуємо значення за замовчуванням (у деталі сьогодні не заглиблюємося)
        HttpServer server = HttpServer.create(address, 0);

        // Запускаємо приймання вхідних зʼєднань
        server.start();

        // Повертаємо сервер, щоб за потреби можна було коректно зупинити (server.stop(...))
        return server;
    }
}

Поки тут немає логування — ми додамо його в наступному розділі. І так, метод повертає HttpServer. Навіть коли зараз здається «навіщо повертати, я ж усе одно нічого з ним не роблю», це корисна звичка: ми зберігаємо можливість у майбутньому коректно зупинити сервер через server.stop(...), не перетворюючи застосунок на набір глобальних змінних.

Тепер підключимо це в точку входу. Важливо: ReadLaterApplication не має будувати сервер по цеглинках. Він має створити LocalApiServer і викликати start(...). Все.

Наприклад, фрагмент гілки SERVER (припускаємо, що launchMode уже розпізнано):

import com.example.readlater.app.server.LocalApiServer;
import com.example.readlater.config.ServerConfig;

// Поки сервер відповідає лише за старт і привʼязку до адреси, тому залежностей у нього мінімум
LocalApiServer localApiServer = new LocalApiServer();

// Припускаємо, що appConfig уже зібрано з application.properties (або іншого джерела)
ServerConfig serverConfig = appConfig.server(); // припустімо, так

localApiServer.start(serverConfig);

Зверніть увагу, як читається код. Він майже «розмовний»: є сервер, є конфіг, запускаємо. І головне — у ReadLaterApplication не з’являються деталі на кшталт InetSocketAddress і HttpServer.create(...). Це деталі серверної частини, їхнє місце — у LocalApiServer.

4. Створюємо HttpServer: адреса і start()

Зараз ми зробимо найважливіший крок лекції: від «у нас є режим server на словах» до «у нас реально стартує HTTP-сервер». І в цьому місці дуже корисно прямо проговорити, що відбувається на рівні коду, без спроби запамʼятати все як заклинання.

Ми беремо host і port, збираємо з них InetSocketAddress і передаємо в HttpServer.create(...). Другий аргумент create(...) — це backlog. Якщо говорити по-людськи, backlog — це «скільки вхідних зʼєднань можна потримати в черзі, поки сервер зайнятий». У production це може бути цікавою темою, але сьогодні — ні. Ми ставимо 0, щоб використовувати значення за замовчуванням, і свідомо не ліземо в низькорівневе налаштування.

Давайте додамо в LocalApiServer логування, обробку винятків на рівні виклику й нормальний лог запуску. Почнімо з логера:

package com.example.readlater.app.server;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LocalApiServer {
    // Логер на клас: у логах буде зрозуміло, звідки прилетіло повідомлення
    private static final Logger log =
            LoggerFactory.getLogger(LocalApiServer.class);
}

Тепер розширимо метод start(...) так, щоб він логував запуск. Важливо: ми хочемо побачити в логах готове посилання, за яким будемо ходити в API. Тому лог краще писати у вигляді http://host:port.

import com.example.readlater.config.ServerConfig;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public HttpServer start(ServerConfig config) throws IOException {
    // Готуємо адресу, до якої привʼязується сервер
    InetSocketAddress address =
            new InetSocketAddress(config.host(), config.port());

    // Створюємо сервер і запускаємо його
    HttpServer server = HttpServer.create(address, 0);
    server.start();

    // Пишемо зрозумілий лог запуску: одразу видно URL і імʼя застосунку
    // {} — плейсхолдери SLF4J (без конкатенації рядків)
    log.info("Локальний API запущено: http://{}:{}, імʼя застосунку={}",
            config.host(), config.port(), config.appName());

    // Повертаємо server, щоб код, який викликає, міг за бажанням зупинити його пізніше
    return server;
}

Зверніть увагу, чого тут ще немає: сервер уже слухає адресу й живе після start(), але зовні йому поки нема на що відповісти по-справжньому. Поки не зʼявиться хоча б один createContext(...), зовнішній клієнт не побачить корисної кінцевої точки з передбачуваним контрактом.

Зверніть увагу на стиль SLF4J: ми не конкатенуємо рядки через +, а використовуємо плейсхолдери {}. Це водночас і коротше, і ефективніше, і просто «так прийнято» в живих проєктах. І так, це та дрібниця, яка в майбутньому відділятиме ваш код від коду, де логування перетворене на “println, але з анотаціями”.

Ще один важливий момент. HttpServer.create(...) і start() можуть кинути IOException. На практиці найчастіша причина — порт зайнятий. Це нормально. Не «зламалась Java», не «поганий JDK», а просто хтось уже слухає цей порт. Тому на рівні ReadLaterApplication ми маємо чесно зловити виняток, залогувати й завершитися.

Приклад (короткий, без зайвої архітектури):

try {
    // Коли порт вільний — сервер стартує, і процес продовжить жити в режимі очікування запитів
    localApiServer.start(serverConfig);
} catch (IOException e) {
    // Важливо передати e: так у логах буде stack trace (часто там одразу видно BindException і причину)
    log.error("Сервер не вдалося запустити на {}:{}",
            serverConfig.host(), serverConfig.port(), e);
}

Тут ми робимо одразу дві корисні речі. По-перше, у логах буде видно, на якій адресі ми намагалися стартувати. По-друге, ми зберігаємо stack trace через e — він стане у пригоді, якщо це не «порт зайнятий», а щось рідкісніше.

5. Лог запуску сервера

У логуванні є просте правило: лог має відповідати на питання «що мені робити далі?». Із запуском сервера це правило працює ідеально. Після запуску застосунку ви зазвичай хочете зрозуміти три речі: чи стартував процес узагалі, на якій адресі слухає сервер і який це застосунок (особливо якщо у вас одночасно відкрито два термінали і три проєкти — а так і буде).

Тому хороший лог запуску для server-mode зазвичай містить host, port, appName, а іноді ще й підказку на кшталт “Press Ctrl+C to stop”. Це не «професійна звичка», це просто маленька турбота про себе з майбутнього. Себе з майбутнього, до речі, треба любити: саме він розгрібатиме ваші ж логи.

Ось приклад формату, який достатньо інформативний і при цьому не перетворюється на роман:

// Слеш у кінці — суто візуальна дрібниця: посилання виглядає "як URL", зручно копіювати
log.info("Сервер запущено на http://{}:{}/ (імʼя застосунку={})",
        config.host(), config.port(), config.appName());

Чому я додав / у кінці? Це суто візуальна дрібниця: посилання виглядає «завершеним». Коли ви копіюєте його мишкою з термінала, іноді хочеться, щоб воно було у формі URL. Це необовʼязково, але приємно.

Можна додати ще один рядок, якщо не хочете, щоб один був надто довгим:

// Підказка для користувача: у навчальному сценарії це найпростіший спосіб зупинити сервер
log.info("Натисніть Ctrl+C, щоб зупинити сервер.");

І так — це той випадок, коли трохи тексту в логах корисніше, ніж мовчання. Згадуйте: наші логи — це «панель приладів» застосунку. Якщо прилади не показують швидкість, ви все одно їдете, просто трохи нервово.

Ще нюанс: на старті server-mode корисно логувати те, що ми взагалі в server-mode, особливо якщо застосунок запускається з IDE і в вас паралельно бувають client-команди. Ви здивуєтеся, як часто «я думав, що запустив сервер, а запустив catalog details». З людиною таке трапляється; людина — не JVM.

Приклад:

log.info("Режим запуску: SERVER");

Головне — не переборщити. Два-три рядки — чудово. Двадцять три рядки з ASCII-артом — уже… теж мистецтво, але не те, яке допомагає.

6. Життєвий цикл після start()

Коли ви пишете консольні програми, main() зазвичай завершується — і процес закінчується. Це звично: зробив роботу, надрукував результат, вийшов. Серверний застосунок живе інакше. Він запускається і далі «чекає подій» — вхідних запитів. У сервера немає природного «я закінчив» (хіба що ви написали сервер, який після першого запиту йде у відпустку).

У HttpServer після start() з’являються робочі потоки, які продовжують слухати порт і приймати з’єднання. Тому навіть якщо ваш метод startServer(...) повернувся, JVM залишається жити. Це той самий момент, коли новачок дивиться в термінал і думає: «Ой, воно зависло». Ні, воно не зависло. Воно працює. Це сервер. Він чекає.

Що важливо зробити на рівні мислення: розділити два стани. «Програма зависла» — це коли вона не може продовжити й нічого не робить. «Сервер працює» — це коли він здебільшого нічого не робить (бо запитів немає), але готовий миттєво відреагувати, коли вони зʼявляться. Це як охоронець: якщо він сидить спокійно, це не означає, що він зламався. Це означає, що день спокійний (поки що).

Ще один момент: вам може захотітися «заблокувати main thread», щоб “точно не завершилося”. У HttpServer зазвичай це не потрібно, він і так тримає процес живим. І це добре: менше коду, менше магії, менше «а навіщо тут CountDownLatch?». Ми зберігаємо простоту доти, доки вона не заважає.

На цьому етапі важливо просто прийняти: у server-режимі застосунок не має завершуватися одразу. Він має запуститися, залогувати адресу — і жити, поки ви його не зупините. Зупинка в нашому навчальному сценарії — це банально Ctrl+C. Тема graceful shutdown — цікава, але це явно не мета сьогоднішнього дня (і вже точно не мета цієї лекції).

7. Типові помилки під час запуску сервера

У цьому місці зазвичай трапляється перша серія маленьких «а чому не працює». Це нормально: ви вперше переходите від «усе всередині процесу» до «є порт, є сервер, є зовнішній світ». Помилки тут часто не про Java, а про середовище й дисципліну запуску.

Помилка №1: хардкод порту в коді.
Здається безневинним написати new InetSocketAddress("localhost", 8080), тому що «ну ми ж на своїй машині». Але на своїй машині порт 8080 може бути зайнятий чим завгодно: іншим вашим проєктом, Docker (якщо він у вас є), чужим демо, IDE-плагіном. Правильна стратегія — брати host/port із конфігурації та мати можливість змінити порт без перекомпіляції. Навіть у навчальному проєкті це економить час.

Помилка №2: лог “Server started” без адреси.
Такий лог виглядає бадьоро, але він марний. Ви все одно в наступну секунду поставите питання: «А куди стукати?». Тому на старті сервера майже завжди логують URL або хоча б host:port. Один хороший лог зменшує кількість «чому не працює» приблизно наполовину — перевірено на людях (зокрема на авторі цих рядків).

Помилка №3: ловити IOException, але не логувати stack trace.
Іноді пишуть catch(IOException e) { log.error("Не вдалося"); }. Це як «лікар сказав: вам погано». Так, дякую. Якщо порт зайнятий, ви побачите у винятку BindException (часто всередині), і це одразу пояснює проблему. Тому в логові при помилці запуску майже завжди передають e, щоб отримати stack trace. У навчальному проєкті це особливо важливо: ви вчитеся читати помилки, а не боятися їх.

Помилка №4: запускати server-mode, але думати, що запустили client-mode (або навпаки).
Це дуже людська помилка. Ви змінюєте args, запускаєте, бачите, що щось вивелося, і мозок каже: «Ну, значить, сервер». А потім виявляється, що ви знову зробили catalog details, і жодного сервера немає. Лікується логом режиму запуску (Режим запуску: SERVER) і звичкою дивитися на лог запуску.

Помилка №5: складати код запуску сервера в ReadLaterApplication, поки він не стане нечитабельним.
На початку здається, що «так простіше». Але вже за наступну лекцію ви додасте /health, зʼявиться ObjectMapper, зʼявляться обробники — і ReadLaterApplication перетвориться на довгий сценарій, де складно знайти, що взагалі відбувається. Набагато здоровіше виділити LocalApiServer і тримати код запуску там, а точку входу залишити «диригентом»: обрати режим, зібрати залежності, викликати потрібний компонент.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ