JavaRush /Курси /Spring Boot /ApplicationArguments...

ApplicationArguments: option і non-option

Spring Boot
Рівень 6 , Лекція 1
Відкрита

1. Що таке ApplicationArguments

Коли застосунок запускається, він майже завжди отримує певні параметри: іноді це режим (dev, demo, local), іноді прапорці (--debug), іноді файли або просто «щось позиційне». У чистій Java ці параметри потрапляють у main(String[] args), і далі розробник або чесно розбирає їх сам, або відкладає це «на потім» і живе з вічним args.length > 0 ? args[0] : .... У Spring Boot підхід інший: оскільки застосунок усе одно працює всередині контейнера, аргументи запуску мають стати «нормальним об’єктом», який можна впровадити як залежність і читати без кустарщини.

ApplicationArguments — це обгортка над аргументами запуску, яку Boot створює автоматично. Її можна отримати прямо в ApplicationRunner — це особливо зручно, — а можна впровадити в будь-який Spring-керований компонент через конструктор. І головне: ApplicationArguments вже вміє відрізняти «опції» від «просто аргументів», тож вам не потрібно вручну гадати, де прапорець, де значення, а де випадково загубили пробіл або лапки.

Щоб відчути різницю, уявіть два підходи. Перший — «по-старому»:

// Ручний парсинг: самі шукаємо потрібний параметр у сирому масиві рядків
for (String arg : args) {
    // Домовляємося про формат і перевіряємо префікс
    if (arg.startsWith("--mode=")) {
        // ...
    }
}

Другий — у стилі Boot: ви отримуєте ApplicationArguments і запитуєте в нього: «Чи є опція mode?» та «Які в неї значення?». Код виходить коротшим, зрозумілішим і, що особливо приємно, менш крихким. Spring Boot у цьому місці поводиться як хороший колега: він уже зробив нудну частину роботи, а вам залишив змістову.

2. Option і non-option аргументи: домовимося про формат

Перед тим як писати код, потрібно навести лад у термінах, інакше ми сперечатимемося з комп’ютером на тему «це прапорець чи просто слово». Spring Boot ділить аргументи запуску на дві великі групи: option і non-option. Option-аргументи — це ті, що починаються з -- і виглядають як «параметр» або «прапорець» застосунку. Non-option — це все інше, тобто позиційні штуки: імʼя файлу, довільний текст, список значень «без ключів». Це дуже схоже на те, як працюють багато консольних утиліт: спочатку прапорці, потім позиційні аргументи, і кожен знає своє місце.

У світі Boot найбезпечніший і найпередбачуваніший формат option-аргументу — це --name=value. Наприклад, --mode=dev або --limit=10. Окремо є прапорці без значення, наприклад --debug. Їхній зміст зазвичай такий: «увімкнути щось», і для них найчастіше достатньо просто перевірити наявність опції. А ось non-option аргументи — це, наприклад, catalog-data.yaml або demo, якщо ви передали його без --.

Важливо не переплутати ще один момент: --mode dev через пробіл для Boot не найнадійніший формат. У реальних командах люди так пишуть часто, але для парсингу Spring Boot простіше й стабільніше дотримуватися --mode=dev. Тому далі в прикладах ми будемо використовувати саме цей варіант, щоб не отримати ситуацію «я ж передав mode, чому він не читається?!».

Невеличка табличка для фіксації домовленості:

Що передали під час запуску Як Boot це бачить Приклад змісту
--debug option debug, без значень увімкнути режим налагодження
--mode=dev option mode зі значенням dev обрати режим запуску
courses.json non-option аргумент умовно «файл даних»
hello non-option аргумент позиційне значення

3. Основні методи ApplicationArguments

Коли ви вперше бачите ApplicationArguments, хочеться запитати: «Гаразд, а що я можу в нього попросити?» Хороша новина: методів там небагато, і всі вони з людськими назвами. Погана новина, радше кумедна: розробники іноді примудряються помилитися навіть із ними — наприклад, забувають, що getOptionValues(...) може повернути null або порожній список. Тому тут ми закріпимо базовий набір методів і трохи їхній характер.

Нижче — той мінімум, який закриває 90% навчальних і реальних сценаріїв запуску:

Метод Що повертає Коли використовувати
getSourceArgs() вихідний масив String[] якщо потрібно побачити, як надійшло
containsOption("name") boolean для прапорців типу --debug
getOptionValues("name") List<String> або null для --mode=dev, --tag=spring
getNonOptionArgs() List<String> для позиційних аргументів

Є ще getOptionNames(), який повертає набір імен опцій. Він корисний для «діагностичного принта»: показати користувачеві або собі, що реально надійшло на вхід. Але зловживати ним не варто: стартова логіка має бути короткою, а не перетворюватися на «консольний серіал на 12 сезонів».

Одразу проговоримо два тонкі моменти, які часто ловлять новачків.

Перший: у containsOption("mode") імʼя пишеться без --. Тобто ви перевіряєте "mode", а не "--mode".

Другий: getOptionValues("debug") для прапорця --debug цілком може повернути порожній список. Це не вада, це логічно: значення не передавали. Тому прапорці зазвичай читають через containsOption, а не через getOptionValues.

4. Практика в catalog-service: читаємо --mode, прапорці й аргументи

Зараз ми зробимо те, що дуже люблять початківці: «швиденько надрукуємо, що надійшло». Але зробимо це акуратно й у правильному місці — в ApplicationRunner, а не в main() і не в конструкторі біна. І так, поки що ми друкуємо в консоль через System.out.println, тому що зараз нам потрібен простий видимий сигнал, а не окрема історія про логування.

Припустімо, що catalog-service ми іноді запускаємо в різних режимах, суто для навчальної діагностики: --mode=demo, --mode=local тощо. Плюс хочемо підтримати прапорець --summary, який дозволяє друкувати стартове зведення. І ще хочемо вміти приймати позиційні аргументи як «список файлів» — навіть якщо поки що це просто рядки.

Нижче — серія коротких пробних класів. Не тримайте їх усі одночасно в catalog-service: кожен показує один бік ApplicationArguments, а робочу, більш спокійну форму зберемо наприкінці секції.

Приклад 1: друкуємо опції та позиційні аргументи

Створимо невеликий runner, який покаже структуру входу. Покладемо його, наприклад, у пакет com.example.catalogservice.catalog.bootstrap (пакети ми ще наведемо до ладу пізніше, зараз головне — щоб це був Spring-компонент).

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class ArgsInspectorRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Показуємо, які option-імена взагалі надійшли (без значень)
        System.out.println("Імена опцій: " + args.getOptionNames());      // Імена опцій: [mode, summary]
        // Показуємо позиційні аргументи (усе, що не починається з --)
        System.out.println("Позиційні аргументи: " + args.getNonOptionArgs()); // Позиційні аргументи: [data.txt]
    }
}

Цей клас робить дві корисні речі. По-перше, показує, що опції справді виділяються як «імена». По-друге, показує, що позиційні аргументи залишаються окремо. Уже на цьому етапі ви можете перестати писати args[0], args[1] і гадати, чого саме ви там «за позицією» очікували.

Приклад 2: читаємо --mode=... зі значенням

Тепер зробимо runner, який читає --mode=... і акуратно обирає режим. Тут ми зобов’язані пам’ятати про null і порожній список, тому що --mode може бути вказаний без =значення — люди іноді так роблять — або взагалі не вказаний.

import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class ModePrinterRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Для option із можливим значенням читаємо список значень (може бути null або порожній)
        List<String> values = args.getOptionValues("mode");
        // Беремо перше значення або використовуємо дефолт, якщо значення не надійшли
        String mode = (values == null || values.isEmpty()) ? "default" : values.get(0);

        System.out.println("Режим = " + mode); // Режим = dev
    }
}

Так, це схоже на «багато перевірок заради одного рядка», але в реальному світі це рятує від загадкових NullPointerException на старті. А ще цей приклад поступово вчить доброї звички: якщо в аргументу є значення, ви завжди маєте думати про випадки, коли значення не надійшло.

Приклад 3: читаємо прапорець --summary без значень

Прапорець — це ідеальний кандидат для containsOption. Не треба викликати getOptionValues і сподіватися, що список буде непорожнім. Якщо опція присутня, значить прапорець увімкнули.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class SummaryFlagRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Для прапорців важлива наявність опції, а не список значень
        boolean summaryEnabled = args.containsOption("summary");
        System.out.println("Увімкнено зведення = " + summaryEnabled); // Увімкнено зведення = true
    }
}

Психологічно це приємно: код читається як нормальна фраза. А ще це зручно, коли ви привчаєте себе не ускладнювати — якщо прапорець бінарний, не треба будувати трактат про його розбір.

Приклад 4: позиційні аргументи як список «файлів»

Non-option аргументи особливо корисні, коли ви хочете приймати «щось списком». Наприклад, catalog-service data1.txt data2.txt. Поки що ми не робимо справжнє файлове завантаження — і не будемо, — але навчимося хоча б отримувати список.

import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class InputFilesRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Усе, що не є option (--...), потрапляє сюди як «позиційний хвіст»
        List<String> files = args.getNonOptionArgs();
        System.out.println("Вхідні файли = " + files); // Вхідні файли = [data1.txt, data2.txt]
    }
}

Навіть якщо ви потім узагалі перестанете використовувати позиційні аргументи, цей приклад корисний як «розумовий якір»: Boot не намагається все перетворити на властивості, він дає вам і опції, і позиційні значення — просто розкладеними по поличках.

5. Акуратне читання значень: null, порожній список і кілька значень

На цьому місці майже в усіх виникає питання: «Чому не можна просто взяти args.getOptionValues("mode").get(0) і жити спокійно?» Можна. Рівно до першого запуску, коли --mode не передали, або передали як --mode без значення, або передали двічі, або IDE якимось чином зʼїла лапки. Аргументи запуску — це зовнішні дані. А зовнішні дані, як відомо, обожнюють приходити не так, як ви очікували.

Тому давайте зафіксуємо три найчастіші «особливі випадки» і як із ними жити.

null: опцію не передали взагалі

Якщо getOptionValues("mode") повернув null, це означає просту річ: опцію --mode взагалі не передавали. У такому випадку логічно обрати значення за замовчуванням. У навчальному проєкті це часто "default" або "local". Важливо, що це не помилка — це нормальна ситуація.

Порожній список: опцію передали, але без значення

Якщо передали --mode без =dev, то опція технічно є, але значень немає. Для прапорців це нормально, а от для «режиму» — уже дивно. Зазвичай у таких випадках або беруть дефолт, або друкують попередження. Поки що ми обмежимося дефолтом, тому що наше завдання — навчитися читати, а не сварити користувача.

Ось мініприклад «акуратної функції», яку можна тримати прямо в runner або трохи пізніше винести:

import java.util.List;

public class CliReadSupport {

    public static String firstOrDefault(List<String> values, String def) {
        // values == null: опцію не передали взагалі
        // values.isEmpty(): опцію передали, але без значень
        return (values == null || values.isEmpty()) ? def : values.get(0);
    }
}

Так, це утиліта. Так, звучить як «ще один клас». Але думка корисна: винести повторювану перевірку — це не гріх, якщо вона робить код читабельнішим.

Кілька значень: опцію передали багато разів

Іноді ви хочете дозволити повторювану опцію: наприклад --tag=spring --tag=boot. Boot спокійно перетворює це на список. Це гарний момент, щоб побачити, що getOptionValues справді повертає List<String>, а не «одне значення».

import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class TagsRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Повторювана опція перетворюється на список значень
        List<String> tags = args.getOptionValues("tag");
        System.out.println("Теги = " + tags); // Теги = [spring, boot]
    }
}

Якщо tags == null, значить --tag не передавали. Якщо список порожній, значить передали --tag без значень. Якщо є елементи — значить усе добре. І ось тепер ваш мозок починає «відчувати» ApplicationArguments: він перетворює вхід у структуру, але не вдає, що вхід завжди ідеальний.

6. Передавання аргументів: IDE і bootRun

Поки ми пишемо код, важливо вміти швидко перевіряти його в дії. Аргументи запуску — це тема, яка не закріплюється читанням, вона закріплюється моментом «передали — побачили — виправили». Хороша новина в тому, що в Boot-проєкті зазвичай є щонайменше два зручні способи передати аргументи: через IDE і через Gradle-завдання bootRun. І обидва способи корисні, тому що в житті ви стикатиметеся з різними сценаріями запуску.

Якщо ви запускаєте застосунок з IDE, майже в будь-якому середовищі розробки є поле “Program arguments” або аналог. Туди можна писати, наприклад:

--mode=dev --summary data1.txt data2.txt

Після старту ваші ранери побачать mode=dev, прапорець summary і два non-option аргументи.

Якщо ви запускаєте через Gradle, зазвичай використовують bootRun і передають аргументи через --args. Виглядає це приблизно так — зверніть увагу на лапки: оболонка командного рядка дуже любить «допомогти» вам і розібрати рядок по-своєму:

./gradlew bootRun --args="--mode=dev --summary data1.txt"

І так, якщо ви бачите дивні речі, наприклад усе потрапило в один аргумент, у 80 % випадків винен не Spring Boot, а лапки, пробіли й те, що ваша оболонка вирішила зіграти в «вгадай формат». Це нормально. Навіть досвідчені розробники іноді воюють із лапками. Просто вони вже не соромляться.

7. Мінірефакторинг: StartupCliOptions

Коли ви тільки вчитеся, дуже хочеться читати аргументи прямо в runner. І це нормально для перших прикладів. Але щойно runner починає робити більше ніж дві перевірки, він швидко перетворюється на комбайн: тут читаємо mode, тут читаємо summary, тут парсимо limit, тут друкуємо… і раптом ви вже не розумієте, де логіка старту, а де логіка читання входу. Тому корисний проміжний крок — винести читання аргументів у маленький клас «опції запуску». Це вже ближче до варіанта, який має сенс залишати в проєкті, якщо аргументи справді потрібні сервісу.

Зробимо простий компонент StartupCliOptions, який читає ApplicationArguments і дає зрозумілі методи. Це допоможе тримати runner коротким і читабельним — а це одне з головних правил сьогодні: runner не повинен ставати «другим застосунком усередині застосунку».

import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

@Component
public class StartupCliOptions {

    private final ApplicationArguments args;

    public StartupCliOptions(ApplicationArguments args) {
        // Впроваджуємо вже розібрані аргументи запуску як залежність
        this.args = args;
    }

    public boolean isSummaryEnabled() {
        // Прапорець: достатньо перевірити наявність option за ім'ям
        return args.containsOption("summary");
    }

    public String modeOrDefault() {
        // Опція зі значенням: читаємо список і акуратно обробляємо null/empty
        List<String> values = args.getOptionValues("mode");
        return (values == null || values.isEmpty()) ? "default" : values.get(0);
    }
}

А тепер runner, який використовує цей клас, виглядає майже як нормальний сценарій, а не як «парсер аргументів на коліні»:

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

@Component
public class StartupOptionsRunner implements ApplicationRunner {

    private final StartupCliOptions options;

    public StartupOptionsRunner(StartupCliOptions options) {
        // Тут runner отримує вже «читабельні» опції, а не розбирає args сам
        this.options = options;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Зверніть увагу: args майже не потрібен — ми працюємо через options
        System.out.println("Режим = " + options.modeOrDefault());                 // Режим = dev
        System.out.println("Увімкнено зведення = " + options.isSummaryEnabled()); // Увімкнено зведення = true
    }
}

Зверніть увагу на маленьку, але важливу річ: ApplicationArguments args у методі run() нам тут майже не потрібен. Ми все читаємо через StartupCliOptions. Це не «єдино правильний» стиль, але він дуже навчально здоровий: runner відповідає за стартову задачу, а окремий компонент — за інтерпретацію аргументів.

8. Типові помилки під час роботи з ApplicationArguments

Помилка №1: плутати імʼя опції та її «командний запис».
Дуже часта дрібниця: розробник пише containsOption("--mode") і дивується, чому завжди false. У ApplicationArguments імʼя опції — це "mode", без --. Префікс -- існує тільки в рядку запуску, а не в програмному API. Якщо тримати це в голові, половина дивних багів зникає.

Помилка №2: вважати, що getOptionValues(...) завжди повертає список, і одразу робити .get(0).
На старті це виглядає «логічно», але на практиці ламається миттєво: коли опцію не передали, ви отримаєте null; коли передали без значення — порожній список. Тому для значущих опцій майже завжди потрібен шаблон values == null || values.isEmpty() ? default : values.get(0).

Помилка №3: намагатися читати прапорці через getOptionValues.
Для --debug або --summary вам зазвичай не важливе значення, вам важлива наявність. Якщо ви читаєте такі прапорці через getOptionValues, ви починаєте писати зайві перевірки, і код стає важчим. Для прапорців краще використовувати containsOption: це і простіше, і ближче до змісту.

Помилка №4: робити парсинг аргументів усередині бізнес-логіки і розмазувати його по проєкту.
Якщо один сервіс читає --mode, інший читає --summary, третій читає позиційні аргументи, то за тиждень ви не зможете відповісти на запитання: «Які параметри запуску взагалі підтримує застосунок?» Набагато спокійніше мати одне місце, хай навіть маленьке, на кшталт StartupCliOptions, де правила читання аргументів зібрані разом, а решта коду просто запитує: «Який режим?» і «Чи ввімкнене зведення?».

Помилка №5: перетворювати аргументи запуску на «універсальну конфігурацію всього».
Аргументи — це зручний інструмент, але в них інша роль: швидко й явно вплинути на старт. Якщо ви починаєте пхати туди десятки параметрів, це перетворюється на погано керовану конфігурацію «на рядках». Для серйозної конфігурації в Boot є окрема, набагато зручніша модель. Тут нам достатньо навчитися акуратно приймати кілька зрозумілих прапорців і значень на старті, не влаштовуючи ручний парсинг.

1
Задача
Spring Boot, 6 рівень, 1 лекція
Недоступна
Прапорець `--debug` через ApplicationArguments
Прапорець `--debug` через ApplicationArguments
1
Задача
Spring Boot, 6 рівень, 1 лекція
Недоступна
Режим запуску та позиційні аргументи
Режим запуску та позиційні аргументи
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ