CLI с Spring Shell

1. Общ преглед

Най-просто казано, проектът Spring Shell предоставя интерактивна обвивка за обработка на команди и изграждане на пълнофункционален CLI с помощта на програмен модел Spring.

В тази статия ще изследваме неговите функции, класове класове и пояснения и ще внедрим няколко персонализирани команди и персонализации.

2. Зависимост на Maven

Първо, трябва да добавим зависимостта spring-shell към нашия pom.xml :

 org.springframework.shell spring-shell 1.2.0.RELEASE 

Най-новата версия на този артефакт може да бъде намерена тук.

3. Достъп до черупката

Има два основни начина за достъп до черупката в нашите приложения.

Първият е да заредите черупката в точката на влизане на нашето приложение и да позволите на потребителя да въведе командите:

public static void main(String[] args) throws IOException { Bootstrap.main(args); }

Второто е да се получи JLineShellComponent и да се изпълнят командите програмно:

Bootstrap bootstrap = new Bootstrap(); JLineShellComponent shell = bootstrap.getJLineShellComponent(); shell.executeCommand("help");

Ще използваме първия подход, тъй като е най-подходящ за примерите в тази статия, но в изходния код можете да намерите тестови случаи, които използват втората форма.

4. Команди

Вече има няколко вградени команди в черупката, като изчистване , помощ , изход и т.н., които осигуряват стандартната функционалност на всеки CLI.

Персонализираните команди могат да бъдат изложени чрез добавяне на методи, отбелязани с анотацията @CliCommand вътре в Spring компонент, реализиращ интерфейса CommandMarker .

Всеки аргумент на този метод трябва да бъде маркиран с анотация @CliOption , ако не успеем да направим това, ще срещнем няколко грешки при опит за изпълнение на командата.

4.1. Добавяне на команди към черупката

Първо, трябва да уведомим черупката къде са нашите команди. За това той изисква файлът META-INF / spring / spring-shell-plugin.xml да присъства в нашия проект, там можем да използваме функционалността за сканиране на компоненти на Spring:

След като компонентите се регистрират и създадат екземпляр от Spring, те се регистрират с анализатора на обвивката и техните анотации се обработват.

Нека създадем две прости команди, едната, за да вземем съдържанието на URL и да ги покажем, а другата, за да запишем това съдържание във файл:

@Component public class SimpleCLI implements CommandMarker { @CliCommand(value = { "web-get", "wg" }) public String webGet( @CliOption(key = "url") String url) { return getContentsOfUrlAsString(url); } @CliCommand(value = { "web-save", "ws" }) public String webSave( @CliOption(key = "url") String url, @CliOption(key = { "out", "file" }) String file) { String contents = getContentsOfUrlAsString(url); try (PrintWriter out = new PrintWriter(file)) { out.write(contents); } return "Done."; } }

Имайте предвид, че можем да предадем повече от един низ на атрибутите стойност и ключ на @CliCommand и @CliOption съответно, това ни позволява да изложим няколко команди и аргументи, които се държат еднакво.

Сега нека проверим дали всичко работи както се очаква:

spring-shell>web-get --url //www.google.com web-save --url //www.google.com --out contents.txt Done.

4.2. Наличност на команди

Можем да използваме анотацията @CliAvailabilityIndicator за метод, връщащ булева стойност, за да се промени по време на изпълнение, ако дадена команда трябва да бъде изложена на черупката.

Първо, нека създадем метод за промяна на наличността на командата за уеб запазване :

private boolean adminEnableExecuted = false; @CliAvailabilityIndicator(value = "web-save") public boolean isAdminEnabled() { return adminEnableExecuted; }

Сега нека създадем команда за промяна на променливата adminEnableExecuted :

@CliCommand(value = "admin-enable") public String adminEnable() { adminEnableExecuted = true; return "Admin commands enabled."; }

И накрая, нека го проверим:

spring-shell>web-save --url //www.google.com --out contents.txt Command 'web-save --url //www.google.com --out contents.txt' was found but is not currently available (type 'help' then ENTER to learn about this command) spring-shell>admin-enable Admin commands enabled. spring-shell>web-save --url //www.google.com --out contents.txt Done.

4.3. Необходими аргументи

По подразбиране всички командни аргументи не са задължителни. Въпреки това можем да ги направим задължителни със задължителния атрибут на анотацията @CliOption :

@CliOption(key = { "out", "file" }, mandatory = true)

Сега можем да тестваме, че ако не го въведем, води до грешка:

spring-shell>web-save --url //www.google.com You should specify option (--out) for this command

4.4. Аргументи по подразбиране

Празна ключова стойност за @CliOption прави този аргумент по подразбиране. Там ще получим стойностите, въведени в черупката, които не са част от нито един именуван аргумент:

@CliOption(key = { "", "url" })

Сега нека проверим дали работи както се очаква:

spring-shell>web-get //www.google.com 

4.5. Helping Users

@CliCommand and @CliOption annotations provide a help attribute that allows us to guide our users when using the built-in help command or when tabbing to get auto-completion.

Let's modify our web-get to add custom help messages:

@CliCommand( // ... help = "Displays the contents of an URL") public String webGet( @CliOption( // ... help = "URL whose contents will be displayed." ) String url) { // ... }

Now, the user can know exactly what our command does:

spring-shell>help web-get Keyword: web-get Keyword: wg Description: Displays the contents of a URL. Keyword: ** default ** Keyword: url Help: URL whose contents will be displayed. Mandatory: false Default if specified: '__NULL__' Default if unspecified: '__NULL__' * web-get - Displays the contents of a URL. * wg - Displays the contents of a URL.

5. Customization

There are three ways to customize the shell by implementing the BannerProvider, PromptProvider and HistoryFileNameProvider interfaces, all of them with default implementations already provided.

Also, we need to use the @Order annotation to allow our providers to take precedence over those implementations.

Let's create a new banner to begin our customization:

@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class SimpleBannerProvider extends DefaultBannerProvider { public String getBanner() { StringBuffer buf = new StringBuffer(); buf.append("=======================================") .append(OsUtils.LINE_SEPARATOR); buf.append("* Baeldung Shell *") .append(OsUtils.LINE_SEPARATOR); buf.append("=======================================") .append(OsUtils.LINE_SEPARATOR); buf.append("Version:") .append(this.getVersion()); return buf.toString(); } public String getVersion() { return "1.0.1"; } public String getWelcomeMessage() { return "Welcome to Baeldung CLI"; } public String getProviderName() { return "Baeldung Banner"; } }

Note that we can also change the version number and welcome message.

Now, let's change the prompt:

@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class SimplePromptProvider extends DefaultPromptProvider { public String getPrompt() { return "baeldung-shell"; } public String getProviderName() { return "Baeldung Prompt"; } }

Finally, let's modify the name of the history file:

@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class SimpleHistoryFileNameProvider extends DefaultHistoryFileNameProvider { public String getHistoryFileName() { return "baeldung-shell.log"; } public String getProviderName() { return "Baeldung History"; } }

The history file will record all commands executed in the shell and will be put alongside our application.

With everything in place, we can call our shell and see it in action:

======================================= * Baeldung Shell * ======================================= Version:1.0.1 Welcome to Baeldung CLI baeldung-shell>

6. Converters

So far, we've only used simple types as arguments to our commands. Common types such as Integer, Date, Enum, File, etc., have a default converter already registered.

By implementing the Converter interface, we can also add our converters to receive custom objects.

Let's create a converter that can transform a String into an URL:

@Component public class SimpleURLConverter implements Converter { public URL convertFromText( String value, Class requiredType, String optionContext) { return new URL(value); } public boolean getAllPossibleValues( List completions, Class requiredType, String existingData, String optionContext, MethodTarget target) { return false; } public boolean supports(Class requiredType, String optionContext) { return URL.class.isAssignableFrom(requiredType); } }

Finally, let's modify our web-get and web-save commands:

public String webSave(... URL url) { // ... } public String webSave(... URL url) { // ... }

As you may have guessed, the commands behave the same.

7. Conclusion

In this article, we had a brief look at the core features of the Spring Shell project. We were able to contribute our commands and customize the shell with our providers, we changed the availability of commands according to different runtime conditions and created a simple type converter.

Complete source code for this article can be found over on GitHub.