Eine CLI mit Spring Shell

Eine CLI mit Spring Shell

1. Überblick

Einfach ausgedrückt bietet die Spring Shellproject eine interaktive Shell zum Verarbeiten von Befehlen und zum Erstellen einer CLI mit vollem Funktionsumfang unter Verwendung des Spring-Programmiermodells.

In diesem Artikel werden die Funktionen, Schlüsselklassen und Anmerkungen erläutert und verschiedene benutzerdefinierte Befehle und Anpassungen implementiert.

2. Maven-Abhängigkeit

Zuerst müssen wir die Abhängigkeit vonspring-shellzu unserenpom.xmlhinzufügen:


    org.springframework.shell
    spring-shell
    1.2.0.RELEASE

Die neueste Version dieses Artefakts befindet sich inhere.

3. Zugriff auf die Shell

In unseren Anwendungen gibt es zwei Möglichkeiten, auf die Shell zuzugreifen.

Die erste besteht darin, die Shell im Einstiegspunkt unserer Anwendung zu booten und den Benutzer die folgenden Befehle eingeben zu lassen:

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

Die zweite besteht darin, einJLineShellComponent zu erhalten und die Befehle programmgesteuert auszuführen:

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

Wir werden den ersten Ansatz verwenden, da er für die Beispiele in diesem Artikel am besten geeignet ist. Im Quellcode finden Sie jedoch Testfälle, die das zweite Formular verwenden.

4. Befehle

In der Shell sind bereits mehrere Befehle integriert, z. B.clear,help,exit usw., die die Standardfunktionalität jeder CLI bereitstellen.

Benutzerdefinierte Befehle können durch Hinzufügen von Methoden verfügbar gemacht werden, die mit der Annotation@CliCommandin einer Spring-Komponente gekennzeichnet sind, die die SchnittstelleCommandMarkerimplementiert.

Jedes Argument dieser Methode muss mit einer@CliOption-Annotation gekennzeichnet sein. Wenn wir dies nicht tun, treten beim Versuch, den Befehl auszuführen, mehrere Fehler auf.

4.1. Hinzufügen von Befehlen zur Shell

Zuerst müssen wir der Shell mitteilen, wo sich unsere Befehle befinden. Dazu muss die DateiMETA-INF/spring/spring-shell-plugin.xml in unserem Projekt vorhanden sein. Dort können wir die Komponenten-Scan-Funktion von Spring verwenden:


    

Sobald die Komponenten von Spring registriert und instanziiert wurden, werden sie beim Shell-Parser registriert und ihre Anmerkungen verarbeitet.

Erstellen wir zwei einfache Befehle, einen zum Abrufen und Anzeigen des Inhalts einer URL und einen zum Speichern dieses Inhalts in einer Datei:

@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.";
    }
}

Beachten Sie, dass wir mehr als eine Zeichenfolge an die Attributevalue undkey von@CliCommand bzw.@CliOption übergeben können. Auf diese Weise können wir mehrere Befehle und Argumente verfügbar machen, die sich gleich verhalten .

Überprüfen wir nun, ob alles wie erwartet funktioniert:

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

4.2. Verfügbarkeit von Befehlen

Wir können die Annotation@CliAvailabilityIndicator für eine Methode verwenden, dieboolean zurückgibt, um zur Laufzeit zu ändern, ob ein Befehl für die Shell verfügbar gemacht werden soll.

Erstellen wir zunächst eine Methode, um die Verfügbarkeit des Befehlsweb-savezu ändern:

private boolean adminEnableExecuted = false;

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

Erstellen wir nun einen Befehl zum Ändern der VariablenadminEnableExecuted:

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

Lassen Sie es uns abschließend überprüfen:

spring-shell>web-save --url https://www.google.com --out contents.txt
Command 'web-save --url https://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 https://www.google.com --out contents.txt
Done.

4.3. Erforderliche Argumente

Standardmäßig sind alle Befehlsargumente optional. Wir können sie jedoch mit dem Attributmandatory der Annotation@CliOptionerforderlich machen:

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

Jetzt können wir testen, ob ein Fehler auftritt, wenn wir ihn nicht einführen:

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

4.4. Standardargumente

Ein leererkey-Wert für ein@CliOption macht dieses Argument zum Standard. Dort erhalten wir die in die Shell eingeführten Werte, die nicht Teil eines benannten Arguments sind:

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

Überprüfen wir nun, ob es wie erwartet funktioniert:

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

4.5. Benutzer helfen

Die Anmerkungen@CliCommand und@CliOption bieten ein Attributhelp, mit dem wir unsere Benutzer bei der Verwendung des integrierten Befehlshelp oder beim Tabulieren zur automatischen Vervollständigung anleiten können.

Ändern Sie unsereweb-get, um benutzerdefinierte Hilfemeldungen hinzuzufügen:

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

Jetzt kann der Benutzer genau wissen, was unser Befehl tut:

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. Anpassung

Es gibt drei Möglichkeiten, die Shell anzupassen, indem die SchnittstellenBannerProvider,PromptProvider undHistoryFileNameProvider implementiert werden. Alle Schnittstellen sind bereits mit Standardimplementierungen ausgestattet.

Außerdem müssen wir die Annotation@Orderverwenden, damit unsere Anbieter Vorrang vor diesen Implementierungen haben.

Erstellen wir ein neues Banner, um mit der Anpassung zu beginnen:

@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("*          example 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 example CLI";
    }

    public String getProviderName() {
        return "example Banner";
    }
}

Beachten Sie, dass wir auch die Versionsnummer und die Begrüßungsnachricht ändern können.

Ändern wir nun die Eingabeaufforderung:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimplePromptProvider extends DefaultPromptProvider {

    public String getPrompt() {
        return "example-shell";
    }

    public String getProviderName() {
        return "example Prompt";
    }
}

Zuletzt ändern wir den Namen der Verlaufsdatei:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleHistoryFileNameProvider
  extends DefaultHistoryFileNameProvider {

    public String getHistoryFileName() {
        return "example-shell.log";
    }

    public String getProviderName() {
        return "example History";
    }

}

Die Verlaufsdatei zeichnet alle in der Shell ausgeführten Befehle auf und wird zusammen mit unserer Anwendung abgelegt.

Wenn alles vorhanden ist, können wir unsere Shell aufrufen und in Aktion sehen:

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

6. Konverter

Bisher haben wir nur einfache Typen als Argumente für unsere Befehle verwendet. Bei gängigen Typen wieInteger,Date,Enum,File usw. ist bereits ein Standardkonverter registriert.

Durch die Implementierung derConverter-Schnittstelle können wir auch unsere Konverter hinzufügen, um benutzerdefinierte Objekte zu empfangen.

Erstellen wir einen Konverter, derString inURL umwandeln kann:

@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);
    }
}

Zum Schluss ändern wir unsere Befehleweb-get undweb-save:

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

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

Wie Sie vielleicht erraten haben, verhalten sich die Befehle gleich.

7. Fazit

In diesem Artikel haben wir uns kurz mit den Kernfunktionen des Spring Shell-Projekts befasst. Wir konnten unsere Befehle einbringen und die Shell mit unseren Providern anpassen, die Verfügbarkeit von Befehlen an unterschiedliche Laufzeitbedingungen anpassen und einen einfachen Typkonverter erstellen.

Den vollständigen Quellcode für diesen Artikel finden Sie unterover on GitHub.