ステロイドのCLI:Google GuiceとJCommander

この記事では、JavaでCLIアプリケーションを構築する方法の1つについて説明します。
実際、そのようなアプリケーションの必要性は消えていません。たとえば、私の場合、それはサーバー側の機能テストおよび負荷テスト用のアプリケーションでした。 もちろん、一連のJUnitを使用して必要なテストを実行するオプションがありましたが、時間は非常に限られており、テスト部門からのプログラミングを必要としないソリューションを求めていました。 さらに、クライアントとサーバーが対話するバイナリプロトコルが明確に指定されました。

アイデア


今回は、入力アプリケーションの構文解析、コマンドと引数の強調表示、引数の検証、コマンドの実行、プロンプトの表示など、CLIアプリケーションにとって些細なものを作成する際に、車輪を再発明したくありませんでした。
既製のコンポーネントを探すことにしました。

少し前のHabréにはcommons-cliに関する記事がありました。 commons-cli自体は「木製」のAPIのために好きではありませんでしたが、記事自体のコメントからJCommanderを含むいくつかの選択肢について学びました

彼が注目を集めたのは、次の理由からです。



サーバー部分はGoogle Guiceフレームワークを使用して構築されているため、特にサーバーとクライアントには共通の依存関係とコンポーネントがあるため、CLIクライアントを構築することをお勧めします。

それの由来


クラス図:



図では:
-CLISupport-ユーザーのコンソール入力を監視し、その解析をJCommanderに委任します。
-CLIApplication-特定の順序でアプリケーションコンポーネントを起動および停止します。
- コマンド -抽象クラスコマンド、他のすべてのチームのベース。 主な方法は実行です。
-CommandXXX-たとえばコマンドの実装。
-JCommanderProvider -com.google.inject.Providerインターフェースの実装。 外部リクエスト(インジェクション)でJCommanderのインスタンスを作成します。
-CLIConfigurationModule -Guiceのコンポーネントコンフィギュレーター 、com.google.inject.AbstractModuleの実装。

コード例


CLIConfigurationModuleはGuice構成モジュールであり、実行に使用できるすべての依存関係とコマンドのみを記述します。

public class CLIConfigurationModule extends AbstractModule {

protected void configure() {
AnsiConsole.systemInstall();

bind(PrimaryBusinessLogicService. class ).to(PrimaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(SecondaryBusinessLogicService. class ).to(SecondaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(CLISupport. class ).asEagerSingleton();

bind(CLIApplication. class ).asEagerSingleton();

bind(JCommander. class ).toProvider(JCommanderProvider. class );

bind(CommandClearScreen. class );
bind(CommandExit. class );
bind(CommandUsage. class );
bind(CommandPrimary. class );
bind(CommandSecondary. class );
}

@Provides
@Inject
public Collection<Command> provideAvailableCommands(Injector injector) {
Collection<Command> commands = new ArrayList <Command>();
commands.add(injector.getInstance(CommandClearScreen. class ));
commands.add(injector.getInstance(CommandExit. class ));
commands.add(injector.getInstance(CommandUsage. class ));
commands.add(injector.getInstance(CommandPrimary. class ));
commands.add(injector.getInstance(CommandSecondary. class ));
return commands;
}
}

* This source code was highlighted with Source Code Highlighter .
public class CLIConfigurationModule extends AbstractModule {

protected void configure() {
AnsiConsole.systemInstall();

bind(PrimaryBusinessLogicService. class ).to(PrimaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(SecondaryBusinessLogicService. class ).to(SecondaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(CLISupport. class ).asEagerSingleton();

bind(CLIApplication. class ).asEagerSingleton();

bind(JCommander. class ).toProvider(JCommanderProvider. class );

bind(CommandClearScreen. class );
bind(CommandExit. class );
bind(CommandUsage. class );
bind(CommandPrimary. class );
bind(CommandSecondary. class );
}

@Provides
@Inject
public Collection<Command> provideAvailableCommands(Injector injector) {
Collection<Command> commands = new ArrayList <Command>();
commands.add(injector.getInstance(CommandClearScreen. class ));
commands.add(injector.getInstance(CommandExit. class ));
commands.add(injector.getInstance(CommandUsage. class ));
commands.add(injector.getInstance(CommandPrimary. class ));
commands.add(injector.getInstance(CommandSecondary. class ));
return commands;
}
}

* This source code was highlighted with Source Code Highlighter .
public class CLIConfigurationModule extends AbstractModule {

protected void configure() {
AnsiConsole.systemInstall();

bind(PrimaryBusinessLogicService. class ).to(PrimaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(SecondaryBusinessLogicService. class ).to(SecondaryBusinessLogicServiceImpl. class ).asEagerSingleton();
bind(CLISupport. class ).asEagerSingleton();

bind(CLIApplication. class ).asEagerSingleton();

bind(JCommander. class ).toProvider(JCommanderProvider. class );

bind(CommandClearScreen. class );
bind(CommandExit. class );
bind(CommandUsage. class );
bind(CommandPrimary. class );
bind(CommandSecondary. class );
}

@Provides
@Inject
public Collection<Command> provideAvailableCommands(Injector injector) {
Collection<Command> commands = new ArrayList <Command>();
commands.add(injector.getInstance(CommandClearScreen. class ));
commands.add(injector.getInstance(CommandExit. class ));
commands.add(injector.getInstance(CommandUsage. class ));
commands.add(injector.getInstance(CommandPrimary. class ));
commands.add(injector.getInstance(CommandSecondary. class ));
return commands;
}
}

* This source code was highlighted with Source Code Highlighter .



JCommanderProvider-インジェクト /取得時にJCommanderインスタンスを作成します。 CLIConfigurationModuleクラスのProvidesアノテーションによって宣言されたコマンドのコレクションを取得します。 主な機能は、次のコマンドを解析する前に、JCommanderの新しい初期化されたインスタンスを作成する必要があることです。 解析後、状態を保存し、その後の解析中に破損する可能性があります。 それが、JCommanderをSingleton / asEagerSingletonとして宣言できない理由です。

public class JCommanderProvider implements Provider<JCommander> {

@Inject
private Collection<Command> commands;

/**
* Constructs the new JCommander instance with all commands.
*
* @return
*/
public JCommander get () {
JCommander commander = new JCommander();
for (Command command : commands) {
addCommand(commander, command);
}
return commander;
}

private void addCommand(JCommander commander, Command command) {
commander.addCommand(command.getCommandName(), command, command.getAliases());
}
}

* This source code was highlighted with Source Code Highlighter .
public class JCommanderProvider implements Provider<JCommander> {

@Inject
private Collection<Command> commands;

/**
* Constructs the new JCommander instance with all commands.
*
* @return
*/
public JCommander get () {
JCommander commander = new JCommander();
for (Command command : commands) {
addCommand(commander, command);
}
return commander;
}

private void addCommand(JCommander commander, Command command) {
commander.addCommand(command.getCommandName(), command, command.getAliases());
}
}

* This source code was highlighted with Source Code Highlighter .


コマンドは、すべてのコマンドの基本クラスです。 主な方法は実行です。 コマンドの名前はjavax.inject.Namedアノテーションで示されます-Guiceを使用するとjavax.injectへの依存性が現れるため、これはかなりエレガントなソリューションであることがわかりました。論理的には、このアノテーションは非常に意味があります。 メイン名に加えて、エイリアス(エイリアス)の配列も定義できます。たとえば、コマンド「exit」にはエイリアス「q」と「x」を含めることができます。 さらに、JCommanderでコマンドを登録するときにgetCommandNameメソッドとgetAliasesメソッドが使用されます(JCommanderProviderのaddCommandメソッドを参照)。

public abstract class Command {

private static final String [] NO_ALIASES = new String []{};

protected Logger logger;
private String commandName;

protected Command() {
logger = LoggerFactory.getLogger(getClass());
commandName = getClass().getAnnotation(Named. class ). value ();
}

public String [] getAliases() {
return NO_ALIASES;
}

public final String getCommandName() {
return commandName;
}

public abstract void execute() throws ExecutionException;
}

* This source code was highlighted with Source Code Highlighter .
public abstract class Command {

private static final String [] NO_ALIASES = new String []{};

protected Logger logger;
private String commandName;

protected Command() {
logger = LoggerFactory.getLogger(getClass());
commandName = getClass().getAnnotation(Named. class ). value ();
}

public String [] getAliases() {
return NO_ALIASES;
}

public final String getCommandName() {
return commandName;
}

public abstract void execute() throws ExecutionException;
}

* This source code was highlighted with Source Code Highlighter .
public abstract class Command {

private static final String [] NO_ALIASES = new String []{};

protected Logger logger;
private String commandName;

protected Command() {
logger = LoggerFactory.getLogger(getClass());
commandName = getClass().getAnnotation(Named. class ). value ();
}

public String [] getAliases() {
return NO_ALIASES;
}

public final String getCommandName() {
return commandName;
}

public abstract void execute() throws ExecutionException;
}

* This source code was highlighted with Source Code Highlighter .



CommandPrimaryは、ビジネスロジックサービスコールを備えた実際のチームの一例です。 @Parametersアノテーションに示されているものは、このコマンドの説明を生成するときに使用されます(CommandUsageコマンドを参照)。

@Parameters(commandDescription = "Execute the logic of primary service" )
@Named( "do-primary" )
public class CommandPrimary extends Command {

@Parameter(names = { "-verbose" , "-v" }, description = "Verbose mode" )
protected boolean verbose;

@Parameter(names = { "-id" }, description = "Entity ID" , required = true )
protected String id;

@Parameter(names = { "-count" , "-c" }, validateWith = PositiveInteger. class , description = "Entities count" , required = true )
protected long count;

private PrimaryBusinessLogicService primaryBusinessLogicService;

@Inject
public CommandPrimary(PrimaryBusinessLogicService primaryBusinessLogicService) {
this .primaryBusinessLogicService = primaryBusinessLogicService;
}

@Override
public String [] getAliases() {
return new String []{ "dp" , "primary" };
}

@Override
public void execute() throws ExecutionException {
try {
if (verbose) {
logger.info( String .format( "Executing primary business logic with parameters: [count=%d, id=%s]" , count, id));
}

primaryBusinessLogicService.executePrimaryBusinessLogic(count, id);
} catch (ServiceException e) {
throw new ExecutionException(e);
}
}
}

* This source code was highlighted with Source Code Highlighter .
@Parameters(commandDescription = "Execute the logic of primary service" )
@Named( "do-primary" )
public class CommandPrimary extends Command {

@Parameter(names = { "-verbose" , "-v" }, description = "Verbose mode" )
protected boolean verbose;

@Parameter(names = { "-id" }, description = "Entity ID" , required = true )
protected String id;

@Parameter(names = { "-count" , "-c" }, validateWith = PositiveInteger. class , description = "Entities count" , required = true )
protected long count;

private PrimaryBusinessLogicService primaryBusinessLogicService;

@Inject
public CommandPrimary(PrimaryBusinessLogicService primaryBusinessLogicService) {
this .primaryBusinessLogicService = primaryBusinessLogicService;
}

@Override
public String [] getAliases() {
return new String []{ "dp" , "primary" };
}

@Override
public void execute() throws ExecutionException {
try {
if (verbose) {
logger.info( String .format( "Executing primary business logic with parameters: [count=%d, id=%s]" , count, id));
}

primaryBusinessLogicService.executePrimaryBusinessLogic(count, id);
} catch (ServiceException e) {
throw new ExecutionException(e);
}
}
}

* This source code was highlighted with Source Code Highlighter .
@Parameters(commandDescription = "Execute the logic of primary service" )
@Named( "do-primary" )
public class CommandPrimary extends Command {

@Parameter(names = { "-verbose" , "-v" }, description = "Verbose mode" )
protected boolean verbose;

@Parameter(names = { "-id" }, description = "Entity ID" , required = true )
protected String id;

@Parameter(names = { "-count" , "-c" }, validateWith = PositiveInteger. class , description = "Entities count" , required = true )
protected long count;

private PrimaryBusinessLogicService primaryBusinessLogicService;

@Inject
public CommandPrimary(PrimaryBusinessLogicService primaryBusinessLogicService) {
this .primaryBusinessLogicService = primaryBusinessLogicService;
}

@Override
public String [] getAliases() {
return new String []{ "dp" , "primary" };
}

@Override
public void execute() throws ExecutionException {
try {
if (verbose) {
logger.info( String .format( "Executing primary business logic with parameters: [count=%d, id=%s]" , count, id));
}

primaryBusinessLogicService.executePrimaryBusinessLogic(count, id);
} catch (ServiceException e) {
throw new ExecutionException(e);
}
}
}

* This source code was highlighted with Source Code Highlighter .



プロジェクトの依存関係
< properties >
< version.jcommander > 1.18 </ version.jcommander >
< version.jansi > 1.6 </ version.jansi >
< version.commons-io > 2.0.1 </ version.commons-io >
< version.jline > 0.9.94 </ version.jline >
< version.guice > 3.0 </ version.guice >
< version.logback > 0.9.29 </ version.logback >
< version.slf4j > 1.6.2 </ version.slf4j >
< version.commons-lang > 3.0.1 </ version.commons-lang >

< version.maven-compiler-plugin > 2.3.2 </ version.maven-compiler-plugin >
< version.maven-jar-plugin > 2.3.2 </ version.maven-jar-plugin >
< version.maven-surefire-plugin > 2.9 </ version.maven-surefire-plugin >
< version.onejar-maven-plugin > 1.4.4 </ version.onejar-maven-plugin >
< version.maven-assembly-plugin > 2.2.1 </ version.maven-assembly-plugin >
</ properties >

< dependencies >

<!-- Logging -->

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-classic </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-core </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > org.slf4j </ groupId >
< artifactId > slf4j-api </ artifactId >
< version > ${version.slf4j} </ version >
</ dependency >

<!-- Google Stuff -->

< dependency >
< groupId > com.google.inject </ groupId >
< artifactId > guice </ artifactId >
< version > ${version.guice} </ version >
</ dependency >

<!--External Stuff-->

< dependency >
< groupId > commons-io </ groupId >
< artifactId > commons-io </ artifactId >
< version > ${version.commons-io} </ version >
</ dependency >

< dependency >
< groupId > org.fusesource.jansi </ groupId >
< artifactId > jansi </ artifactId >
< version > ${version.jansi} </ version >
</ dependency >

< dependency >
< groupId > com.beust </ groupId >
< artifactId > jcommander </ artifactId >
< version > ${version.jcommander} </ version >
</ dependency >

< dependency >
< groupId > jline </ groupId >
< artifactId > jline </ artifactId >
< version > ${version.jline} </ version >
</ dependency >

< dependency >
< groupId > org.apache.commons </ groupId >
< artifactId > commons-lang3 </ artifactId >
< version > ${version.commons-lang} </ version >
</ dependency >
</ dependencies >

* This source code was highlighted with Source Code Highlighter .
< properties >
< version.jcommander > 1.18 </ version.jcommander >
< version.jansi > 1.6 </ version.jansi >
< version.commons-io > 2.0.1 </ version.commons-io >
< version.jline > 0.9.94 </ version.jline >
< version.guice > 3.0 </ version.guice >
< version.logback > 0.9.29 </ version.logback >
< version.slf4j > 1.6.2 </ version.slf4j >
< version.commons-lang > 3.0.1 </ version.commons-lang >

< version.maven-compiler-plugin > 2.3.2 </ version.maven-compiler-plugin >
< version.maven-jar-plugin > 2.3.2 </ version.maven-jar-plugin >
< version.maven-surefire-plugin > 2.9 </ version.maven-surefire-plugin >
< version.onejar-maven-plugin > 1.4.4 </ version.onejar-maven-plugin >
< version.maven-assembly-plugin > 2.2.1 </ version.maven-assembly-plugin >
</ properties >

< dependencies >

<!-- Logging -->

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-classic </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-core </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > org.slf4j </ groupId >
< artifactId > slf4j-api </ artifactId >
< version > ${version.slf4j} </ version >
</ dependency >

<!-- Google Stuff -->

< dependency >
< groupId > com.google.inject </ groupId >
< artifactId > guice </ artifactId >
< version > ${version.guice} </ version >
</ dependency >

<!--External Stuff-->

< dependency >
< groupId > commons-io </ groupId >
< artifactId > commons-io </ artifactId >
< version > ${version.commons-io} </ version >
</ dependency >

< dependency >
< groupId > org.fusesource.jansi </ groupId >
< artifactId > jansi </ artifactId >
< version > ${version.jansi} </ version >
</ dependency >

< dependency >
< groupId > com.beust </ groupId >
< artifactId > jcommander </ artifactId >
< version > ${version.jcommander} </ version >
</ dependency >

< dependency >
< groupId > jline </ groupId >
< artifactId > jline </ artifactId >
< version > ${version.jline} </ version >
</ dependency >

< dependency >
< groupId > org.apache.commons </ groupId >
< artifactId > commons-lang3 </ artifactId >
< version > ${version.commons-lang} </ version >
</ dependency >
</ dependencies >

* This source code was highlighted with Source Code Highlighter .
< properties >
< version.jcommander > 1.18 </ version.jcommander >
< version.jansi > 1.6 </ version.jansi >
< version.commons-io > 2.0.1 </ version.commons-io >
< version.jline > 0.9.94 </ version.jline >
< version.guice > 3.0 </ version.guice >
< version.logback > 0.9.29 </ version.logback >
< version.slf4j > 1.6.2 </ version.slf4j >
< version.commons-lang > 3.0.1 </ version.commons-lang >

< version.maven-compiler-plugin > 2.3.2 </ version.maven-compiler-plugin >
< version.maven-jar-plugin > 2.3.2 </ version.maven-jar-plugin >
< version.maven-surefire-plugin > 2.9 </ version.maven-surefire-plugin >
< version.onejar-maven-plugin > 1.4.4 </ version.onejar-maven-plugin >
< version.maven-assembly-plugin > 2.2.1 </ version.maven-assembly-plugin >
</ properties >

< dependencies >

<!-- Logging -->

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-classic </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > ch.qos.logback </ groupId >
< artifactId > logback-core </ artifactId >
< version > ${version.logback} </ version >
</ dependency >

< dependency >
< groupId > org.slf4j </ groupId >
< artifactId > slf4j-api </ artifactId >
< version > ${version.slf4j} </ version >
</ dependency >

<!-- Google Stuff -->

< dependency >
< groupId > com.google.inject </ groupId >
< artifactId > guice </ artifactId >
< version > ${version.guice} </ version >
</ dependency >

<!--External Stuff-->

< dependency >
< groupId > commons-io </ groupId >
< artifactId > commons-io </ artifactId >
< version > ${version.commons-io} </ version >
</ dependency >

< dependency >
< groupId > org.fusesource.jansi </ groupId >
< artifactId > jansi </ artifactId >
< version > ${version.jansi} </ version >
</ dependency >

< dependency >
< groupId > com.beust </ groupId >
< artifactId > jcommander </ artifactId >
< version > ${version.jcommander} </ version >
</ dependency >

< dependency >
< groupId > jline </ groupId >
< artifactId > jline </ artifactId >
< version > ${version.jline} </ version >
</ dependency >

< dependency >
< groupId > org.apache.commons </ groupId >
< artifactId > commons-lang3 </ artifactId >
< version > ${version.commons-lang} </ version >
</ dependency >
</ dependencies >

* This source code was highlighted with Source Code Highlighter .



ライブラリーの説明


jansi図書館サイトです。 古い学校の擬似グラフィック愛好家には、コンソールへの出力を多様化し、テスターの仕事に少しの喜びを加えたいという要望がありました。 何かが行われました-色の結論と別れのフレーズ「さようなら!」 白で終了するとき。 プロジェクトの最後に現れたわずかな空き時間からのみ。
logback図書館サイトです。 ロギングのこの特定の実装を使用する必要はありませんが、ログバックの主な肯定的な品質-高性能、オンザフライでの再読み取り設定、JMX経由の構成、パラメーター化とinclude-sのサポートなどに注目する価値があります。 一般に、Logbackは別の記事に値します。
jline図書館サイトです。 Up / Downキー(以下を参照)を使用して、以前に入力されたコマンドを介したナビゲーションの操作性の問題を解決し、コマンド完了の機能(たとえば、「do-p」を入力してTabキーを押してみます)。 理想的には、コンテキストだけでなく、コマンドだけでなくその引数の自動補完を実装できます。

問題点


Linuxでは、ユーザーは上/下矢印が正しく機能しなかったことに注意しました-以前に機能したコマンドのリストをナビゲートする代わりに、理解できない擬似シーケンスが表示されました。 この問題により、 jlineライブラリが使用されることになりました
そうでなければ、すべてが明確かつ調和して機能します。

ソースコード


この記事のすべてのソースコードは、 ここから入手できます
アプリケーションをビルドするには、Mavenバージョン2.xをインストールする必要があります。その後、「mvnパッケージ」をインストールします。
アセンブリの結果として、jcommander-guice-sample-XXX-client.tar.gzアーカイブが作成されます。 解凍して、OSに対応するシェルスクリプトrun.shまたはrun.batを実行する必要があります。

結果は次のようになります。


この記事が誰かに役立つことを願っています。
ご清聴ありがとうございました!

Source: https://habr.com/ru/post/J128781/


All Articles