Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

オブジェクト返却メソッドの異常系実装

概要

今回は、メソッド仕様の話。
オブジェクト値を返却するメソッドにおいて、入力値・プログラム状態によって異常な結果になったときにそれを示したい。
その方法として、以下の3通りが考えられる。

これらの使いどころやそれぞれの利点について記述する。

詳細内容

実装内容

コンソールアプリケーションでユーザが入力した文字列からコマンド(コード内に定義)を実行する。
実行例

ls
カレントのファイルリスト出力
ls /usr/local/bin
/usr/local/bin ディレクトリのファイルリスト出力
rm /usr/local/bin
/usr/local/binの削除
aaa
不正なコマンド aaa

基本実装

※ この後実装するため getCommandメソッドを空実装にしている。

package com.example1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	/**
	 * コマンド定義.
	 *
	 */
	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		};
		public abstract void execute(String... params);
	}

	/**
	 * メインロジック.標準入力からコマンドを入力して,登録された処理を実行する.
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			getCommand(elements[0]).execute(elements);
		}
	}
	/**
	 * 入力文字列から処理するコマンドを選択する.
	 * @param cmd コマンド文字列
	 * @return コマンド実体
	 */
	public static Command getCommand(String cmd) {
		return null; // TODO: 空実装
	}
}

getCommandメソッドの異常系処理、それに伴うロジック部の対応

登録されていないコマンドを指定した場合の処理を3通りの方法を実装する。

null を返却する場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return null;
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			Command cmd = getCommand(elements[0]);
			if(cmd != null) {
				cmd.execute(elements);
			}else {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

例外を発生させる場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		throw new IllegalArgumentException(cmd + " is not defined.");
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			try {
				getCommand(elements[0]).execute(elements);
			}catch (IllegalArgumentException e) {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

NULL オブジェクトを返却する場合

NULLオブジェクトの定義

	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		},
		// NULL オブジェクト追加
		UNDEFINED {
			@Override
			public void execute(String... params) {
				System.out.println("不正なコマンド " + params[0]);
			}
		};
		public abstract void execute(String... params);
	}

getCommandメソッド修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return Command.UNDEFINED;
	}

ロジック部修正

変更不要

それぞれの実装利点/欠点

null 返却

利点

  • 結果が存在しないことを意図的に示せる。
  • オブジェクトを作成しないため、メモリ領域の節約になる。

欠点

  • メソッド利用者の処理で null チェックが必要になると、利用者の処理が複雑化する。
  • メソッド利用者が意図しない場所で NullPointerException が発生する可能性がありデバッグに大きなコストがかかる可能性あり。

例外発生

利点

  • 処理が続行できない場合、その後の処理を中断できるためメソッド利用者のコードがシンプルになる。
  • チェック例外をスローすれば、異常時対応をメソッド利用者に通知できる。

欠点

  • 実行時例外をスローすると、メソッド利用者が意図せず処理が中断することがある。
  • チェック例外をスローすると、異常時対応を強制するため、メソッド利用者のコードが複雑化する。

NULL オブジェクト返却

利点

  • null チェックが必要ないため、利用者のコードがシンプルになる。
  • 関連した処理に格納されるため、コードが見やすくなる
  • 利用者のコードが複数あっても、お決まりの処理の記述が其々で不要なため、冗長性がなくなり保守性が向上する。

欠点

  • 事前に定義する必要がある。

まとめ

null 返却の利点は基本的に大きな利点にならないため、意図的に null を返す場合は少ない。
また、null を返すことは思わぬバグを生む可能性があるため余程の意図がない限り行うべきではない。

基本的には、NULL オブジェクトで実装する方が保守性が良くなる。
しかし、全メソッドに NULL オブジェクトを作成しても、多くの NULL オブジェクト定義ができるだけで、あまり効果はない。
つまり、それほど重要でない処理にまでこのような実装をする必要はない。

例外を返す場合、その処理が「本質的にどういう処理ものであるべきか」に基づいて決定すべき。
その処理が仕様として、起こりえなかったり・明らかに不自然であれば、例外で実装するのが良い。
例外仕様設計 - Status Code 303 - See Other

また、異常時に返す値・その意図をドキュメントに示す方が良い(特に null を返す場合)。
そして、これらを意識することによって複数人開発においては以下の利点が見込まれる。

  • バグの少ない品質の良いコード
  • デバッグに要する時間が少ない保守面で優れたコード
  • 開発者に異常系を意識したコーディングの促進


一通りコードが書けるようになった人は、次はメソッド仕様として
このようなことを考えてコーディングできるようにしてみよう!

if-else 文と条件演算子(?:)の使い分け

概要

プログラミングを行ううえで、複数の選択があり基本的にどちらでも実装できる処理は数多くある。
これらは常にどっちを使うではなく、これらはプログラムが対象とする処理の内容に応じて使い分けるべきである。

今回は、その中でも分岐を表す方法として if-else 文と条件演算子の使い分けについて記述する。

詳細説明

記述法の違い

ここでは、ある条件に応じて、文字列変数 val を分岐させる処理を記述する。

if-else文
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = null;
	if(condition) {
		// 真の場合の処理
		val = "真の場合の値";
	}else {
		// 偽の場合の処理
		val = "偽の場合の値";
	}
条件演算子
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = condition ? "真の場合の値" : "偽の場合の値"; // 値を分岐

それぞれの利点/欠点

if-else 文

  • void 型命令も記述可能
  • 処理を複数記述できる

条件演算子

  • 乱用しなければ読みやすい
  • 事前に変数宣言する必要がなし(変数が冗長にならない)

どっちを使うべきか?

if-else 文は処理を分岐させることができ、条件演算子は値を分岐させるために用いる。
そして、一般的には if-else 文の方が値の分岐に関わらず処理も記述できるためできる事が多い。

逆に 条件演算子にできないことは、値に応じた処理内容が変わるとき。
ここから、一つの使い分け条件ができる。

条件演算子
データを選択する必要があるが、概念的にどの分岐があっても処理が同じ
if-else 文
分岐によって、全く異なる処理を選択

例1(条件演算子を使うべきだと思う例)

使用している Linux OS から使用すべきパッケージマネージャを判別し、
パッケージをインストールさせようとしているとする。
今回は CentOS/RedHat系なら rpm、そうでなければ apt-get を利用するとする。

if-else文の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = null;
	if(rpmUses.contains(getOsName())) {
		pkgmngr = "rpm -ivh";
	}else {
		pkgmngr = "apt-get install";
	}
	installPackage(pkgmngr, "zabbix-server-mysql");
条件演算子の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = rpmUses.contains(getOsName()) ? "rpm -ivh" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

これらは、パッケージマネージャのプログラム名(文字列データ)を選択する責任を担っている。
概念的にやってることはどの分岐でも同じなので、後者の条件演算子で記述すべきと思う。

もし、RedHat の場合は別のパッケージマネージャ、例えば yum を使うことになったら、下記のようになる。

	List<String> rpmUses = Arrays.asList("centos");
	List<String> yumUses = Arrays.asList("redhat");
	String os = getOsName();
	String pkgmngr = rpmUses.contains(os) ? "rpm -ivh" : 
			 yumUses.contains(os) ? "yum install" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

ここで、ネストするときは行を分けて記述すると処理内容が分かりやすい。
(よく条件演算子は可読性が悪いというけれど、書式を工夫すれば問題ないと思う)
しかしこれ以上増やすと見苦しいソースコードになるので、3~4個くらいが限界か。

それ以上の分岐を書くときは、列挙型などを使用して記述する方が良い。
試しに、fedora のときは dnf というパッケージマネージャを利用することにすると。
私なら、処理内容は次のように修正する。

まず、各 OS の列挙型に使用するパッケージマネージャ情報を定義。

private static enum LinuxOS {
	CENT_OS("centos", "rpm -ivh"),
	RED_HAT("redhat", "yum install"),
	FEDORA("fedora", "yum install"),
	OTHER("other", "apt-get install");
	
	private String os;
	private String pkgmngr;
	private LinuxOS(String os, String pkgmngr) {
		this.os = os;
		this.pkgmngr = pkgmngr;
	}
	static LinuxOS get(String osName) {
		for(LinuxOS linux : values()) {
			if(linux.os.equals(osName)) {
				return linux;
			}
		}
		return OTHER;
	}
}

パッケージマネージャを取得するロジック。

	String pkgmngr = LinuxOS.get(getOsName()).pkgmngr;
	installPackage(pkgmngr, "zabbix-mysql");

ロジック自体の処理が見辛くなることはなくなるし、今後新しいパッケージマネージャが増えても列挙型を増やすだけで対応可能。
また、OS で使用するパッケージマネージャを変更したければ、OS の列挙型の中身を修正するだけで対応できる。

例2 (if文を使うべきだと思う例)

ユーザログイン画面でパスワード認証するページを想定する。
認証に成功すればメンバーページに遷移し、逆に失敗すれば認証ページを再び表示する。

if-else文の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
			gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
			gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
条件演算子の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

この例では、認証失敗・成功で表示ページを切り替えるだけであるから、データの分岐に見えるかもしれない。
しかし、個人的には、この場合はif文で実装にすべきだと思っている。

なぜなら、分岐による処理の内容は本質に異なるから。
成功時に処理したい内容、認証失敗時に処理したい内容、これらは当然異なる処理になりやすい。

例えば、セキュリティ対策の一環として、あまりにログイン処理失敗が多い場合はログに記録しておきたい場合もあるし、
ログインに成功すれば、その場でセッションを新しくする処理も必要になる。
これらは、処理が本質的に異なるため条件演算子で対応すると複雑になりやすい。

以下は、仮に認証失敗時にログを取得する場合をさきほどの実装から追加した場合。

	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
		gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
		writeLog(new Date() + " id->" + id); // 一行追加
		gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(!pwd.equals(getPassword(id))) {
		writeLog(new Date() + " id->" + id); // 認証失敗時、結局if文を使う
	}
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

おまけに条件演算子の実装は、失敗時に次に遷移するページを表示する前に
ログを出力するということが分かり辛いと思う。(前者に比べて)

この処理は端的に言うと「ログインが成功したときは~~、失敗したなら~~。」
というログインが成功したかが基準となり、どちらかの処理が選択される構造。
これを端的に表現できるのは、if文の強みでもある。

まとめ

条件演算子はダメで if-else 文のみという現場もあるらしいが、ナンセンスな話だと思う。
たしかに条件演算子は書き方によっては複雑になるが、フォーマットを工夫すれば見やすくなる。
なにより、分岐が多くなれば条件演算子に関わらず、見辛くなるのは if-else 文もあまり変わらない。

今回は if-else 文と条件演算子の違いについて記述したが、これらですら書き方一つでコードの読みやすさは変わる。
文法レベルだけの解釈だけでなく、現在書いている処理の意味に応じて柔軟に対応できた方が良いだろう。
このようなことでも、意識してプログラミングできる/できないが、プログラマとして実力差につながると思う。

謝辞

これらの記事を書くきっかけを作ってくれた、H氏。いつもいい刺激を頂きありがたい限り。

Seasar2 (導入編)

概要

今回は、DI コンテナという少し特殊な技術を用いる。
この技術を使うと、プログラムをコンパイルせずに、プログラム外部にあるリソースファイルを使いプログラムの挙動を制御できる。
また、デザインパターンで言う Factory や Singleton パターンを容易に実装できるため、
オブジェクトのライフサイクルに気を取られず、どのようなクラスを作成するかに注力できるようになる。

DI って?

プログラミングのスタイルとして、まずインタフェースを決定し、その後詳細を実装していくスタイルがある。
このスタイルは、インタフェースでコンポーネントの仕様を決め、それぞれの仕様に従ったクラスを実装するという
流れでプログラミングを行うため、各コンポーネントが互いに依存しない(疎な)コンポーネント群になりやすい。

そして、この考えを発展させたものとして DI (Dependency Injection) という設計パターンがある。
これは、それぞれのコンポーネント間が疎になることにより、コンポーネントを自由に取り替えることができるようになる。
そのため、どのコンポーネントを使うかをプログラム内で記述するのではなく、外部リソースを使って決定させるようにする。
これによって、外部リソースを変更するだけで、プログラムをコンパイルし直さなくても、プログラムの挙動を変えることができる。

これによって、以下の利点が生まれる。

  • コンポーネントの再利用促進
  • オブジェクト使用コードとオブジェクト生成コードの分離
  • テスト単純化
  • 保守性向上

今回用いる DI コンテナ

Java では、有名なものには Spring DI, Dagger, Google Guice などがあるらしいが、
参考:Dependency Injection Options For Java | Keyhole Software
今回は Seasar2 を使う。(Seasar2 - Seasar2)

Seasar2 は、世界的に見るとシェアは低く、新規機能開発も現時点では終了している(バグ改修のみ)ため、
今ある以上のことが今後できる見込みはない。

しかし、

  • 少ない設定で使用できる
  • 専用プラグインが充実している
  • 国産のため日本語文献が多い

といった利点があるため、規模の小さい開発や DI の実践演習としては有効な選択肢になりやすい。
なお、Searsar2 では、DI コンテナ以外にも、DB連携機能、アスペクト指向プログラミングなどの機能も提供している。

導入方法

日本語版の WindowsEclipse (Mars, ver4.5.1)を使用している場合を示す。
(画像は別環境でのスクリーンショットなため、Mac 英語版ですが...)

SeaSar2 用のプラグインの導入

次のプラグインを導入。

Eclipseツールバーより、 [ヘルプ] -> [新規ソフトウェアのインストール...]を選択。
上記テキストボックスの「作業対象」に http://eclipse.seasar.org/updates/3.3/ を追加する。
f:id:kouki_hoshi:20160316201117p:plain
そして、以上から、Dolteng, Kijimuna, SAStruts Plugin, Resource Synchronizer をインストールする。

なお、SAStruts Plugin のインストールがうまくいかなかった場合(私はなんかエラーが出た...)
http://download.eclipse.org/releases/mars/"Eclipse のバージョンによってURL 変える感じ。
を対象に入れて、「Web, XML, Java EE and OSGi Enterprise Development」をインストールすると、
SAStruts Plugin もインストールできた。
参考:SAStrutsPlugin がインストールできない場合の対応方法 - KusoBoze is here.

次に、作業対象に「http://eclipse.seasar.org/updates/3.2/」を追加する。
そして、DbLauncher をインストールする。
f:id:kouki_hoshi:20160316201124p:plain

ライブラリ追加

あとは、Seasar2 - Downloads にアクセスして、
S2Tiger は便利な拡張機能があるので、入れておいた方が良さそう。
また、DBを使うなら S2JDBC-GEN も入れた方が良い。
f:id:kouki_hoshi:20160316201130p:plain
インストール方法:Seasar - DI Container with AOP -

これで導入フェーズは終了。
次回からはサンプルの構築に移る予定。

参考文献

Seasar2徹底入門 SAStruts/S2JDBC対応

Seasar2徹底入門 SAStruts/S2JDBC対応

Dropbox API を使ってみる

概要

普段何気なく使っているツールに、Dropbox がある。

このツールは、様々なデバイス間のファイル変更を自動でマージしてくれる他、
リビジョンも保存するため、間違って上書きした場合などに、前回のファイルを復帰することができる。
また、Dropbox は記録領域をアプリ動作環境以外でも共有できるため、使用ユーザはその情報をどこからでも自由に参照できる。

今回は、このDropbox API を使って、Dropbox の操作をプログラムの一部に組み込む。

導入

本家にチュートリアルがあるため、手順はそれほど難しくない。
しかし、英語なので得意でない人は、覚悟がいるかも。(それほど難しくない)
Dropbox - Developers

ライブラリ導入

API は、バージョン1, 2 の2つ存在する。新しい方の v2 を今回は用いる。
Maven 使っている人は、以下を pom.xml に追加する。

<dependency>
    <groupId>com.dropbox.core</groupId>
    <artifactId>dropbox-core-sdk</artifactId>
    <version>2.0-beta-5</version>
</dependency>

(上記はDropbox 本家の内容を引用。2016/3/3 から、正式版ライブラリ 2.0.0 がリリースされてます。)

ビルドパスだけで設定したい人は、Maven レポジトリ(Maven Repository: Search/Browse/Explore)
から以下の jarファイル をダウンロードしてビルドパスに加える。

API ライブラリ
JSON処理系ライブラリ
  • jackson-core-2.7.1.jar
  • jackson-annotations-2.5.4.jar
  • jackson-databind-2.5.4.jar
その他依存ライブラリ
  • animal-sniffer-annotations-1.10.jar
  • okhttp-2.5.0.jar
  • okio-1.6.0.jar

app登録

Dropbox のアカウントがない人は、まず作成。
本日時点では、メールアドレスを使って無料で作成できる。

次に、Dropbox API を使用するアプリの登録する。
Dropbox の Developer サイト(Dropbox - Developers) のメニューから【My apps】を選択。
右上のボタン【Create app】からアプリ登録をする。内容はそれほど難しくないため設定内容は割愛。

設定ファイル

プログラムから Dropbox API を使用するのに必要な情報は次の2つ。

  • アプリ名
  • トークン文字列 (アプリ詳細の settings タブ /OAuth 2 /Generated access token /【Generate】ボタンを押して作成)

なお、これらがあると Dropbox API をどこでも使用できるため、情報管理には気を使うこと。
この部分を dropbox.properties に記述。

dropbox.app.name = 
dropbox.access.token = 

参考:プロパティファイルの使用方法 - Status Code 303 - See Other

コード実装

ほぼ機能として作り込まれているため、そのままロジックに記述。今回の実装内容と動きは以下の通り。

  1. API と通信するクライアント作成
  2. Dropbox の現在の最大容量・使用量の表示
  3. フォルダ作成 (+フォルダ内にファイルが存在しないか確認)
  4. ファイルをアップロード (+フォルダ内にファイルが作られたこと確認)
  5. 10秒間スレッドスリープ (本来不要。リビジョンの更新日時を分かりやすくするため)
  6. 別ファイルを同名で上書きアップロード (+フォルダ内にあるアップロードファイルは1つであること確認)
  7. アップロードしたファイルのリビジョン表示 (+リビジョンが複数ある(サイズ/更新時間が異なる)ことを確認)
  8. アップロードしたファイルをダウンロード
  9. 最初に作成したフォルダの削除
package api.dropbox;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;

import com.dropbox.core.DbxException;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.Metadata;
import com.dropbox.core.v2.files.WriteMode;

import resources.properties.Property;

public class Main {

	public static void main(String[] args) throws DbxException, IOException, InterruptedException {
		Property property = new Property("dropbox");
		DbxRequestConfig config = new DbxRequestConfig(property.getString("dropbox.app.name"), "ja_JP");
		DbxClientV2 client = new DbxClientV2(config, property.getString("dropbox.access.token"));

		
		System.out.println("Capacity: "
				+ client.users().getSpaceUsage().getAllocation().getIndividualValue().getAllocated() / 1024 / 1024 + " MB");
		System.out.println("Usage: " + client.users().getSpaceUsage().getUsed() / 1024 / 1024 + " MB");
		
		System.out.println("Create Folder /test ---------");
		client.files().createFolder("/test"); // 既に存在していると例外発生
		System.out.println("Show file list under /test ---------");
		for (Metadata metadata : client.files().listFolder("/test").getEntries()) {
			System.out.println(metadata.getName());
		}
		System.out.println("Upload /test/sample.jar ---------");
		FileInputStream fileStream = new FileInputStream(new File("./lib/dropbox/okhttp-2.5.0.jar"));
		client.files().uploadBuilder("/test/sample.jar").withMode(WriteMode.OVERWRITE).uploadAndFinish(fileStream);
		System.out.println("Show file list under /test ---------");
		for (Metadata metadata : client.files().listFolder("/test").getEntries()) {
			System.out.println(metadata.getName());
		}
		System.out.println("Sleep 10 seconds......please wait a moment.");
		Thread.sleep(10000);

		System.out.println("Overwrite /test/sample.jar ---------");
		FileInputStream fileStream2 = new FileInputStream(new File("./lib/dropbox/okio-1.6.0.jar"));
		client.files().uploadBuilder("/test/sample.jar").withMode(WriteMode.OVERWRITE).uploadAndFinish(fileStream2);
		System.out.println("Show file list under /test ---------");
		for (Metadata metadata : client.files().listFolder("/test").getEntries()) {
			System.out.println(metadata.getName());
		}
		
		System.out.println("Show file list under /test ---------");
		for (Metadata metadata : client.files().listFolder("/test").getEntries()) {
			System.out.println(metadata.getName());
		}

		System.out.println("Show revision list about /test/sample.jar ---------");
		List<FileMetadata> revisions = client.files().listRevisions("/test/sample.jar").getEntries();
		for (FileMetadata filemetadata : revisions) {
			System.out.println("Name:" + filemetadata.getName() + "\tSize:" + filemetadata.getSize() + "\tDate:" + filemetadata.getClientModified());
		}

		System.out.println("Download sample.jar into THIS project under /lib/sample.jar ---------");
		FileOutputStream outputStream = new FileOutputStream(new File("./lib/dropbox/sample.jar"));
		client.files().download("/test/sample.jar").download(outputStream);
		
		System.out.println("Remove /test ---------");
		client.files().delete("/test"); //削除対象がないと例外発生
	}
}

これによって、次のような出力が表示される。

Capacity: 3072 MB
Usage: 3019 MB
Create Folder /test ---------
Show file list under /test ---------
Upload /test/sample.jar ---------
Show file list under /test ---------
sample.jar
Sleep 10 seconds......please wait a moment.
Overwrite /test/sample.jar ---------
Show file list under /test ---------
sample.jar
Show file list under /test ---------
sample.jar
Show revision list about /test/sample.jar ---------
Name:sample.jar	Size:65928	Date:Sat Mar 05 01:30:35 JST 2016
Name:sample.jar	Size:317886	Date:Sat Mar 05 01:30:23 JST 2016
Download sample.jar into THIS project under /lib/sample.jar ---------
Remove /test ---------

それに呼応して、Dropboxを起動していると、「test を追加しました」「sample.jar を作成しました」
「sample.jar を上書きしました」「2つのファイルを削除しました」などのメッセージが表示され、
標準出力をしなくても、Java アプリから Dropbox を操作できたことを確認できる。

Zabbix API との通信を暗号化する

概要

前回記事
Javaアプリから Zabbix API を使う - Status Code 303 - See Other

java アプリケ−ションから通信はできたが、HTTP 通信のため平文で通信している。
外部にこれらを公開することを考慮して、今回は通信を暗号化する。

また、Zabbix サーバとの SSL 通信に用いる証明書はいわゆる「オレオレ証明書」を用いる。
これらは信頼できない証明にあたるため、Java はデフォルトで通信を許可しないが、これらを無理矢理設定して通信する。

Raspberrypi 設定

Raspberrypi の SSL 対応

作業手順は、下記記事を参考に行った。
Qaplaの覚書・メモ・備忘録・独言 Raspberry Piを公開サーバ化(HTTPS対応)

Linux 系 OS の SSL 導入記事は、Apache2 の conf ファイル名が違ったり、
apt-get から取得したアプリケーションのフォルダ階層が違ったりしていたため、
手順だけ知りたい人は、Raspberrypi での導入記事を選択した方が良さそう。

なお、この証明書作成の過程で何をやっているかについて興味があるなら、この記事を参考に。
オレオレ証明書をopensslで作る(詳細版) - ろば電子が詰まっている

Java アプリケーション

前回と同じく Jersey を用いて実装を行う。まず、何もしないでアクセス先を http → https に変更すると、
下記のようなエラーログが出る。

Exception in thread "main" javax.ws.rs.ProcessingException: Already connected
	at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:264)
	at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:684)
	at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:681)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
	at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:444)
	at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:681)
	at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:437)
	at org.glassfish.jersey.client.JerseyInvocation$Builder.post(JerseyInvocation.java:343)
	at api.zabbix.ZabbixAPIAccess.getResponse(ZabbixAPIAccess.java:76)
	at api.zabbix.ZabbixAPIAccess.getToken(ZabbixAPIAccess.java:82)
	at api.zabbix.ZabbixAPIAccess.request(ZabbixAPIAccess.java:41)
	at api.zabbix.ZabbixAPIAccess.request(ZabbixAPIAccess.java:51)
	at api.zabbix.Main.main(Main.java:24)
Caused by: java.lang.IllegalStateException: Already connected
	at sun.net.www.protocol.http.HttpURLConnection.setRequestProperty(HttpURLConnection.java:3014)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.setRequestProperty(HttpsURLConnectionImpl.java:316)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.setOutboundHeaders(HttpUrlConnector.java:421)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.access$100(HttpUrlConnector.java:96)
	at org.glassfish.jersey.client.internal.HttpUrlConnector$4.getOutputStream(HttpUrlConnector.java:384)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(CommittingOutputStream.java:200)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(CommittingOutputStream.java:194)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commit(CommittingOutputStream.java:262)
	at org.glassfish.jersey.message.internal.OutboundMessageContext.commitStream(OutboundMessageContext.java:816)
	at org.glassfish.jersey.client.ClientRequest.writeEntity(ClientRequest.java:545)
	at org.glassfish.jersey.client.internal.HttpUrlConnector._apply(HttpUrlConnector.java:388)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:285)
	at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:255)
	... 14 more

これは、どうやら https に変更すると、自動的に SSL で通信を行ってくれるのだが、
信頼できない証明書を利用した SSL 通信はデフォルトで、例外を発生させるみたいだ。

事実、信頼できる証明書を使用するサイトでは、前回実装でも SSL でアクセスできる(GoogleYahoo! Japan! で確認)。
では、信頼できない証明書を使っている場合はどうすれば良いか。

答えは、デフォルト設定を無理矢理変更して、アクセスする。
一般的に SSL 導入利点として挙げられるのは、

  1. 通信の暗号化(機密性)
  2. 通信先の相手が正しいことを保証(真正性)
  3. 改竄を検知できることを保証 (完全性)

信頼できない証明書を受け入れるということは、少なくとも「通信先の相手が正しいことを保証」はできなくなる。
もし、 名前解決の部分を悪意をもった誰かが操作して別のサイトに導いた場合、攻撃者はプログラムの通信に関与できてしまう。

このようなセキュリティ上のリスクがあることを認識すること。・・とはいえ、SSL 証明書は導入コストがかかるので、
開発中に「外部に公開しない・重要な処理や情報を扱わない」なら、例外を設けて通信をすることも必要になる。

前置きが長くなったが、実装内容。前回の ZabbixAPIAccess クラスに対して、次の static メソッドを追加。

	private static Client getSSLClient() throws NoSuchAlgorithmException, KeyManagementException {
		SSLContext sc = SSLContext.getInstance("TLSv1");
		System.setProperty("https.protocols", "TLSv1");
		// 全ての証明書を信用するトラストマネージャの作成。詳しくは、JSSE を検索すること。
		TrustManager[] trustAllCerts = { new X509TrustManager() {

			@Override
			public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
			}

			@Override
			public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
			}

			@Override
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}
		} };
		sc.init(null, trustAllCerts, new java.security.SecureRandom());

		// 全ホストを妥当だとする。詳しくは、JSSE を検索。
		HostnameVerifier allHostsValid = new HostnameVerifier() {

			@Override
			public boolean verify(String hostname, SSLSession session) {
				return true;
			}
		};
		// Client を Builder から作成するときに、上記設定を適用する。
		return ClientBuilder.newBuilder().hostnameVerifier(allHostsValid).sslContext(sc).build();
	}

そして、ZabbixAPIAccess クラスのgetResponse メソッドを次のように変更する。

	private Response getResponse(Map<String, Object> params) throws KeyManagementException, NoSuchAlgorithmException {
		return getSSLClient().target(property.getString("zabbix.api.url")).request()
				.post(Entity.entity(params, MediaType.APPLICATION_JSON));
	}

これに伴い、様々な所でキャッチ例外(KeyManagementException, NoSuchAlgorithmException)をどうにかしろと言われるので、throws 宣言を追加。
以上で完了。

ただし、さきほども記述した通り。セキュリティ上あまり良くはないので、
本番時には、ルート証明書を取得するか、例外設定処理を記述した方が良い。
そのときは、getSSLClient メソッドのクライアント取得手順を変更する。

その設定内容としては、以下が参考になる。
JavaのSSLSocketでSSLクライアントとSSLサーバーを実装する:CodeZine(コードジン)
Java SE 6 用 JSSE リファレンスガイド

そういえば、暗号化といえば。
2016年2月から正式リリースされた、Zabbix 3.0 の拡張機能で、 Zabbix-server と Zabbix-agent 間の暗号化をサポートするようになったとか。
Zabbix-Agentとの通信を暗号化してみる. (3.0の機能) - フロッピーディスクの残骸

これも時間があるときに試してみよう。

例外仕様設計

概要

プログラムが正しく実行できない状態に陥ったときに、それを処理側に通知するための機構として例外がある。
これによって、もしユーザからの入力が想定外のものだったりしても、安全にプログラムを終了させることができる。

しかし、仕様を考慮もせず適当に例外を発生させたりすれば、そのクラスはユーザによって使いづらいだろうし、
そうかといって、例外発生に対して適切な処理をしなければ、バグが発生する原因をユーザに調査させることになる。

このような可能性から、クラス設計において例外は、本質的な内容ではないことが多いが、
仕様の一部であるべきと考えている。今回は、この例外仕様について個人的な意見を記述する。

事前チェック VS 例外

コマンドライン引数から数値変換・計算・表示するプログラムを例として説明する。
前提として、これは「2つの整数を除算した結果(整数)を表示するプログラム」という仕様ということにする。

public class Main {
	public static void main(String[] args) {
		System.out.println(Integer.parseInt(args[0])/Integer.parseInt(args[1]));
	}
}

このプログラムは、単純な処理にも関わらず、以下の3種類の実行時例外が起こる可能性がある。

  • ArrayIndexOutOfBoundsException
  • NumberFormatException
  • ArithmeticException

それぞれの例外に対して、どのように対処すべきかの個人的な見解を述べる。

ArrayIndexOutOfBoundsException

これが発生するのは、コマンドライン引数が2個未満のときになる。
仕様が「2つの整数を除算した結果」なのだから、これは致命的な例外なので処理続行は不可能である。
つまり、例外を発生させるべきだと考える。

しかし、単純に異常終了させるべきではない。なぜなら、この問題の発生原因を考えれば想像がつく。
この例外が発生する原因は、入力項目が足りないことにある。
しかし、「ArrayIndexOutOfBoundsException」は配列外にアクセスしようとした例外である。

【ユーザ入力漏れ】→【配列外アクセス異常】というのは、プログラムの内部構造を意識して初めて理解できる。
プログラムを利用するユーザはこの例外を見て、ユーザが何の操作を間違っていたか分かるだろうか。
これを考慮すれば、違う例外を発生させるべきだろう。

public class Main {
	public static void main(String[] args) {
		if (args.length < 2) throw new IllegalArgumentException("数値は2つ必要"); //コマンドライン引数が2個未満のときは引数異常で終了
		System.out.println(Integer.parseInt(args[0])/Integer.parseInt(args[1]));
	}
}

NumberFormatException

これが発生するのは、入力が整数値じゃなかったとき。この例外に対しては何も対処しない。

まず、数値に変換できない時点で除算ができない。つまり、処理続行が不可能なため例外を発生させる。
また、そのときに「値が整数形式ではない」という例外が発生し、これはプログラム仕様からマッチした内容なのでユーザも理解しやすい。

もし異常終了が問題になる場合は、try-catch で例外をキャッチして異常系処理を記述する。
正常なロジックとして記述しないのは、この動作は正常動作の範囲内ではないという判断からだ。

public class Main {
	public static void main(String[] args) {
		if (args.length < 2) throw new IllegalArgumentException("数値は2つ必要");
		try {
			System.out.println(Integer.parseInt(args[0]) / Integer.parseInt(args[1]));
		} catch (NumberFormatException e) {
			System.out.println(e.getMessage());
		}
	}
}

ArithmeticException

これが発生するのは2番目の引数が 0 によって、0 除算が発生するとき。
0 は本仕様の整数値に含まれる入力なので、例外による異常終了はさせないで、正常系動作として実装すべき内容だ。
つまり、分岐によって事前チェックを行い対応する。

public class Main {
	public static void main(String[] args) {
		if (args.length < 2) throw new IllegalArgumentException("数値は2つ必要です");
		int arg0 = Integer.parseInt(args[0]);
		int arg1 = Integer.parseInt(args[1]);
		if (arg1 != 0) {
			System.out.println(arg0/arg1);
		}else {
			System.out.println("∞");
		}
	}
}

今回は、無限という結果(-∞は面倒臭いので考慮してない)にしているが、「計算できません」でも良いだろう。
大事なことは正常系の処理として記述することである。

異常終了 VS 例外キャッチ

例外をキャッチするか、そのまま異常終了させるかどうかの判断基準を記述する。

仕様上発生しない例外

さきほどの例でもあったが、仕様に対して明らかにおかしい入力値に対する処理は基本しなくて良い。
そのような例外にいちいち対処すると、本質ではないロジックのためにコードが読み辛くなり、保守性もおそらく落ちる。
また、そのような対処による品質向上効果は大抵薄い。

エラーアトミック性

エラーアトミック性とは、異常(例外)が起こってもシステム内部の状態を正常動作時に戻せる性質である。
この性質を持たせることができるならば、例外をキャッチして行うべきだろう。

しかし、このような仕組みはコストがかかりやすい。例えば、ある値を変更したものを元に戻す場合は、
そのための領域を確保しなければならないし、例外のタイミングで元に戻す処理内容が変わったりする。

また、例外の発生要因とは、多岐に渡るため全ての要因を塞ぐことは基本的にできないため、
影響度合や発生頻度が低いと判断できる場合は、例外によって処理を中断させることを検討した方が良い。

異常検知

発生する例外に全てロジックで対処する必要はないものの、それによってシステム内で整合性が取れない状態になってしまう可能性はある。
だから、どのような例外であっても、発生時には一度はキャッチしてその内容を調べるべき。

ただし、これを複数の箇所で記述すると複雑になりやすいので、大きな塊のエントリーポイントで行った方が良い。
この箇所では、実行ログ取得、運用チームへの通知など異常時に行うべき処理を記述する。
なお、例外によって中断させる意図を含むものもあるので、一般的にはその後異常終了させた方が良い。

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import org.slf4j.LoggerFactory;

public class Main {

	static void main(String[] args) {
		try(BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("~/data.txt")))) {
			bos.write(args[0].getBytes());
		} catch (IOException e) {
			LoggerFactory.getLogger(Main.class).debug(e.getMessage()); //エラーログ取得
			throw e; //異常なので結局スローする
		}
	}
}

例外をコントロールする

例外とは、そもそも異常状態になったことをプログラム実行側やユーザに伝える目的で行う。
ならば適切にその原因は伝えるべきだろう。

public class Main {

	static void main(String[] args) {
		try {
			System.out.println(args[0]);
		}catch(ArrayIndexOutOfBoundsException e) {
			throw new IllegalArgumentException("入力値不正"); //例外翻訳
		}
	}
}

なお、例外を無視するために使ってはならない。

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {

	static void main(String[] args) {
		try(BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("~/data.txt")))) {
			bos.write(args[0].getBytes());
		} catch (IOException e) { // ダメ!ゼッタイ!
		}
	}
}

上記のコードは、 IOException が起こっても正常終了するため、ユーザは正しい操作が行われていないにも関わらず気づかない。
また、このような内部状態の不整合は、後々にバグを生み出す原因となり、いつ発生するか予測がつかない。

参考文献

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

Javaアプリから Zabbix API を使う

概要

前回 curl で疎通確認を行ったが、今回はこれらを Java で実装する。
前回記事:Zabbix API を使ってみる - Status Code 303 - See Other

Zabbix API は、JSON 形式で API と通信するため、JSON 形式で通信できるようセットアップする必要がある。
また、Zabbix API は、API の種類が多く、そのリクエストパラメータとそのレスポンスデータの種類も
オブジェクトリスト・オブジェクト・文字リスト・基本データ型など多種多様なので、これらをなるべく扱えるよう設計する。

また、最後には拡張方法の方針について触れる。

ライブラリ

今回は、Jersey (Jersey)を用いる。
JAX-RS (JAX-RS - Wikipedia)という、RESTFUL なアーキテクチャを実装するための API として有名。
しかし、jersey には、クライアントの機能を提供するライブラリもあり、それらは結構使い易いらしい。
参考:HTTPクライアントとして使うjersey-client – Akira Koyasu's WebLog

導入

まずは、ライブラリをダウンロードして、それらに対してビルドパスを設定する。
まずは、HTTP クライアント用ライブラリの導入。

Jersey-client 本体
  • jersey-client.jar
Jersey-client に必要なライブラリ
  • jersey-common.jar
  • javax.ws.rs-api-2.0.1.jar
  • javax.annotation-api-1.2.jar
  • javax.inject-2.4.0-b31.jar
  • hk2-api-2.4.0-b31.jar
  • hk2-locator-2.4.0-b31.jar
  • hk2-utils-2.4.0-b31.jar
  • jersey-guava-2.22.1.jar
  • jersey-entity-filtering-2.22.2.jar
  • jersey-media-json-jackson-2.22.2.jar
  • jackson-core-2.7.1.jar
  • jackson-databind-2.5.4.jar
  • jackson-jaxrs-base-2.5.4.jar
  • jackson-jaxrs-json-provider-2.5.4.jar
JSON を扱うためのライブラリ

上記ライブラリだけでも HTTP 通信は可能だが、JSON には対応できないらしく、専用ライブラリを導入する必要がある。
java - MessageBodyWriter not found for media type=application/json - Stack Overflow

  • jackson-annotations-2.5.4.jar
  • jackson-module-jaxb-annotations-2.5.4.jar
Lombok

ボイラープレートコード削減ライブラリ。
Lombok ライブラリ - Status Code 303 - See Other

実装内容

今回作成したのは、大きく分けて3つ

  • Zabbix API クラス (API 通信コントロール)
  • パラメータ構築クラス (リクエストデータ格納)
  • Zabbix プロパティ (設定ファイル)

この他、API に応じて以下を作成する。

  • API 結果型 (値格納)

Zabbix API クラス

今回は、実装量がある程度多いため、コメントは結構適当。

package api.zabbix;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;

import lombok.Data;
import resources.properties.Property;

public class ZabbixAPIAccess {

	// Zabbix の設定ファイル
	static Property property = new Property("zabbix");

	// トークン格納先
	private String token;

	// トークンの管理を担当、結果はレスポンス形式。
	public Response request(ZabbixParam params) throws JsonProcessingException, IOException {
		// トークン取得条件。本来はタイムアウトとか考慮だろうけど、かなり期間長いみたいなので適当
		if (token == null) {
			token = getToken();
		}
		Map<String, Object> p = getWrappingRequest(params);
		p.put("auth", token);
		return getResponse(p);
	}

	// レスポンスデータをオブジェクト形式にマッピング。第1引数にリクエストデータ、第2引数にマッピングクラス記述
	public <T> T request(ZabbixParam params, Class<T> klass) throws JsonProcessingException, IOException {
		ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		JavaType type = mapper.getTypeFactory().constructParametrizedType(ZabbixResponse.class, ZabbixResponse.class, klass);
		ZabbixResponse<T> value = mapper.readValue((InputStream) request(params).getEntity(), type);
		return value.result;
	}

	// レスポンスデータをリスト形式にマッピング。第1引数にリクエストデータ、第2引数に各要素に適用するマッピングクラス記述
	public <E> List<E> requestWithList(ZabbixParam params, Class<E> componentKlass)
			throws JsonProcessingException, IOException {
		ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		TypeFactory factory = mapper.getTypeFactory();
		ZabbixResponse<List<E>> value = mapper.readValue((InputStream) request(params).getEntity(), factory
				.constructParametrizedType(ZabbixResponse.class, ZabbixResponse.class, factory.constructCollectionType(ArrayList.class, componentKlass)));
		return value.result;
	}

	// パラメータをAPI 仕様の形式にラッピング
	private Map<String, Object> getWrappingRequest(ZabbixParam params) {
		Map<String, Object> result = new HashMap<>();
		result.put("jsonrpc", "2.0");
		result.put("method", params.getMethod());
		result.put("id", 1);
		result.put("params", params.getParameters());
		return result;
	}

	// 通信
	private Response getResponse(Map<String, Object> params) {
		return ClientBuilder.newClient().target(property.getString("zabbix.api.url")).request()
				.post(Entity.entity(params, MediaType.APPLICATION_JSON));
	}

	// トークン取得
	private String getToken() throws JsonProcessingException, IOException {
		ZabbixParam loginParams = new MapParam(new ZabbixParam("user.login"))
				.put("user", property.getString("zabbix.user")).put("password", property.getString("zabbix.password"));
		return new ObjectMapper().readTree((InputStream) getResponse(getWrappingRequest(loginParams)).getEntity())
				.get("result").asText();
	}

	// 一時的なレスポンスデータ格納先。使わないため private
	@Data
	private static class ZabbixResponse<T> {
		private String jsonrpc;
		private T result;
		private int id;
	}
}

パラメータ構築クラス

メソッドとパラメータを格納。
パラメータがリストやマップの場合は、下記 Decorator クラスを使った方が良い。

package api.zabbix;

import lombok.Data;

@Data
public class ZabbixParam {

	// (Zabbix APIの) メソッド名
	private final String method;
	// リクエストパラメータ
	private Object parameters;

}

ZabbixParameter がMapだったときに活躍しそうな、Decorator クラス。

package api.zabbix;

import java.util.LinkedHashMap;
import java.util.Map;

public class MapParam extends ZabbixParam {
	
	public MapParam(ZabbixParam param){
		super(param.getMethod());
		super.setParameters(new LinkedHashMap<String, Object>());
	}
	
	@SuppressWarnings("unchecked")
	public MapParam put(String key, Object value) {
		((Map<String, Object>)super.getParameters()).put(key, value);
		return this;
	}
}

ZabbixParameter が List だったときに活躍しそうな、Decorator クラス。

package api.zabbix;

import java.util.ArrayList;
import java.util.List;

public class ListParam<T> extends ZabbixParam {

	public ListParam(ZabbixParam param){
		super(param.getMethod());
		super.setParameters(new ArrayList<T>());
	}
	
	@SuppressWarnings("unchecked")
	public ListParam<T> add(T elements) {
		((List<T>)super.getParameters()).add(elements);
		return this;
	}
}

利用方法

今回は、例として次の流れでロジックを構成する。

  • ホストグループ作成
  • 作成したホストグループ情報取得
  • 作成したホストグループ削除

対応する Zabbix API リファレンスはこちら。(※対応 Zabbix バージョン 2.4)

なお、API 仕様はバージョンによって、変わることがあるので注意すること。

Zabbix 設定ファイル (zabbix.properties)

API 通信できるユーザ名・そのパスワード、そして API 問い合わせ先の設定。

zabbix.user = xxxx
zabbix.password = yyyy

zabbix.api.url = http://xxx.xxx.com/zabbix/api_jsonrpc.php
値格納クラス

API リファレンスから、どんなデータが返ってくるか分かるので、それら用のマッピングクラスを作成。

  • ホストグループクラス (ホストグループ取得に使う)
package api.zabbix.vo;

import com.fasterxml.jackson.annotation.JsonProperty;

public class HostGroup {

	// JSON では groupid 要素をココにマッピング
	@JsonProperty("groupid")
	public String id;
	
	// ホストグループ名
	public String name;

	public String internal;
}
  • ホストグループ ID リスト格納クラス。(ホストグループ作成時・削除時使用)
package api.zabbix.vo;

import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;

public class HostGroupIds {

	@JsonProperty("groupids")
	public List<String> groupIds;
}
ロジッククラス

上記クラスを利用して、以下のようにロジックに記述する。

package api.zabbix;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;

import api.zabbix.vo.HostGroup;
import api.zabbix.vo.HostGroupIds;

public class Main {

	public static void main(String[] args) throws JsonProcessingException, IOException {

		ZabbixAPIAccess api = new ZabbixAPIAccess();

		// ホストグループ作成
		ZabbixParam createHostGroupParams = new MapParam(new ZabbixParam("hostgroup.create")).put("name", "test");
		HostGroupIds createValue = api.request(createHostGroupParams, HostGroupIds.class);

		// ホストグループ一覧取得 (フィルターで上記作成ホストグループだけ取得)
		Map<String, Object> filter = new HashMap<>();
		filter.put("name", "test");
		ZabbixParam getHostGroupParams = new MapParam(new ZabbixParam("hostgroup.get")).put("output", "extend").put("filter", filter);
		List<HostGroup> getValue = api.requestWithList(getHostGroupParams, HostGroup.class);
		System.out.println(getValue);
		
		// 上記のホストグループ削除
		ZabbixParam deleteHostGroupParams = new ListParam<String>(new ZabbixParam("hostgroup.delete")).add(createValue.groupIds.get(0));
		HostGroupIds deleteValue = api.request(deleteHostGroupParams, HostGroupIds.class);
		System.out.println(deleteValue.groupIds);
	}
}

出力結果

[{groupid=59, name=test, internal=0, flags=0}]
[59]

拡張手順

他の API 利用

次の手順で実装。

  1. Zabbix API 仕様を調べる
  2. 値格納用データクラス作成
  3. Main ロジック記述

例:ホストを作成する

  • Zabbix API 仕様

host.create [Zabbix Documentation 2.4]
host.get [Zabbix Documentation 2.4]
host.delete [Zabbix Documentation 2.4]

  • 値格納用クラスの追加
package api.zabbix.vo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class Host {

	@JsonProperty("hostid")
	public String id;

	public int available;
	
	public String name;
	
	public String host;
	
	@JsonProperty("status")
	public String status;
}
package api.zabbix.vo;

import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;

public class HostIds {

	@JsonProperty("hostids")
	public List<String> ids;
}

ロジックの記述(追加分)

		// ホスト作成
		List<Map<String, Object>> interfaces = new ArrayList<>();
		Map<String, Object> if0 = new HashMap<>();
		if0.put("type", 1);
		if0.put("main", 1);
		if0.put("useip", 1);
		if0.put("ip", "127.0.0.1");
		if0.put("dns", "");
		if0.put("port", "10050");
		interfaces.add(if0);
		List<Map<String, Object>> groups = new ArrayList<>();
		Map<String, Object> group = new HashMap<>();
		group.put("groupid", createValue.groupIds.get(0)); // hostgroup.create から取得
		groups.add(group);
		ZabbixParam createHostParams = new MapParam(new ZabbixParam("host.create")).put("host", "test_server")
				.put("interfaces", interfaces).put("groups", groups);
		HostIds createHostValue = api.request(createHostParams, HostIds.class);
		System.out.println(createHostValue.ids);

		// ホスト取得
		Map<String, Object> filter = new HashMap<>();
		filter.put("host", "test_server");
		ZabbixParam getHostParams = new MapParam(new ZabbixParam("host.get")).put("output", "extend").put("filter", filter);
		List<Host> getValue = api.requestWithList(getHostParams, Host.class);
		System.out.println(getValue);

		// ホスト削除
		ZabbixParam deleteHostParams = new ListParam<String>(new ZabbixParam("host.delete")).add(getValue.get(0).id);
		Response deleteHostValue = api.request(deleteHostParams);
		System.out.println(deleteHostValue.readEntity(String.class));

これでホスト作成・取得・削除も実装できる。

便利なリクエストパラメータを構築するクラスが欲しい

ZabbixParam クラスに対する、Decorator 実装を行う。

  1. ZabbixParam かそれらを継承したクラスを継承したクラス作成
  2. コンストラクタ引数は ZabbixParam を取るもの
  3. 便利なメソッドを追加
  4. ロジッククラスで利用

Zabbix API の通信ログ取得

ZabbixAPIAccess の getResponse で全ての通信を一括しているため、そこでログ取得ライブラリ(Log4j, Logbackなど)で取得する。
実装詳細は、本題からそれるため割愛する。

トークンの取得条件を変更

本実装のトークン管理は、インスタンスに任せているためインスタンスが消滅すると、トークンを毎回取得する。
これをデータベースなどに格納して、頻繁にトークン取得させないようにしたいなら。

  1. トークン管理用データベース・テーブル作成
  2. トークン取得の際にデータベースにトークン/取得時間を格納
  3. データベースからトークン情報取得
  4. トークンが期限内なら使用。そうでなければ、破棄して取得するロジックを記述

ただし、この実装の際には、データベースに大量のトークンが残る可能性あるため
何かしら管理する機構を持たなければならない。