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 є окрема, набагато зручніша модель. Тут нам достатньо навчитися акуратно приймати кілька зрозумілих прапорців і значень на старті, не влаштовуючи ручний парсинг.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ