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-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 есть отдельная, гораздо более удобная модель. Здесь нам достаточно научиться аккуратно принимать несколько понятных флажков и значений на старте, не устраивая ручной парсинг.

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