JCommander
人気のあるアノテーションベースのJava CLIライブラリ。使いやすく、よくドキュメント化されています。
フレームワーク
JCommander
概要
JCommanderは、Javaのコマンドライン引数を解析するためのアノテーションベースのライブラリです。TestNGの作者であるCédric Beust氏によって開発され、アノテーションを使った宣言的で直感的なAPI設計が特徴です。Javaフィールドに@Parameter
アノテーションを付与するだけで、コマンドライン引数の解析、型変換、バリデーション、ヘルプ生成を自動的に行うことができます。
なぜJCommanderが選ばれるのか:
- アノテーションベース: 宣言的で直感的なAPI設計
- 豊富な機能: 型変換、バリデーション、サブコマンド、ヘルプ生成
- 使いやすさ: 最小限のコードで高機能なCLIを実現
- 安定性: TestNG作者による長期間のメンテナンス
- 国際化サポート: 多言語でのヘルプメッセージ対応
詳細
歴史と発展
JCommanderは2010年にCédric Beust氏によって開発が開始されました。TestNGプロジェクトでのコマンドライン解析のニーズから生まれ、アノテーションベースの設計思想により、Java開発者にとって自然で使いやすいライブラリとして成長しました。現在でも積極的にメンテナンスされており、Java8以降の新機能にも対応しています。
エコシステムでの位置づけ
Java CLIライブラリのエコシステムにおいて重要な位置を占めています:
- TestNGエコシステム: TestNGの内部でコマンドライン解析に使用
- エンタープライズツール: 企業内ツールでの採用実績が豊富
- 教育分野: Javaにおけるアノテーションベース設計の学習教材
- Legacy支援: Java8以前のプロジェクトでの安定した選択肢
最新動向(2024-2025年)
- Java17+対応: 最新のJavaバージョンでの動作保証
- セキュリティ強化: 依存関係の脆弱性対策とセキュリティアップデート
- Maven Central: 継続的なリリースとメンテナンス
- 互換性維持: 既存APIの下位互換性を保ちながらの機能追加
- パフォーマンス最適化: 引数解析の高速化とメモリ使用量の削減
主な特徴
コアアノテーション
- @Parameter: コマンドライン引数をJavaフィールドにバインド
- @Parameters: サブコマンドの定義
- @DynamicParameter: キー・バリューペアの動的パラメータ
- @ParametersDelegate: パラメータのグループ化と委譲
高度な機能
- 型変換: String入力を各種Java型(Integer、Boolean、Enum等)に自動変換
- カスタムコンバーター: IStringConverterインターフェースによる独自変換
- バリデーション: 必須パラメータ、arity、カスタムバリデーション
- ヘルプ生成: アノテーションベースの自動usage生成
- 国際化: ResourceBundleによる多言語サポート
- ファイル読み込み: @構文によるパラメータファイルの読み込み
- パスワード入力: セキュアな入力プロンプト
サブコマンドサポート
- 階層構造: git風のネストしたコマンド構造
- コマンド分離: 各サブコマンドの独立したパラメータ定義
- 動的追加: 実行時のサブコマンド追加
- ヘルプ統合: サブコマンド別のヘルプ表示
メリット・デメリット
メリット
- 宣言的設計: アノテーションによる直感的なAPI
- 豊富な機能: 型変換、バリデーション、ヘルプ生成を内包
- コード量の削減: 最小限のコードで高機能を実現
- 型安全性: コンパイル時の型チェック
- 安定性: 長期間にわたる安定したAPI
- 軽量: 最小限の依存関係
デメリット
- アノテーション依存: アノテーションベースの制約
- 実行時エラー: アノテーション設定ミスが実行時に発覚
- カスタマイズの制限: 複雑なカスタマイズには限界
- モダンさの不足: 新しいJava機能への対応の遅れ
- ドキュメント: 公式ドキュメントの更新頻度
主要リンク
書き方の例
基本的な使用例
import com.beust.jcommander.*;
public class HelloWorld {
@Parameter(names = "--name", description = "ユーザー名", required = true)
private String name;
@Parameter(names = {"-v", "--verbose"}, description = "詳細出力")
private boolean verbose = false;
@Parameter(names = "--help", help = true, description = "ヘルプを表示")
private boolean help;
public static void main(String[] args) {
HelloWorld app = new HelloWorld();
JCommander jc = JCommander.newBuilder()
.addObject(app)
.build();
try {
jc.parse(args);
if (app.help) {
jc.usage();
return;
}
System.out.println("こんにちは、" + app.name + "さん!");
if (app.verbose) {
System.out.println("詳細モードが有効です");
}
} catch (ParameterException e) {
System.err.println("エラー: " + e.getMessage());
jc.usage();
}
}
}
型変換とバリデーションの例
import com.beust.jcommander.*;
import java.io.File;
import java.time.LocalDate;
import java.util.List;
public class ServerConfig {
@Parameter(names = "--port", description = "サーバーポート")
private Integer port = 8080;
@Parameter(names = "--hosts", description = "ホスト名のリスト")
private List<String> hosts;
@Parameter(names = "--config", description = "設定ファイルパス",
converter = FileConverter.class)
private File configFile;
@Parameter(names = "--start-date", description = "開始日(YYYY-MM-DD)",
converter = LocalDateConverter.class)
private LocalDate startDate;
@Parameter(names = "--log-level", description = "ログレベル")
private LogLevel logLevel = LogLevel.INFO;
@Parameter(names = "--threads", description = "スレッド数",
validateWith = PositiveInteger.class)
private int threads = 4;
// カスタムコンバーター
public static class FileConverter implements IStringConverter<File> {
@Override
public File convert(String value) {
return new File(value);
}
}
public static class LocalDateConverter implements IStringConverter<LocalDate> {
@Override
public LocalDate convert(String value) {
return LocalDate.parse(value);
}
}
// バリデーター
public static class PositiveInteger implements IParameterValidator {
@Override
public void validate(String name, String value) throws ParameterException {
int n = Integer.parseInt(value);
if (n <= 0) {
throw new ParameterException("パラメータ " + name + " は正の整数である必要があります (入力値: " + value + ")");
}
}
}
public enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
public static void main(String[] args) {
ServerConfig config = new ServerConfig();
JCommander.newBuilder()
.addObject(config)
.build()
.parse(args);
System.out.println("サーバー設定:");
System.out.println(" ポート: " + config.port);
System.out.println(" ホスト: " + config.hosts);
System.out.println(" 設定ファイル: " + config.configFile);
System.out.println(" 開始日: " + config.startDate);
System.out.println(" ログレベル: " + config.logLevel);
System.out.println(" スレッド数: " + config.threads);
}
}
サブコマンドの例
import com.beust.jcommander.*;
// メインクラス
public class GitTool {
@Parameter(names = {"-v", "--verbose"}, description = "詳細出力")
private boolean verbose = false;
public static void main(String[] args) {
GitTool main = new GitTool();
AddCommand addCmd = new AddCommand();
CommitCommand commitCmd = new CommitCommand();
PushCommand pushCmd = new PushCommand();
JCommander jc = JCommander.newBuilder()
.addObject(main)
.addCommand("add", addCmd)
.addCommand("commit", commitCmd)
.addCommand("push", pushCmd)
.build();
jc.parse(args);
String parsedCommand = jc.getParsedCommand();
if (parsedCommand == null) {
jc.usage();
return;
}
if (main.verbose) {
System.out.println("詳細モードが有効です");
}
switch (parsedCommand) {
case "add":
addCmd.execute();
break;
case "commit":
commitCmd.execute();
break;
case "push":
pushCmd.execute();
break;
}
}
}
// サブコマンド: add
@Parameters(commandNames = "add", commandDescription = "ファイルをステージングエリアに追加")
class AddCommand {
@Parameter(description = "追加するファイル")
private List<String> files;
@Parameter(names = {"-A", "--all"}, description = "すべてのファイルを追加")
private boolean all = false;
public void execute() {
if (all) {
System.out.println("すべてのファイルを追加します");
} else if (files != null && !files.isEmpty()) {
System.out.println("ファイルを追加: " + files);
} else {
System.out.println("追加するファイルが指定されていません");
}
}
}
// サブコマンド: commit
@Parameters(commandNames = "commit", commandDescription = "変更をコミット")
class CommitCommand {
@Parameter(names = {"-m", "--message"}, description = "コミットメッセージ", required = true)
private String message;
@Parameter(names = {"-a", "--all"}, description = "全ての変更をコミット")
private boolean all = false;
public void execute() {
System.out.println("コミット: " + message);
if (all) {
System.out.println("全ての変更をコミットします");
}
}
}
// サブコマンド: push
@Parameters(commandNames = "push", commandDescription = "変更をリモートにプッシュ")
class PushCommand {
@Parameter(names = "--remote", description = "リモート名")
private String remote = "origin";
@Parameter(names = "--branch", description = "ブランチ名")
private String branch = "main";
@Parameter(names = {"-f", "--force"}, description = "強制プッシュ")
private boolean force = false;
public void execute() {
System.out.println("プッシュ先: " + remote + "/" + branch);
if (force) {
System.out.println("強制プッシュモード");
}
}
}
動的パラメータと委譲の例
import com.beust.jcommander.*;
import java.util.Map;
import java.util.HashMap;
public class BuildTool {
@Parameter(names = "--help", help = true)
private boolean help;
// 動的パラメータ(-Dkey=value形式)
@DynamicParameter(names = "-D", description = "システムプロパティ")
private Map<String, String> systemProperties = new HashMap<>();
// パラメータ委譲
@ParametersDelegate
private LoggingOptions loggingOptions = new LoggingOptions();
@ParametersDelegate
private DatabaseOptions databaseOptions = new DatabaseOptions();
public static void main(String[] args) {
BuildTool tool = new BuildTool();
JCommander jc = JCommander.newBuilder()
.addObject(tool)
.build();
try {
jc.parse(args);
if (tool.help) {
jc.usage();
return;
}
tool.execute();
} catch (ParameterException e) {
System.err.println("エラー: " + e.getMessage());
jc.usage();
}
}
private void execute() {
System.out.println("ビルドツール実行中...");
// システムプロパティの表示
if (!systemProperties.isEmpty()) {
System.out.println("システムプロパティ:");
systemProperties.forEach((key, value) ->
System.out.println(" " + key + " = " + value));
}
// ロギング設定の表示
System.out.println("ログレベル: " + loggingOptions.getLogLevel());
System.out.println("ログファイル: " + loggingOptions.getLogFile());
// データベース設定の表示
if (databaseOptions.getUrl() != null) {
System.out.println("データベースURL: " + databaseOptions.getUrl());
System.out.println("ユーザー名: " + databaseOptions.getUsername());
}
}
}
// ロギングオプションのグループ
class LoggingOptions {
@Parameter(names = "--log-level", description = "ログレベル")
private String logLevel = "INFO";
@Parameter(names = "--log-file", description = "ログファイルパス")
private String logFile;
public String getLogLevel() { return logLevel; }
public String getLogFile() { return logFile; }
}
// データベースオプションのグループ
class DatabaseOptions {
@Parameter(names = "--db-url", description = "データベースURL")
private String url;
@Parameter(names = "--db-user", description = "データベースユーザー名")
private String username;
@Parameter(names = "--db-password", description = "データベースパスワード", password = true)
private String password;
public String getUrl() { return url; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
国際化とカスタムヘルプの例
import com.beust.jcommander.*;
import java.util.ResourceBundle;
public class I18nExample {
@Parameter(names = "--name", descriptionKey = "name.description", required = true)
private String name;
@Parameter(names = "--age", descriptionKey = "age.description")
private int age = 0;
@Parameter(names = "--help", help = true, descriptionKey = "help.description")
private boolean help;
public static void main(String[] args) {
I18nExample app = new I18nExample();
// リソースバンドルの設定
ResourceBundle bundle = ResourceBundle.getBundle("messages");
JCommander jc = JCommander.newBuilder()
.addObject(app)
.resourceBundle(bundle)
.programName("i18n-example")
.build();
try {
jc.parse(args);
if (app.help) {
jc.usage();
return;
}
System.out.println("名前: " + app.name);
if (app.age > 0) {
System.out.println("年齢: " + app.age);
}
} catch (ParameterException e) {
System.err.println("エラー: " + e.getMessage());
jc.usage();
}
}
}
対応するリソースファイル messages.properties
:
name.description=ユーザーの名前を指定します
age.description=ユーザーの年齢を指定します(オプション)
help.description=ヘルプメッセージを表示します
英語版 messages_en.properties
:
name.description=Specify the user's name
age.description=Specify the user's age (optional)
help.description=Display help message
ファイルからのパラメータ読み込み
import com.beust.jcommander.*;
public class FileParameterExample {
@Parameter(names = "--config", description = "設定ファイルパス")
private String configFile;
@Parameter(names = "--output", description = "出力ディレクトリ")
private String outputDir = "build";
@Parameter(names = "--verbose", description = "詳細出力")
private boolean verbose = false;
public static void main(String[] args) {
FileParameterExample app = new FileParameterExample();
JCommander jc = JCommander.newBuilder()
.addObject(app)
.build();
jc.parse(args);
System.out.println("設定ファイル: " + app.configFile);
System.out.println("出力ディレクトリ: " + app.outputDir);
System.out.println("詳細出力: " + app.verbose);
}
}
パラメータファイル build.args
:
--output
dist
--verbose
実行例:
# ファイルからパラメータを読み込み
java FileParameterExample @build.args --config app.properties
# 通常のコマンドライン引数と組み合わせ可能
java FileParameterExample @build.args --output custom-output