1. Что такое ApplicationArguments
Когда приложение запускается, оно почти всегда получает какие-то параметры: иногда это режим (dev, demo, local), иногда флаги (--debug), иногда файлы или просто «что-то позиционное». В чистой Java эти параметры попадают в main(String[] args), и дальше разработчик либо честно парсит их сам, либо откладывает это «на потом» и живёт с вечным args.length > 0 ? args[0] : .... В Spring Boot идея другая: раз уж приложение всё равно живёт внутри контейнера, то и аргументы старта должны стать «нормальным объектом», который можно внедрять как зависимость и читать без кустарщины.
ApplicationArguments — это обёртка над аргументами запуска, которую Boot создаёт автоматически. Её можно получить прямо в ApplicationRunner (что особенно удобно), а можно внедрить в любой Spring-managed компонент через constructor injection. И главное: 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, потому что сейчас нам нужен простой видимый сигнал, а не отдельная история про logging.
Представим, что catalog-service мы иногда запускаем в разных режимах, чисто для учебной диагностики: --mode=demo, --mode=local и так далее. Плюс хотим поддержать флаг --summary, который «разрешает» печатать стартовую сводку. И ещё хотим уметь принимать позиционные аргументы как «список файлов» (пусть даже пока это просто строки).
Ниже — серия коротких probe-классов. Не держим их все одновременно в catalog-service: каждый показывает одну грань ApplicationArguments, а рабочую, более спокойную форму соберём в конце секции.
Пример 1: печатаем опции и позиционные аргументы
Создадим небольшой runner, который покажет структуру входа. Положим его, например, в пакет com.example.catalogservice.catalog.bootstrap (пакеты мы будем наводить красотой позже, сейчас главное — чтобы это был Spring-component).
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("Option names: " + args.getOptionNames()); // Option names: [mode, summary]
// Показываем позиционные аргументы (всё, что не начинается с --)
System.out.println("Non-option args: " + args.getNonOptionArgs()); // Non-option args: [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 = " + mode); // 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("Summary enabled = " + summaryEnabled); // Summary enabled = 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("Input files = " + files); // Input 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 = " + tags); // 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
После старта ваши runner’ы увидят 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("Mode = " + options.modeOrDefault()); // Mode = dev
System.out.println("Summary enabled = " + options.isSummaryEnabled()); // Summary enabled = 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 есть отдельная, гораздо более удобная модель. Здесь нам достаточно научиться аккуратно принимать несколько понятных флажков и значений на старте, не устраивая ручной парсинг.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ