Spring-Boot で、
と
Commons CLI と、
参考
Designing a CL App with SpringBoot CommandLineRunner Interface · Mert Kara
java - How do I manually autowire a bean with Spring? - Stack Overflow
args4j/SubCommandHandler.java at b1b2d131c348c3da771046b209c0f39e1204357d · kohsuke/args4j
必要な ライブラリ
gradle
のdependencies
定義です。
dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter compile group: 'org.springframework.boot', name: 'spring-boot-starter', version: '1.5.3.RELEASE' // https://mvnrepository.com/artifact/args4j/args4j compile group: 'args4j', name: 'args4j', version: '2.33' compile group: 'com.google.api-client', name: 'google-api-client', version: '1.22.0' compile 'com.google.apis:google-api-services-webmasters:v3-rev22-1.22.0' }
作った もの
いきなりですが、
[1].Google Search Console API の
CliApplication.java
Applicationクラスです。
取得結果を、
OFFに
package xyz.monotalk.google.webmaster.cli; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class CliApplication { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(CliApplication.class); springApplication.setBannerMode(Banner.Mode.OFF); springApplication.run(args); } }
Command.java
args4j
のSubCommandHandler
で
package xyz.monotalk.google.webmaster.cli; /** * interface Command */ public interface Command { void execute(); }
WebmastersCommandRunner.java
CommandLineRunner
の
説明が
package xyz.monotalk.google.webmaster.cli; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.spi.SubCommand; import org.kohsuke.args4j.spi.SubCommandHandler; import org.kohsuke.args4j.spi.SubCommands; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsDeleteCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsGetCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsListCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsSubmitCommand; import java.util.ArrayList; import java.util.List; /** * WebmastersCommandRunner */ @Component public class WebmastersCommandRunner implements CommandLineRunner { @Autowired private ApplicationContext context; /** * 引数によって実行するオブジェクトを切り替える */ @Argument(handler = SubCommandHandler.class) @SubCommands( { @SubCommand(name = "sitemaps.list", impl = SiteMapsListCommand.class), @SubCommand(name = "sitemaps.delete", impl = SiteMapsDeleteCommand.class), @SubCommand(name = "sitemaps.get", impl = SiteMapsGetCommand.class), @SubCommand(name = "sitemaps.submit", impl = SiteMapsSubmitCommand.class) }) private Command command; @Override public void run(String... args) throws Exception { // ------------------------------------------------ // "--application" を含むコマンドライン引数を除外 // ---------------- List<String> cmdArgs = new ArrayList<>(); for (String arg : args) { if (!arg.contains("--application")) { cmdArgs.add(arg); } } new CmdLineParser(this).parseArgument(cmdArgs); AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory(); autowireCapableBeanFactory.autowireBean(this.command); this.command.execute(); } }
説明
Application クラスと
同一パッケージに 作成
@Component
アノテーションを付与しているので、 Application クラスの 同一パッケージ以下に 存在すれば、
Application クラス 起動後にrun
メソッドを実行してくれます。 @Argument
アノテーションと、@SubCommands
アノテーションを使用して、 SubCommand を 定義
指定される引数に 対応する、 Command
の実装クラスを アノテーションで 定義しています。 run メソッドの
引数 args から、 --application
を含む オプション引数を 除外
後述するJava クラスで、 application.keyFileLocation
を使っています。
args4j
は、調べた 限りだと、 知らない 引数が あると、 エラーを 吐くので、 既知の args4j
が知らない 引数を、
除外しています。インスタンス化した、
command クラスに inject する で、AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory(); autowireCapableBeanFactory.autowireBean(this.command);
インスタンス化した commandクラスに 手動で inject できます。
WebmastersFactory.java
AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory(); autowireCapableBeanFactory.autowireBean(this.command);
inject 時に、
keyFileLocation
に、--application.keyFileLocation=@@@@
の@@@@
が@@@@
には、package xyz.monotalk.google.webmaster.cli; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.webmasters.Webmasters; import com.google.api.services.webmasters.WebmastersScopes; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.FileInputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Collections; /** * WebmastersFactory */ @Component public class WebmastersFactory { @Value("${application.keyFileLocation}") private String keyFileLocation; /** * Create Webmasters instance. * * @return */ public Webmasters create() { HttpTransport httpTransport; try { httpTransport = GoogleNetHttpTransport.newTrustedTransport(); } catch (GeneralSecurityException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IllegalStateException(e); } JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); GoogleCredential credential; try { credential = GoogleCredential.fromStream(new FileInputStream(keyFileLocation)).createScoped(Collections.singleton(WebmastersScopes.WEBMASTERS)); } catch (IOException e) { throw new IllegalStateException(e); } // Create a new authorized API client Webmasters service = new Webmasters.Builder(httpTransport, jsonFactory, credential).setApplicationName("Search Console Cli").build(); return service; } }
SiteMapsListCommand.java
Command
の
@Autowired
指定したWebmastersFactory
が、WebmastersCommandRunner
内でexecute
メソッドで、WebmastersFactory#create()
が
サブコマンド内で、args4j
の
package xyz.monotalk.google.webmaster.cli.subcommands.sitemaps; import com.google.api.services.webmasters.Webmasters; import com.google.api.services.webmasters.model.SitemapsListResponse; import org.kohsuke.args4j.Option; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import xyz.monotalk.google.webmaster.cli.Command; import xyz.monotalk.google.webmaster.cli.WebmastersFactory; import java.io.IOException; /** * SiteMapsListCommand */ @Component public class SiteMapsListCommand implements Command { @Autowired private WebmastersFactory factory; @Option(name = "-siteUrl", usage = "Url of site", required = true) private String siteUrl = null; @Override public void execute() { Webmasters webmasters = factory.create(); Webmasters.Sitemaps.List siteMaps; try { siteMaps = webmasters.sitemaps().list(siteUrl); } catch (IOException e) { throw new IllegalStateException(e); } SitemapsListResponse response; try { response = siteMaps.execute(); } catch (IOException e) { throw new IllegalStateException(e); } try { System.out.println(response.toPrettyString()); } catch (IOException e) { throw new IllegalStateException(e); } } }
Args4j の Usage を 出力する
SubCommand にrequired = true, metaVar = "<command name>", usage = "command name"
を
WebmastersCommandRunner.java
package xyz.monotalk.google.webmaster.cli; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.spi.SubCommand; import org.kohsuke.args4j.spi.SubCommandHandler; import org.kohsuke.args4j.spi.SubCommands; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import xyz.monotalk.google.webmaster.cli.subcommands.SearchAnalyticsCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsDeleteCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsGetCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsListCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsSubmitCommand; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.StringJoiner; /** * WebmastersCommandRunner */ @Component public class WebmastersCommandRunner implements CommandLineRunner { @Autowired private ApplicationContext context; /** * 引数によって実行するオブジェクトを切り替える */ @Argument(handler = SubCommandHandler.class, required = true, metaVar = "<command name>", usage = "command name") @SubCommands({@SubCommand(name = "webmasters.sitemaps.list", impl = SiteMapsListCommand.class), @SubCommand( name = "webmasters.sitemaps.delete", impl = SiteMapsDeleteCommand.class), @SubCommand( name = "webmasters.sitemaps.get", impl = SiteMapsGetCommand.class), @SubCommand( name = "webmasters.sitemaps.submit", impl = SiteMapsSubmitCommand.class), @SubCommand( name = "webmasters.searchanalytics.query", impl = SearchAnalyticsCommand.class),}) private Command command; @Override public void run(String... args) throws Exception { // ------------------------------------------------ // "--application" を含むコマンドライン引数を除外 // ---------------- List<String> cmdArgs = new ArrayList<>(); for (String arg : args) { if (!arg.contains("--application")) { cmdArgs.add(arg); } } CmdLineParser parser = new CmdLineParser(this); try { parser.parseArgument(cmdArgs); AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory(); autowireCapableBeanFactory.autowireBean(this.command); this.command.execute(); } catch (CmdLineException e) { System.err.println(e.getMessage()); parser.printUsage(System.err); return; } } }
説明
CmdLineParser
をtry - catch
して、CmdLineException
が出力された 場合、 CmdLineParser#printUsage()
でusage を 出力します。
java -jar xyz.monotalk.google.webmaster.cli-0.0.1.jar --application.keyFileLocation=@@@@@
のように、subcommand
名 指定せずにコマンドを 実行すると、 以下のような usage
が出力されます。 OUTPUT
Argument "<command name>" is required
の部分が、 System.err.println(e.getMessage());
で出力されており、 それ以外の 部分は、 parser.printUsage(System.err);
で出力されています。
Argument "<command name>" is required [webmasters.sitemaps.list | : command name webmasters.sitemaps.delete | webmasters.sitemaps.get | webmasters.sitemaps.submit | webmasters.searchanalytics.query]
Flask風 の Usage を 出力する
apache/incubator-superset: Apache Superset (incubating) is a modern, enterprise-ready business intelligence web application の
usage()
メソッドで、SubCommands
アノテーションの
* WebmastersCommandRunner.java
package xyz.monotalk.google.webmaster.cli; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.text.StrBuilder; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.spi.SubCommand; import org.kohsuke.args4j.spi.SubCommandHandler; import org.kohsuke.args4j.spi.SubCommands; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import xyz.monotalk.google.webmaster.cli.subcommands.SearchAnalyticsCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsDeleteCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsGetCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsListCommand; import xyz.monotalk.google.webmaster.cli.subcommands.sitemaps.SiteMapsSubmitCommand; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.StringJoiner; import static java.lang.System.out; /** * WebmastersCommandRunner */ @Component public class WebmastersCommandRunner implements CommandLineRunner { @Autowired private ApplicationContext context; /** * 引数によって実行するオブジェクトを切り替える */ @Argument(handler = SubCommandHandler.class) @SubCommands({@SubCommand(name = "webmasters.sitemaps.list", impl = SiteMapsListCommand.class), @SubCommand( name = "webmasters.sitemaps.delete", impl = SiteMapsDeleteCommand.class), @SubCommand( name = "webmasters.sitemaps.get", impl = SiteMapsGetCommand.class), @SubCommand( name = "webmasters.sitemaps.submit", impl = SiteMapsSubmitCommand.class), @SubCommand( name = "webmasters.searchanalytics.query", impl = SearchAnalyticsCommand.class),}) private Command command; @Option(name = "-?", aliases = "--help", usage = "show this help message and exit") private boolean usageFlag; @Override public void run(String... args) throws Exception { // ------------------------------------------------ // "--application" を含むコマンドライン引数を除外 // ---------------- List<String> cmdArgs = new ArrayList<>(); for (String arg : args) { if (!arg.contains("--application")) { cmdArgs.add(arg); } } if (cmdArgs.isEmpty()) { CmdLineParser parser = new CmdLineParser(this); parser.parseArgument(cmdArgs); out.println("--------------------------------------------------------------------------"); out.println(usage()); parser.printUsage(out); out.println("------------"); return; } CmdLineParser parser = new CmdLineParser(this); try { parser.parseArgument(cmdArgs); // Output help if (usageFlag) { out.println("--------------------------------------------------------------------------"); out.println(usage()); parser.printUsage(out); out.println("---------------------------"); return; } AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory(); autowireCapableBeanFactory.autowireBean(this.command); this.command.execute(); } catch (CmdLineException e) { out.println("error occurred: " + e.getMessage()); out.println("--------------------------------------------------------------------------"); out.println(usage()); parser.printUsage(out); out.println("------------"); return; } } /** * usage */ private String usage() { StrBuilder sb = new StrBuilder(); sb.appendln("usage: xyz.monotalk.google.webmaster.cli.CliApplication"); sb.appendNewLine(); Field field; try { field = this.getClass().getDeclaredField("command"); } catch (NoSuchFieldException e) { throw new IllegalStateException(e); } SubCommands subCommands = field.getAnnotation(SubCommands.class); StringJoiner joiner = new StringJoiner(",", "{", "}"); Arrays.stream(subCommands.value()).forEach(e -> joiner.add(e.name())); sb.appendln(" " + joiner.toString()); sb.appendln(" " + "..."); sb.appendNewLine(); sb.appendln("positional arguments:"); sb.appendNewLine(); sb.appendln(" " + joiner.toString()); Arrays.stream(subCommands.value()).map(e -> { try { return Pair.of(e.name(), (Command) e.impl().newInstance()); } catch (InstantiationException | IllegalAccessException ex) { throw new IllegalStateException(ex); } }).forEach(e -> { sb.appendln(" " + e.getLeft() + " " + e.getRight().usage()); }); sb.appendNewLine(); sb.append("optional arguments:"); sb.appendNewLine(); return sb.toString(); } }
Command interface に
method を 追加
Commannd.java
に、説明文取得用に メソッドを 追加し、 各Command クラスから、 説明文を 返すようにしました。 package xyz.monotalk.google.webmaster.cli; /** * Command interface */ public interface Command { /** * Main Method */ void execute(); /** * Usage * @return */ String usage(); }
superset の
usage
superset のusage は 以下のように 出力されます。 % superset -? usage: superset [-?] {flower,version,worker,db,runserver,refresh_druid,init,load_examples,shell,update_datasources_cache} ... positional arguments: {flower,version,worker,db,runserver,refresh_druid,init,load_examples,shell,update_datasources_cache} flower Runs a Celery Flower web server Celery Flower is a UI to monitor the Celery operation on a given broker load_examples Loads a set of Slices and Dashboards and a supporting dataset worker Starts a Superset worker for async SQL query execution. db Perform database migrations runserver Starts a Superset web server. refresh_druid Refresh druid datasources init Inits the Superset application version Prints the current version number shell Runs a Python shell inside Flask application context. update_datasources_cache Refresh sqllab datasources cache optional arguments: -?, --help show this help message and exit
作成した
CommandLineRunner の usage
以下のような出力に なります。 なかなか% java -jar xyz.monotalk.google.webmaster.cli-0.0.1.jar --application.keyFileLocation=@@@ -? -------------------------------------------------------------------------- usage: xyz.monotalk.google.webmaster.cli.CliApplication {webmasters.sitemaps.list,webmasters.sitemaps.delete,webmasters.sitemaps.get,webmasters.sitemaps.submit,webmasters.searchanalytics.query} ... positional arguments: {webmasters.sitemaps.list,webmasters.sitemaps.delete,webmasters.sitemaps.get,webmasters.sitemaps.submit,webmasters.searchanalytics.query} webmasters.sitemaps.list webmasters.sitemaps.list's description webmasters.sitemaps.delete webmasters.sitemaps.delete's description webmasters.sitemaps.get webmasters.sitemaps.get's descriptioin webmasters.sitemaps.submit Submits a sitemap for a site. webmasters.searchanalytics.query webmasters.searchanalytics.query's descriptioin optional arguments: -? (--help) : show this help message and exit (default: true)
無理矢理な 感じは あります。
SubCommand の実装は これで 終わりです。
以上です。
コメント