Как стать автором
Обновить
208.43

Spring Boot: создайте свой собственный CLI с помощью Spring Shell

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров1.6K
Автор оригинала: mydeveloperplanet

Команда Spring АйО перевела статью о создании собственного интерфейса командной строки  в Spring Boot приложении. Статья выполнена в форме туториала и приводит все необходимые разработчику простые шаги для первоначального освоения Spring Shell.


Вы хотите создать интерфейс командной строки (Command Line Interface, CLI) для своего Spring Boot приложения, потому что вам не нужен навороченный веб интерфейс? Spring Shell может стать ответом на ваш вопрос. Из этой статьи вы узнаете, как создать базовый CLI. Приятного чтения!

1. Введение

Не каждому приложению требуется GUI, способный откликаться на все действия пользователя. Иногда бывает достаточно базового терминала. Для этой цели Spring предлагает Spring Shell. Как и другие проекты Spring, Spring Shell избавляет вас от необходимости писать стандартный код, чтобы вы могли сосредоточиться на актуальной логике, которую вам необходимо реализовать. Некоторые встроенные команды, например, help, clear, exit и т.д. доступны из коробки, как и дополнение команды при помощи клавиши Tab. Вы можете создавать команды с обязательными и необязательными параметрами, а также добавлять валидацию аргументов, как вы сделали бы это в обычном Spring Boot приложении. Более того, вы можете создать исполняемый файл, используя GraalVM!

На момент написания статьи поддерживаются две модели аннотаций: старая (legacy) модель аннотаций (относящаяся к @ShellMethod и @ShellOption) и новая модель аннотаций. В этой статье мы будем использовать новую модель аннотаций. Однако отметим, что многие источники информации, которые вы можете найти в интернете, используют старую модель.

Если вам необходимо больше источников вдохновения или просто примеров, вы можете посмотреть на Spring Shell git repository.

Исходный код, использованный в этой статье, находится на GitHub.

2. Необходимые условия

Необходимыми условиями для чтения этой статьи являются:

  • Базовые познания о Java;

  • Базовые познания о Maven;

  • Базовые познания о Spring Boot.

3. Мое первое приложение на Shell

Перейдите на сайт Spring Initializr и добавьте зависимость Spring Shell. Посмотрите на pom и убедитесь, что добавлены следующие зависимости.

<dependencies>
    <dependency>
        <groupId>org.springframework.shell</groupId>
        <artifactId>spring-shell-starter</artifactId>
    </dependency>
 
    <dependency>
        <groupId>org.springframework.shell</groupId>
        <artifactId>spring-shell-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
 
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-dependencies</artifactId>
            <version>${spring-shell.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Создайте класс HelloWorld с одной командой hello-world. На что обратить внимание:

  • Класс должен быть помечен аннотацией @Command.

  • Метод, представляющий команды, также должен быть помечен @Command.

  • По умолчанию Spring Shell будет использовать имя метода в качестве имени команды. Вы также можете добавить параметр command в аннотации @Command для задания имени команды. Отметьте для себя, что когда имеются две команды с одним и тем же именем, сообщения об ошибке не будет. Вместо этого, будет использоваться первая найденная команда, а вторая будет недоступна. 

  • Можно добавить краткое описание команды через параметр description. Оно должно начинаться с большой буквы и заканчиваться точкой, потому что это считается best practice.

@Command
public class HelloWorld {
 
    @Command(description = "This command will say hello.")
    public String helloWorld() {
        return "Hello World!";
    }
 
}

Теперь вам надо сообщить Spring, что команду необходимо активировать. Это можно сделать в классе MySpringShellPlanetApplication. Вы можете:

  • Добавить аннотацию @EnableCommand и задать, какие именно классы следует отсканировать (закомментировано в приведенном фрагменте кода);

  • Добавить аннотацию @CommandScan, чтобы Spring отсканировал все классы.

@SpringBootApplication
//@EnableCommand(HelloWorld.class)
@CommandScan
public class MySpringShellPlanetApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(MySpringShellPlanetApplication.class, args);
    }
 
}

Spring Shell по умолчанию использует неинтерактивный режим, и если вы хотите включить интерактивный режим, вы можете сделать это, добавив следующее свойство в application.properties.

spring.shell.interactive.enabled=true

Однако, при сборке приложений с помощью Maven вам понадобится пропустить тесты, добавив -DskipTests, иначе тесты будут ждать интерактивного ввода со стороны пользователя, и сборка не закончится. В оставшейся части этой статьи, интерактивный режим не включен. 

Соберите приложение.

mvn clean verify

Запустите Java приложение командой с аргументом процесса hello-world.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-world
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 
 :: Spring Boot ::                (v3.3.5)
 
2024-11-10T09:48:04.050+01:00  INFO 10688 --- [MySpringShellPlanet] [           main] c.m.m.MySpringShellPlanetApplication     : Starting MySpringShellPlanetApplication v0.0.1-SNAPSHOT using Java 21 with PID 10688 (/home/<project directory>/myspringshellplanet/target/myspringshellplanet-0.0.1-SNAPSHOT.jar started by <user> in /home/<project directory>/myspringshellplanet)
2024-11-10T09:48:04.053+01:00  INFO 10688 --- [MySpringShellPlanet] [           main] c.m.m.MySpringShellPlanetApplication     : No active profile set, falling back to 1 default profile: "default"
2024-11-10T09:48:04.761+01:00  INFO 10688 --- [MySpringShellPlanet] [           main] c.m.m.MySpringShellPlanetApplication     : Started MySpringShellPlanetApplication in 0.952 seconds (process running for 1.215)
Hello World!

Прекрасно, приветственное сообщение напечаталось. А также баннер Spring Boot и лог.

Отключите баннер и логирование в консоли в файле application.properties.

spring.main.banner-mode=off
logging.pattern.console=

Соберите и запустите приложение еще раз. Его вывод теперь гораздо больше похож на вывод shell-приложения.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-world
Hello World!

4. Встроенные команды

Spring Shell предлагает несколько встроенных команд, которые можно просмотреть, выполнив команду help. Вы также увидите описание команды hello-world, которая была добавлена ранее.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar help
AVAILABLE COMMANDS
 
Built-In Commands
       help: Display help about available commands
       history: Display or save the history of previously run commands
       version: Show version info
       script: Read and execute commands from a file.
 
Default
       hello-world: This command will say hello.

Команда history покажет вам последнюю выполненную вами команду.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar history
[help, version, exit, help, exit]

Команда version показывает информацию по сборке и git, если она есть. Добавьте цель build-info к spring-boot-maven-plugin в pom.

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>build-info</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
  ...
</build>

Соберите приложение и покажите информацию о версии.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar version
Build Version: 0.0.1-SNAPSHOT

5. Групповые команды

В выводе команды help вы можете увидеть, что команда hello-world размещена в группе Default. Spring Shell позволяет вам группировать команды самостоятельно, добавляя параметр group.

@Command(group = "Group Commands")
public class GroupCommands {
 
    @Command()
    public String helloWorldGroup() {
        return "Hello World Group!";
    }
 
}

Соберите приложение, покажите help и выполните команду hello-world-group.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar help
AVAILABLE COMMANDS
 
Built-In Commands
       help: Display help about available commands
       history: Display or save the history of previously run commands
       version: Show version info
       script: Read and execute commands from a file.
 
Default
       hello-world: This command will say hello.
 
Group Commands
       hello-world-group:
 
$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-world-group
Hello World Group!

6. Опции

В предыдущих примерах возвращался простой текст. Но вам часто будет необходимо передавать аргументы командам. Поэтому вместо того, чтобы просто передавать Hello World, вы передаете аргумент name.

@Command(group = "Options")
public class Options {
 
    @Command
    public String helloName(String name) {
        return "Hello " + name + "!";
    }
}

Соберите приложение и запустите команду hello-name с аргументом и без.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-name Gunter
Hello Gunter!
$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-name
Missing mandatory option '--name'

Когда аргумент отсутствует, появляется сообщение об отсутствии обязательной опции Если аргумент не является обязательным, вы показываете это с помощью аннотации @Option. Вы можете добавлять значение по умолчанию как параметр к @Option (команда hello-name-default) или реализовать какую-то логику, чтобы попросить пользователя заполнить недостающий аргумент (команда hello-name-option).

@Command
public String helloNameDefault(@Option(defaultValue = "World") String name) {
    return "Hello " + name + "!";
}
 
@Command
public String helloNameOption(@Option String name) {
    if (name == null) {
        return askForName();
    }
 
    return "Hello " + name + "!";
}
 
private String askForName() {
    System.out.print("Please enter a name: ");
    return helloNameOption(System.console().readLine());
}

Соберите приложение и выполните обе команды.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-name-default
Hello World!
$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar hello-name-option
Please enter a name: Gunter
Hello Gunter!

7. Валидация

После передачи аргументов вам следует валидировать введенные данные, прежде чем начинать их обрабатывать. Просто воспользуйтесь Jakarta Bean Validation API, как вы привыкли поступать при использовании Spring Boot. В следующем примере мы проверяем, попадает ли параметр name в диапазон между минимальным и максимальным количеством символов.

@Command(group = "Validation")
public class Validation {
 
    @Command
    public String validateName(@Size(min = 2, max = 40, message = "Name must be between 2 and 40 characters long") String name) {
        return "Hello " + name + "!";
    }
 
}

Соберите приложение и запустите команду validate-name.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar validate-name G
The following constraints were not met:
ConstraintViolationImpl{interpolatedMessage='Name must be between 2 and 40 characters long', propertyPath=validateName.name, rootBeanClass=class com.mydeveloperplanet.myspringshellplanet.examples.Validation, messageTemplate='Name must be between 2 and 40 characters long'}

Это работает, но такое сообщение может создать плохой user experience. Чтобы обработать сообщение об ошибке надлежащим образом, вам надо создать CustomExceptionResolver, который реализует CommandExceptionHandler. Заметьте, что вам также необходимо добавить аннотацию @Order, иначе ваш кастомизированный resolver не обработается.

@Order(0)
public class CustomExceptionResolver implements CommandExceptionResolver {
 
    @Override
    public CommandHandlingResult resolve(Exception ex) {
        if (ex instanceof ParameterValidationException pve) {
            return handleConstraintViolation((pve));
        }
        return null; // Let other exception handlers deal with other types of exceptions
    }
 
    private CommandHandlingResult handleConstraintViolation(ParameterValidationException pve) {
        StringBuilder errorMessage = new StringBuilder("Validation error(s):\n");
        for (ConstraintViolation<?> violation : pve.getConstraintViolations()) {
            errorMessage.append("- ")
                        .append(violation.getMessage())
                        .append("\n");
        }
        return CommandHandlingResult.of(errorMessage.toString());
    }
}

Кроме того, вы регистрируете CustomExceptionResolver как бин в MySpringShellPlanetApplication.

@Bean
CustomExceptionResolver customExceptionResolver() {
    return new CustomExceptionResolver();
}

Соберите приложение и запустите команду validate-name. На этот раз появится адекватное сообщение об ошибке.

$ java -jar target/myspringshellplanet-0.0.1-SNAPSHOT.jar validate-name G
Validation error(s):
- Name must be between 2 and 40 characters long

8. Сборка в нативный образ

До этого момента вы исполняли shell приложение как программу на Java. Но было бы удобнее, если бы оно наш CLI стал исполняемым файлом. Это можно сделать с помощью GraalVM.

Добавьте следующий фрагмент к pom.

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <metadataRepository>
                        <enabled>true</enabled>
                    </metadataRepository>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Установите GraalVM для Java 21. Это можно сделать довольно просто, используя SDKMan. Команды ниже сначала установят GraalVM JDK, а потом сделают его JDK по умолчанию.

$ sdk install java 21.0.2-graalce
$ sdk use java 21.0.2-graalce

Соберите образ GraalVM.

$ mvn native:compile -Pnative
...
Produced artifacts:
 /home/<project directory>/myspringshellplanet/target/myspringshellplanet (executable)
========================================================================================================================
Finished generating 'myspringshellplanet' in 1m 26s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:33 min
[INFO] Finished at: 2024-11-10T14:42:28+01:00
[INFO] ------------------------------------------------------------------------

Опробуйте исполняемый файл. Он прекрасно и быстро работает!

$ ./target/myspringshellplanet hello-name Gunter
Hello Gunter!

9. Заключение

Spring Shell предоставляет очень удобный способ для создания CLI. И да, вы можете написать весь код сами в чистой Java, но вы получаете столько приятных возможностей из коробки, когда используете Spring Shell, что это сэкономит вам кучу времени.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Теги:
Хабы:
+7
Комментарии0

Публикации

Информация

Сайт
t.me
Дата регистрации
Численность
11–30 человек