Opdrachtregelparameters parseren met JCommander

1. Overzicht

In deze tutorial we zullen leren hoe we JCommander kunnen gebruiken om opdrachtregelparameters te ontleden. We zullen verschillende functies onderzoeken terwijl we een eenvoudige opdrachtregelapplicatie bouwen.

2. Waarom JCommander?

"Omdat het leven te kort is om opdrachtregelparameters te ontleden" - Cédric Beust

JCommander, gemaakt door Cédric Beust, is een annotatie-gebaseerde bibliotheek voorhet ontleden van opdrachtregelparameters. Het kan de moeite van het bouwen van opdrachtregelapplicaties verminderen en ons helpen om ze een goede gebruikerservaring te bieden.

Met JCommander kunnen we lastige taken zoals parsing, validatie en typeconversies overbodig maken, zodat we ons kunnen concentreren op onze applicatielogica.

3. JCommander instellen

3.1. Maven-configuratie

Laten we beginnen met het toevoegen van de jcommander afhankelijkheid in onze pom.xml:

 com.beust jcommander 1.78 

3.2. Hallo Wereld

Laten we een eenvoudig maken HalloWorldApp dat vereist een enkele invoer genaamd naam en drukt een begroeting af, "Hallo ".

Sinds JCommander bindt opdrachtregelargumenten aan velden in een Java-klasse, we zullen eerst een HalloWereldArgs klasse met een veld naam geannoteerd met @Parameter:

class HelloWorldArgs {@Parameter (names = "--name", description = "User name", required = true) private String naam; }

Laten we nu de JCommander class om de opdrachtregelargumenten te ontleden en de velden in onze HalloWereldArgs voorwerp:

HelloWorldArgs jArgs = nieuw HelloWorldArgs (); JCommander helloCmd = JCommander.newBuilder () .addObject (jArgs) .build (); helloCmd.parse (args); System.out.println ("Hallo" + jArgs.getName ());

Laten we tot slot de hoofdklasse aanroepen met dezelfde argumenten vanuit de console:

$ java HelloWorldApp --naam JavaWorld Hallo JavaWorld

4. Een echte applicatie bouwen in JCommander

Nu we aan de slag zijn, gaan we eens kijken naar een complexere use-case: een opdrachtregel-API-client die communiceert met een factureringstoepassing zoals Stripe, met name het scenario met gemeten (of op gebruik gebaseerde) facturering. Deze externe facturatiedienst beheert onze abonnementen en facturering.

Laten we ons voorstellen dat we een SaaS-bedrijf runnen, waarin onze klanten abonnementen op onze services kopen en worden gefactureerd voor het aantal API-aanroepen naar onze services per maand. We voeren twee bewerkingen uit in onze client:

  • indienen: Dien het aantal en de eenheidsprijs van het gebruik in voor een klant voor een bepaald abonnement
  • halen: Kosten voor een klant ophalen op basis van het verbruik van sommige of alle abonnementen in de huidige maand - we kunnen deze kosten bij elkaar opgeteld krijgen over alle abonnementen of gespecificeerd per abonnement

We bouwen de API-client terwijl we de functies van de bibliotheek doornemen.

Laten we beginnen!

5. Een parameter definiëren

Laten we beginnen met het definiëren van de parameters die onze applicatie kan gebruiken.

5.1. De @Parameter Annotatie

Een veld annoteren met @Parameter vertelt JCommander om er een overeenkomend opdrachtregelargument aan te binden. @Parameter heeft attributen om de hoofdparameter te beschrijven, zoals:

  • namen - een of meer namen van de optie, bijvoorbeeld ‘–naam’ of ‘-n’
  • Omschrijving - de betekenis achter de optie, om de eindgebruiker te helpen
  • verplicht - of de optie verplicht is, is standaard ingesteld op false
  • ariteit - aantal aanvullende parameters dat de optie verbruikt

Laten we een parameter configureren Klanten ID in ons scenario met gemeten facturering:

@Parameter (names = {"--customer", "-C"}, description = "Id van de klant die de services gebruikt", arity = 1, required = true) String customerId; 

Laten we nu onze opdracht uitvoeren met de nieuwe parameter "–customer":

$ java App --customer cust0000001A Lees klant-ID: cust0000001A. 

Evenzo kunnen we de kortere "-C" -parameter gebruiken om hetzelfde effect te bereiken:

$ java App -C cust0000001A Lees klant-ID: cust0000001A. 

5.2. Vereiste parameters

Waar een parameter verplicht is, verlaat de applicatie het gooien van een ParameterException als de gebruiker het niet specificeert:

$ java App-uitzondering in thread "main" com.beust.jcommander.ParameterException: De volgende optie is vereist: [--customer | -C]

We moeten opmerken dat in het algemeen elke fout bij het parseren van de parameters resulteert in een ParameterException in JCommander.

6. Ingebouwde typen

6.1. IStringConverter Koppel

JCommander voert typeconversie uit vanaf de opdrachtregel Draad invoer in de Java-typen in onze parameterklassen. De IStringConverter interface zorgt voor de typeconversie van een parameter van Draad naar elk willekeurig type. Dus alle ingebouwde converters van JCommander implementeren deze interface.

JCommander wordt standaard geleverd met ondersteuning voor veelgebruikte gegevenstypen, zoals Draad, Geheel getal, Boolean, BigDecimal, en Enum.

6.2. Single-Arity-typen

Arity heeft betrekking op het aantal aanvullende parameters dat een optie gebruikt. JCommander's ingebouwde parametertypes hebben een standaard ariteit van één, behalve voor Boolean en Lijst. Daarom zijn veel voorkomende soorten zoals Draad, Geheel getal, BigDecimal, Lang, en Enum, zijn typen met één ariteit.

6.3. Boolean Type

Velden van type boolean of Boolean geen extra parameter nodig - deze opties hebben een ariteit van nul.

Laten we naar een voorbeeld kijken. Misschien willen we de kosten voor een klant ophalen, gespecificeerd naar abonnement. We kunnen een boolean veld- gespecificeerd, dat is false standaard:

@Parameter (names = {"--itemized"}) privé booleaans gespecificeerd; 

Onze applicatie zou geaggregeerde kosten retourneren met gespecificeerd ingesteld op false. Wanneer we de opdrachtregel oproepen met de gespecificeerd parameter, stellen we het veld in op waar:

$ java App --itemized Lees vlag gespecificeerd: waar. 

Dit werkt goed, tenzij we een use-case hebben waarbij we altijd gespecificeerde kosten willen, tenzij anders vermeld. We zouden de parameter kunnen veranderen in zijn notItemized, maar het is misschien duidelijker om te kunnen voorzien false als de waarde van gespecificeerd.

Laten we dit gedrag introduceren door een standaardwaarde te gebruiken waar voor het veld, en het instellen van zijn ariteit als een:

@Parameter (names = {"--itemized"}, arity = 1) private boolean itemized = true; 

Nu, wanneer we de optie specificeren, wordt de waarde ingesteld op false:

$ java App --itemized false Lees vlag gespecificeerd: false. 

7. Lijst Types

JCommander geeft een aantal manieren om argumenten aan te binden Lijst velden.

7.1. De parameter meerdere keren specificeren

Laten we aannemen dat we de kosten van slechts een subset van de abonnementen van een klant willen ophalen:

@Parameter (names = {"--subscription", "-S"}) private List subscriptionIds; 

Het veld is niet verplicht en de toepassing haalt de kosten voor alle abonnementen op als de parameter niet wordt opgegeven. We kunnen echter meerdere abonnementen specificeren door de parameternaam meerdere keren te gebruiken:

$ java App -S abonnementA001 -S abonnementA002 -S abonnementA003 Lees abonnementen: [abonnementA001, abonnementA002, abonnementA003]. 

7.2. Verbindend Lijsten Met behulp van de splitter

In plaats van de optie meerdere keren op te geven, proberen we de lijst te binden door een door komma's gescheiden door te geven Draad:

$ java App -S abonnementA001, abonnementA002, abonnementA003 Lees abonnementen: [abonnementA001, abonnementA002, abonnementA003]. 

Dit gebruikt een enkele parameterwaarde (arity = 1) om een ​​lijst weer te geven. JCommander zal de klas gebruiken CommaParameterSplitter om de door komma's gescheiden waarden te binden Draad naar onze Lijst.

7.3. Verbindend Lijsten Met behulp van een aangepaste splitter

We kunnen de standaardsplitter opheffen door het IParameterSplitter koppel:

class ColonParameterSplitter implementeert IParameterSplitter {@Override public List split (String value) {return asList (value.split (":")); }}

En vervolgens de implementatie toewijzen aan het splitser attribuut in @Parameter:

@Parameter (names = {"--subscription", "-S"}, splitter = ColonParameterSplitter.class) private List subscriptionIds; 

Laten we het uitproberen:

$ java App -S "abonnementA001: abonnementA002: abonnementA003" Lees abonnementen: [abonnementA001, abonnementA002, abonnementA003]. 

7.4. Variabele Arity Lijsten

Variabele ariteit stelt ons in staat om te declarerenlijsten die onbepaalde parameters kunnen aannemen, tot aan de volgende optie. We kunnen het attribuut instellen variabeleArity net zo waar om dit gedrag te specificeren.

Laten we dit proberen om abonnementen te ontleden:

@Parameter (names = {"--subscription", "-S"}, variableArity = true) privélijst subscriptionIds; 

En als we onze opdracht uitvoeren:

$ java App -S abonnementA001 abonnementA002 abonnementA003 --itemized Lees abonnementen: [abonnementA001, abonnementA002, abonnementA003]. 

JCommander bindt alle invoerargumenten volgend op de optie "-S" aan het lijstveld, tot de volgende optie of het einde van het commando.

7.5. Arity opgelost Lijsten

Tot nu toe hebben we onbegrensde lijsten gezien, waar we zoveel lijstitems kunnen doorgeven als we willen. Soms willen we het aantal items beperken dat wordt doorgegeven aan een Lijst veld. Om dit te doen, kunnen we specificeer een integer ariteitswaarde voor a Lijst veld-om het te laten begrenzen:

@Parameter (names = {"--subscription", "-S"}, arity = 2) private List subscriptionIds; 

Fixed arity dwingt een controle af van het aantal parameters dat wordt doorgegeven aan een Lijst optie en gooit een ParameterException bij overtreding:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003 De hoofdparameter 'subscriptionA003' is doorgegeven, maar er is geen hoofdparameter gedefinieerd in je arg-klasse 

De foutmelding suggereert dat aangezien JCommander slechts twee argumenten verwachtte, het probeerde de extra invoerparameter “subscriptionA003” als de volgende optie te ontleden.

8. Aangepaste typen

We kunnen ook parameters binden door aangepaste converters te schrijven. Net als ingebouwde converters moeten aangepaste converters het IStringConverter koppel.

Laten we een conversieprogramma schrijven voor het parseren van een ISO8601-tijdstempel:

klasse ISO8601TimestampConverter implementeert IStringConverter {privé statische laatste DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern ("uuuu-MM-dd'T'HH: mm: ss"); @Override public Instant convert (String-waarde) {probeer {return LocalDateTime .parse (waarde, TS_FORMATTER) .atOffset (ZoneOffset.UTC) .toInstant (); } catch (DateTimeParseException e) {throw new ParameterException ("Ongeldig tijdstempel"); }}} 

Deze code zal de invoer ontleden Draad en retourneer een Onmiddellijk, het gooien van een ParameterException als er een conversiefout is. We kunnen deze converter gebruiken door hem aan een veld van het type te binden Onmiddellijk de ... gebruiken omzetter attribuut in @Parameter:

@Parameter (names = {"--timestamp"}, converter = ISO8601TimestampConverter.class) privé Instant tijdstempel; 

Laten we het in actie zien:

$ java-app --timestamp 2019-10-03T10: 58: 00 Tijdstempel lezen: 2019-10-03T10: 58: 00Z.

9. Parameters valideren

JCommander biedt een aantal standaardvalidaties:

  • of vereiste parameters worden geleverd
  • als het aantal opgegeven parameters overeenkomt met de ariteit van een veld
  • of elk Draad parameter kan worden geconverteerd naar het overeenkomstige veldtype

Daarnaast, we willen misschien aangepaste validaties toevoegen. Laten we bijvoorbeeld aannemen dat de klant-ID's UUID's moeten zijn.

We kunnen een validator schrijven voor het klantveld die de interface implementeert IParameterValidator:

klasse UUIDValidator implementeert IParameterValidator {private static final String UUID_REGEX = "[0-9a-fA-F] {8} (- [0-9a-fA-F] {4}) {3} - [0-9a-fA- F] {12} "; @Override public void validate (String-naam, String-waarde) gooit ParameterException {if (! IsValidUUID (waarde)) {gooi nieuwe ParameterException ("String-parameter" + waarde + "is geen geldige UUID."); }} private boolean isValidUUID (String-waarde) {return Pattern.compile (UUID_REGEX) .matcher (waarde) .matches (); }} 

Dan kunnen we het aansluiten op de validateWith attribuut van de parameter:

@Parameter (names = {"--customer", "-C"}, validateWith = UUIDValidator.class) private String customerId; 

Als we de opdracht aanroepen met een niet-UUID-klant-ID, wordt de toepassing afgesloten met een validatiefoutbericht:

$ java App --C customer001 Tekenreeksparameter customer001 is geen geldige UUID. 

10. Subopdrachten

Nu we hebben geleerd over parameterbinding, laten we alles samenbrengen om onze opdrachten te bouwen.

In JCommander kunnen we meerdere commando's ondersteunen, subcommando's genaamd, elk met een aparte set opties.

10.1. @Parameters Annotatie

We kunnen gebruiken @Parameters om subcommando's te definiëren. @Parameters bevat het attribuut commandNames om een ​​commando te identificeren.

Laten we modelleren indienen en halen als subcommando's:

@Parameters (commandNames = {"submit"}, commandDescription = "Gebruik voor een bepaalde klant en abonnement indienen," + "accepteert één gebruiksitem") class SubmitUsageCommand {// ...} @Parameters (commandNames = {"fetch" }, commandDescription = "Ophaalkosten voor een klant in de huidige maand," + "kunnen gespecificeerd of geaggregeerd worden") class FetchCurrentChargesCommand {// ...} 

JCommander gebruikt de attributen in @Parameters om de subcommando's te configureren, zoals:

  • commandNames - naam van het subcommando; bindt de opdrachtregelargumenten aan de klasse die is geannoteerd met @Parameters
  • commando Beschrijving - documenteert het doel van het subcommando

10.2. Subopdrachten toevoegen aan JCommander

We voegen de subcommando's toe aan JCommander met de addCommand methode:

SubmitUsageCommand submitUsageCmd = nieuwe SubmitUsageCommand (); FetchCurrentChargesCommand fetchChargesCmd = nieuwe FetchCurrentChargesCommand (); JCommander jc = JCommander.newBuilder () .addCommand (submitUsageCmd) .addCommand (fetchChargesCmd) .build (); 

De addCommand methode registreert de subopdrachten met hun respectievelijke namen zoals gespecificeerd in de commandNames kenmerk van @Parameters annotatie.

10.3. Subopdrachten parseren

Om toegang te krijgen tot de keuze van de gebruiker voor het commando, moeten we eerst de argumenten ontleden:

jc.parse (args); 

Vervolgens kunnen we het subcommando extraheren met getParsedCommand:

String parsedCmdStr = jc.getParsedCommand (); 

Naast het identificeren van de opdracht, bindt JCommander de rest van de opdrachtregelparameters aan hun velden in de subopdracht. Nu hoeven we alleen maar het commando te bellen dat we willen gebruiken:

switch (parsedCmdStr) {case "submit": submitUsageCmd.submit (); breken; geval "fetch": fetchChargesCmd.fetch (); breken; standaard: System.err.println ("Ongeldige opdracht:" + parsedCmdStr); } 

11. JCommander-gebruikshulp

We kunnen een beroep doen gebruik om een ​​gebruiksgids weer te geven. Dit is een samenvatting van alle opties die onze applicatie gebruikt. In onze applicatie kunnen we gebruik aanroepen voor het hoofdcommando, of als alternatief voor elk van de twee commando's “submit” en “fetch” afzonderlijk.

Een gebruiksdisplay kan ons op een aantal manieren helpen: helpopties weergeven en tijdens foutafhandeling.

11.1. Help-opties weergeven

We kunnen een help-optie in onze opdrachten binden met behulp van een boolean parameter samen met het attribuut helpen ingesteld op waar:

@Parameter (names = "--help", help = true) privé booleaanse hulp; 

Vervolgens kunnen we detecteren of "–help" is doorgegeven in de argumenten, en bellen gebruik:

if (cmd.help) {jc.usage (); } 

Laten we de help-uitvoer bekijken voor ons subcommando "submit":

$ java App indienen --help Gebruik: indienen [opties] Opties: * --klant, -C Id van de klant die de services gebruikt * --abonnement, -S Id van het abonnement dat is gekocht * --hoeveelheid Gebruikte hoeveelheid ; gerapporteerde hoeveelheid wordt toegevoegd over de factureringsperiode * --pricing-type, -P Pricing type van het gerapporteerde gebruik (waarden: [PRE_RATED, UNRATED]) * --timestamp Tijdstempel van de gebruiksgebeurtenis, moet in de huidige factureringsperiode liggen - -prijs Indien PRE_RATED, eenheidsprijs toe te passen per eenheid gerapporteerde gebruikshoeveelheid 

De gebruik methode maakt gebruik van de @Parameter attributen zoals Omschrijving om een ​​handige samenvatting weer te geven. Parameters gemarkeerd met een asterisk (*) zijn verplicht.

11.2. Foutafhandeling

We kunnen de ParameterException en bel gebruik om de gebruiker te helpen begrijpen waarom zijn invoer onjuist was. ParameterException bevat de JCommander instantie om de hulp weer te geven:

probeer {jc.parse (args); } catch (ParameterException e) {System.err.println (e.getLocalizedMessage ()); jc.usage (); } 

12. Conclusie

In deze tutorial hebben we JCommander gebruikt om een ​​opdrachtregeltoepassing te bouwen. Hoewel we veel van de belangrijkste functies hebben besproken, staat er meer in de officiële documentatie.

Zoals gewoonlijk is de broncode voor alle voorbeelden beschikbaar op GitHub.