SSHJ による暗号化通信
概要
様々な処理をサーバに任せ、その処理結果だけをサーバから取得したい場合がある。
このような通信を行う際、FTP や TELNET を用いて行うと
通信が暗号化されないため、その内容を第三者に盗聴されるリスクを生んでしまう。
ここでは、SSH による暗号化通信を Java で実装する方法について記述する。
また、SSH 用のライブラリは多数存在するが、今回は SSHJ(version 0.15.0) を用いる
実装内容
今回は「サーバと簡単な通信が行える」を前提として、次の機能を提供するクラスを作成。
- 認証 (パスワード/公開鍵(OpenSSHキーファイル)認証)
- リモート先でのコマンド実行
- SCP/SFTP を用いたファイルダウンロード
- SCP/SFTP を用いたファイルアップロード
SCP と SFTP は場合によって使い分ける可能性があると思い、両方使えるようにした。
SCP と SFTP の違いはこちらが参考になる。セキュリティプロトコルマスター(8):軽快なscpか高機能なsftp、sshサーバに向いているのは? (1/2) - @IT
今回は、 SSH 用のライブラリを今後取り替える可能性も考慮して、ロジックにこれらを記述せず
Facade クラス(Facade - GoF デザインパターン - Status Code 303 - See Other) を作成、これらを利用する。
詳細コード
ビルドパス設定
SSHJ を使用する場合に必要なライブラリ
暗号化ライブラリ
圧縮ライブラリ (ダウンロード/アップロードで必要)
- jzlib-1.1.3.jar
ログ出力ライブラリ
- sshd-core-1.0.0.jar
ボイラープレートコード排除
- lombok.jar (Lombok ライブラリ - Status Code 303 - See Other)
クラス
SSHJ のFacade クラス (SSHClientFacade.java)
package communicate.ssh; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.connection.channel.direct.Session.Command; import net.schmizz.sshj.transport.TransportException; import net.schmizz.sshj.transport.verification.PromiscuousVerifier; import net.schmizz.sshj.userauth.UserAuthException; import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; import net.schmizz.sshj.xfer.FileSystemFile; public class SSHClientFacade implements AutoCloseable { /** * SSH Client */ SSHClient ssh = new SSHClient(); /** * Connect server. * * @param host * @param port * @throws IOException */ public void connect(String host, int port) throws IOException { ssh.useCompression(); // need jzlib ssh.addHostKeyVerifier(new PromiscuousVerifier()); ssh.connect(host, port); } /** * Add user information with key file. * * @param user * @param keyFile * @throws TransportException * @throws UserAuthException */ public void authPublickey(String user, File keyFile) throws UserAuthException, TransportException { FileKeyProvider provider = new OpenSSHKeyFile(); provider.init(keyFile); ssh.authPublickey(user, provider); } /** * Add user information with password. * * @param user * @param password * @throws IOException */ public void authPassword(String user, String password) throws UserAuthException, TransportException { ssh.authPassword(user, password); } /** * Execute Command. * * @param command * @return SSH Result * @throws IOException */ public SSHResult execute(String command) throws IOException { try (final Session session = ssh.startSession()) { Command cmd = session.exec(command); cmd.join(30, TimeUnit.SECONDS); String output = IOUtils.readFully(cmd.getInputStream()).toString(); Integer status = cmd.getExitStatus(); if (status == 0) { return new SSHResult(status, output, ""); } return new SSHResult(status, output, IOUtils.readFully(cmd.getErrorStream()).toString()); } } /** * Download file. * * @param src * Source file path (remote) * @param dest * Destination directory or file path (local) * @param method * SCP or SFTP * @throws IOException */ public void download(String src, File dest, TransferMethod method) throws IOException { method.download(ssh, src, dest); } /** * Upload file. * * @param src * Source file path (local) * @param dest * Destination directory or file path (remote) * @param method * @throws IOException */ public void upload(File src, String dest, TransferMethod method) throws IOException { method.upload(ssh, src, dest); } /** * Tear down. * * @throws IOException */ @Override public void close() throws IOException { try { ssh.close(); } finally { ssh.disconnect(); } } /** * Transfer method strategy. * */ public static enum TransferMethod { SCP { @Override void download(SSHClient ssh, String src, File dest) throws IOException { ssh.newSCPFileTransfer().download(src, new FileSystemFile(dest)); } @Override void upload(SSHClient ssh, File src, String dest) throws IOException { ssh.newSCPFileTransfer().upload(new FileSystemFile(src), dest); } }, SFTP { @Override void download(SSHClient ssh, String src, File dest) throws IOException { ssh.newSFTPClient().get(src, new FileSystemFile(dest)); } @Override void upload(SSHClient ssh, File src, String dest) throws IOException { ssh.newSFTPClient().put(new FileSystemFile(src), dest); } }; /** * Download File. * * @param ssh * SSHClient * @param src * Source file(remote) * @param dest * Destination directory or file path(local) * @throws IOException */ abstract void download(SSHClient ssh, String src, File dest) throws IOException; /** * Upload File. * * @param ssh * SSHClient * @param src * Source file(local) * @param dest * Destination directory or file path(remote) * @throws IOException */ abstract void upload(SSHClient ssh, File src, String dest) throws IOException; } }
SSHClient から返る結果を格納するクラス (SSHResult.java)
package communicate.ssh; import lombok.Value; @Value public class SSHResult { /** * Exit status of command. */ public int status; /** * Output. */ public String output; /** * Error. */ public String error; }
設定ファイル (ssh.properties)
# Connection Settings ssh.host = [必須, IPアドレス、ホスト名どちらでも可] ssh.port = [必須] # User Authentication ssh.user = [必須] ssh.keyfile = [キーファイルのパスを指定、今回はOpenSSH形式のキーファイル対応] ssh.password = [パスワード、公開鍵認証なら空でも良い]
使い方
- ssh.properties を変更する
- ロジックを記述する
(Property クラス → プロパティファイルの使用方法 - Status Code 303 - See Other)
package communicate.ssh; import java.io.File; import java.io.IOException; import communicate.ssh.SSHClientFacade.TransferMethod; import resources.properties.Property; public class Main { public static void main(String[] args) throws IOException { Property property = new Property("ssh"); try (SSHClientFacade ssh = new SSHClientFacade()) { // 接続 ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port")); // 認証 ssh.authPublickey(property.getString("ssh.user"), new File(property.getString("ssh.keyfile"))); // ssh.authPassword(property.getString("ssh.user"), property.getString("ssh.password")); // コマンド実行 SSHResult result = ssh.execute("ls -al > test.txt"); System.out.println(result); // ファイルのダウンロード ssh.download("/home/pi/test.txt", new File("./sample"), TransferMethod.SCP); // SCP の場合 // ssh.download("~/test.txt", new File("./"), TransferMethod.SFTP); //SFTP の場合 // ファイルのアップロード ssh.upload(new File("./sample"), "/home/pi/", TransferMethod.SCP); // SCP の場合 // ssh.upload(new File("./test.txt"), "/home/pi/", TransferMethod.SFTP); // STFP の場合 } } }
注意点
リモート先のパスは、なるべくフルパスで記述した方が良い。不思議なことに、ヘテムルでは
download メソッドの引数ではホームディレクトリ(~/xxx/yyy) を解釈するが、upload 時は認識できないようで、
下記の例外が発生した。なお、フルパスに変更すると動作した。
Exception in thread "main" net.schmizz.sshj.xfer.scp.SCPRemoteException: Remote SCP command had error: scp: ~/xxx/yyy: No such file or directory
接続方法を様々に変更する拡張
今回は単純化のため、サーバへの接続方法が同じであると仮定して実装した。
しかし、様々なサーバに接続する際に、その接続設定が異なることもあるだろう。
これらは、SSHClient の設定を変更することで実現できる。
接続方法の追加
まず SSHClientFacade クラスに以下を追加する。
// アクセス方法に関する Strategy public static interface AccessStrategy { public abstract void execute(SSHClient ssh) throws IOException; } // connect メソッドを追加する public void connect(String host, int port, AccessStrategy method) throws IOException { method.execute(ssh); ssh.connect(host, port); }
そして、ロジックでは次のように使う想定。
ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port"), new AccessStrategy(){ @Override public void execute(SSHClient ssh) throws IOException { ssh.setConnectTimeout(60 * 1000); ssh.setTimeout(30 * 60 * 1000); ssh.loadKnownHosts(); ssh.addHostKeyVerifier(new PromiscuousVerifier()); } });
これで、サーバによって柔軟な設定が可能になり、SSHClient を次のメソッドを用いて設定する。
- addHostKeyVerifier
- addAlgorithmsVerifier
- loadKnownHosts
- useCompression
- setSocketFactory
- setTimeout
- setConnectTimeout
Facade を崩さない
このままではロジック部分が SSHJ を意識しなければならず、 SSHJ 特有の設定が SSHClientFacade 外部に漏れてしまう。
これでは、Facade を作った意味がない。なので、これらアクセス方法を定義した Strategy パターンを Enum 型で実装し、利用する。
実装一例。下記を SSHClientFacade に追加。
/** * Definition how to access to your server. * */ public static enum AccessStrategy { SERVER_A { @Override void execute(SSHClient ssh) throws IOException { ssh.setConnectTimeout(60 * 1000); ssh.setTimeout(30 * 60 * 1000); ssh.loadKnownHosts(); ssh.addHostKeyVerifier(new PromiscuousVerifier()); } }, SERVER_B { @Override void execute(SSHClient ssh) throws IOException { ssh.setConnectTimeout(10 * 1000); ssh.setTimeout(10 * 60 * 1000); ssh.loadKnownHosts(); } }; /** * write your settings. * @param ssh * @throws IOException */ abstract void execute(SSHClient ssh) throws IOException; } /** * Access server with your customize settings. * @param host * @param port * @param method * @throws IOException */ public void connect(String host, int port, AccessStrategy method) throws IOException { method.execute(ssh); ssh.connect(host, port); }
ロジック部の使用例
ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port"), AccessStrategy.SERVER_A);
SSH はセキュリティ上重要なため、ライブラリ開発者の都合によっては、今後取り替えることを想定すべきかもしれない。
もしそうであったとしても、おそらくサーバの接続方法や設定は概念的には変わらない。
このように設計しておけば、ロジックの変更は SSHClientFacade クラスを変更するだけなので、取り替えは容易になる。
(またこのメソッドを直接ユーザに使わせないよう、ストラテジーのメソッドアクセスレベルはパッケージにしている)
ただし、この拡張はコードが複雑になるため、本当に必要なときだけ行った方が良い。
また、ここでは触れないが、認証方法に関しても様々なものが用意されているため、
本家のリファレンスは見ておくといいかもしれない。
C言語 の long int データ型仕様
概要
今回、開発環境でテストすると正しいが、本番環境では正しい結果が出力できなかった。
あまりにバカらしい理由ではあるが、戒めのために記述する。
起こった事
問題箇所
ある計算結果を出力するのだが、その結果は結構大きな値になってしまうので、
結果を格納するデータの型に long int を使用。そして、結果の表示において最初以下のように記述していた。
long int result = (計算部); printf("%d", result); // 588491482334(期待値)
すると、出力結果が明らかに小さい。
80962782
そして、自分が フォーマッタを『%d』にしていたミスに気付く。
『%d』→ 『%ld』に修正して、テスト。
long int result = (計算部); printf("%ld", result); // 588491482334(期待値)
出力結果が期待されるものになった。
588491482334
これで問題ないと思い、本番環境に送信して実行。その出力結果。
80962782
なぜか出力が戻った。送信したプログラムを確認したが問題ない。
問題解決フェーズ
計算結果を小さくしてみると正しい値が表示されるため、この現象は計算結果が大きくなった時のみ起こる。
しかし、フォーマッタは『%ld』を指定しているので、問題ないはずだが・・。
試しに『%lu』に変更してみたが効果なし。
今は C 言語の仕様が変わったのか?と思い、C 言語の仕様書を漁るもどこも変更なさそう。
仕方がないので本番環境で試しに、『LONG_MAX』(limits.h)を出力してみた。
2147483647
この瞬間、計算期待値がlong int の表現範囲を超えてることに気づく。
そこで、下記のように修正。
long long int result = (計算部); printf("%lld", result); // 588491482334(期待値)
本番環境でも動作を確認。こんな単純な問題に2時間掛けてしまった。
C 言語のデータ型仕様
この問題の要因を作った、long int の表現範囲を調べる。
まず、一般的には(データ型を参照)
int → 2byte(-32768~32767)を記憶できる
long int → 4byte (-2147483648~2147483647)を記憶できる
long long int → 8byte (-9223372036854775808~9223372036854775807)を記憶できる
まず、ここで既に勘違い。
long int が 8byte だと思ってた。(Java の long 型が 8byte だから)
(付け加えるなら long long int は 16byte だと思ってた)
そして、仕様は結構アバウトであり、基本的に下限だけ決まっている。
つまり、byte 数が大きい分には構わない。実際、私の開発環境では
#include<stdio.h> #include<limits.h> int main() { printf("INT_MAX:\t%d\n", INT_MAX); printf("LONG_MAX:\t%ld\n", LONG_MAX); printf("LLONG_MAX:\t%lld\n", LLONG_MAX); return 0; }
その結果。
INT_MAX: 2147483647 LONG_MAX: 9223372036854775807 LLONG_MAX: 9223372036854775807
long int は 8byte だった。しかも、 long long int とデータサイズ同じだった。
普段そんなに大きな値を使わないから、全く意識できていなかった。
反省。
結論
基本的なことではあるけれど、データ仕様は正しく覚える。
環境によってデータサイズが異なる事にも留意する。
各ケースのデータ選択には分岐より規約に基づいた処理を適用する
概要
分岐処理は、手軽に記述できるため、多くの場面で利用される。
しかし、あるデータの値によって、途中のデータを選択する処理を分岐を用いて
実装してしまうと、全てのケースをカバーしなければならないため、
新たなケースの追加や既存のケースの修正によって、プログラムを修正する必要がある。
もし、これら各ケースをプログラムでは同じ処理だと認識できれば、
ケースの追加やケースの修正を感知しないため、もっと保守性を向上できる。
今回はこれについて説明する。
詳細説明
今回は、会員のカテゴリ(VIP, 会員, 無料会員)があり、
これらのカテゴリ毎にメッセージを別々に表示したいとしよう。
まずは、改善の余地がある処理を提示する。
package sample; import resources.properties.Property; public class Main { public static void main(String[] args) { Property property = new Property("message"); String title, message; // ここで会員カテゴリを取得するとする. String category = getMemberCategory(); // ここでデータを選択する. if(category.equals("A")) { title = property.getString("vip.title"); message = property.getString("vip.message"); }else if(category.equals("B")) { title = property.getString("member.title"); message = property.getString("member.message"); }else if(category.equals("C")) { title = property.getString("normal.title"); message = property.getString("normal.message"); }else { throw new IllegalStateException("カテゴリ名が不正です"); } // データを用いた処理. System.out.println("title: " + title); System.out.println("message: " + message); } // コンパイラを通すために仮の値を作成. private static String getMemberCategory() { return "B"; } }
なお、property クラスは、message.properties からキーに対応した値を取得するクラスである。
以下の過去記事にその実装例がある。
プロパティファイルの使用方法 - Status Code 303 - See Other
コンストラクタでリソースファイル名を指定できる実装にしているが、基本的に動作は同じ。
そして、タイトルやメッセージを格納した message.properties はこちら。
vip.title = VIP会員様へ vip.message = 今お買い物頂くと 40% のポイントが溜まります。 member.title = 会員様へ member.message = 今お買い物頂くと 20% のポイントが溜まります! normal.title = 無料会員様へ normal.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!
タイトルとメッセージを外出しファイルにすることで、内容の変更は properties ファイルの
更新だけになり、プログラムを修正する必要がないようにしている。
しかし、もうちょっと保守性を向上できる。
今回、ネックとなっているのは分岐の部分だ。
分岐を用いた、このような実装は自然に映るかもしれないが、
会員カテゴリが追加、変更された場合は、以下を変更しなければならない。
- プログラム(if 文の分岐部分)
- リソースファイルの xxx.title, xxx.message キーの追加
本来、プログラムは処理を記述するものであるから、データ追加の影響は受けたくない。
タイトルとメッセージだけを追加すれば動作するようにしたい。
これを実現するには、下記のように message.properties のキー名を
カテゴリに対応したキー名にし、それを処理で取得するようにする。
今回の例では、次のようにする。
- [カテゴリ名] + ".title" の値をタイトルに表示する
- [カテゴリ名] + ".message" の値を本文に表示する
このように、リソースデータの記述方法を規約化してしまうことによって、
各ケースに対する処理をまとめてしまう。
package sample; import java.util.MissingResourceException; import resources.properties.Property; public class Main { public static void main(String[] args) { Property property = new Property("message"); String title, message; // ここで会員カテゴリを取得するとする. String category = getMemberCategory(); try { // ここでデータを選択する. title = property.getString(category + ".title"); message = property.getString(category + ".message"); } catch(MissingResourceException e) { throw new IllegalStateException("カテゴリ名が不正です"); } // データを用いた処理. System.out.println("title: " + title); System.out.println("message: " + message); } // コンパイラを通すために仮の値を作成. private static String getMemberCategory() { return "B"; } }
message.properties は以下のようにキー名を変更する。
# 存在するカテゴリ名 + .title, + .message をデータとして取得する A.title = VIP会員様へ A.message = 今お買い物頂くと 40% のポイントが溜まります。 B.title = 会員様へ B.message = 今お買い物頂くと 20% のポイントが溜まります! C.title = 無料会員様へ C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!
このようにすれば、もし、カテゴリが追加されたとしても、message.properties に
新しいカテゴリ "D" に対する title と message を追加すれば、処理は変更しなくて良くなる。
# 存在するカテゴリ名 + .title, + .message をデータとして取得する A.title = VIP会員様へ A.message = 今お買い物頂くと 40% のポイントが溜まります。 B.title = 会員様へ B.message = 今お買い物頂くと 20% のポイントが溜まります! C.title = 無料会員様へ C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます! # 追加 D.title = お試し会員様へ D.message = お試し期間中は全サービスが使えますが、有効期間は1週間です!
その出力結果。(getMemberCategory() で "D" を返すように変更)
title: お試し会員様へ message: お試し期間中は全サービスが使えますが、有効期間は1週間です!
今回は、簡単のため、同一 properties ファイル内にデータを記述しているが、
カテゴリによって変更されるデータの数が多くなればなるほど、大量のデータで混沌とする。
この場合は、読み込むリソースファイル自体を変更する。
例えば カテゴリが "A" の場合、message_A.properties を読み込むようにする。
注意点として、この方法は各ケースの異なる部分が処理データだけだということ。
もし、各ケースの処理自体が異なっている場合は、単純にこの方法は適用できない。
もし、データ選択とそのデータの処理がコード的に分割できるのであれば、
まずそれを行ってから、上記変形を行う。
結論
データの値によって、処理されるデータを変更したい場合は、
リソースファイルに規約を適用した処理を記述することによって、
ケースの追加や変更にリソースファイルだけで対応できるようになる。
しかし、各ケースによって処理自体が異なってしまう場合は、単純に適用できない。
もし、これらからデータ選択部と処理部が分割できるなら、それをまず行う。
テンプレートによるロジックとデザイン切り分け
概要
デザインとロジックを切り分ける内容。
本来これらは全く別個に管理すべきなものであるため、分離させたい場合が多くある。
今回は、Java でこの方法を実現する手順を記述。
デザインとロジックの混在する悪い例
public class Main { public static void main(String[] args){ String user = "hoshi-kouki"; boolean isMember = true; Cart cart = new Cart(); cart.add(new Item("タマネギ", 100)); cart.add(new Item("じゃがいも", 150)); cart.add(new Item("人参", 100)); cart.add(new Item("肉", 500)); System.out.println(user + " 様"); System.out.println(); System.out.println("以下商品を購入されたことを通知します。"); System.out.println("==========================="); for(Item item : cart.getItems()) { System.out.println(item.getName() + " " + item.getPrice() + " 円"); } System.out.println("--------------------------"); System.out.println("合計 " + cart.sum() + " 円"); System.out.println("--------------------------"); if (isMember) { System.out.println("いつもお買い上げ頂きありがとうございます!"); }else { System.out.println("会員登録するとポイントがもらえますよ!"); } } }
まずい理由
- ロジックとデザインが混在するため、どちらも内容が分かり辛い。
- デザイン変更→ロジック破壊、ロジック変更→デザイン破壊が起こる可能性
- デザインを管理すべき人が自由に変更できない
- デザイン担当する人がプログラマと同じことができるとは限らない
本来あるべき姿
- デザインを担当する人は、その人の好きなツールで作成
- そのデザインを内部に取り込む仕組みはプログラマが作成
テンプレートの仕組み作成
Velocity - Velocity User Guide を用いたサンプルを記述する。
インストール手順は、ここ参照(http://hack.aipo.com/archives/8360/)。
事前設定
詳細説明
今回実装したテンプレート利用クラス(OutputBuilderクラス).
import java.io.StringWriter; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.Velocity; /** * 既に定義されたテンプレートファイルからプログラム上で様々な値を * セットして処理に応じた結果を作成する。 */ public class OutputBuilder { /** * ファイル名. */ private String fileName; /** * Velocity コンテキスト. */ private VelocityContext context; /** * コンストラクタ. * @param fileName ファイル名 */ public OutputBuilder(String fileName) { this.fileName = fileName; context = new VelocityContext(); } /** * Velocityコンテキストにパラメータを設定する. * @param key キー名 * @param value 値 * @return 呼び出したテンプレートビルダー自身 */ public OutputBuilder set(String key, Object value) { context.put(key, value); return this; } /** * 格納した結果から文字列を作成する. * @return 文字列 */ public String build() { Velocity.init(); Template template = Velocity.getTemplate(fileName, "UTF-8"); StringWriter writer = new StringWriter(); template.merge(context, writer); return writer.toString(); } }
今回は最小構成で作るため最低限のみ。機能追加は本家ドキュメントを参考に。
使い方
請求書を作成するとして記述する。
- 必要データ洗い出し
請求書に必要なデータは、今回は以下とする。- 購入者
- 購入商品・その値段
- 合計金額
- 購入者が会員/非会員かどうか
- Value Object(VO)用意
基本データ(文字・数値)のみで構成しても動作するが、モデル化クラスを利用した方が分かりやすくなる。- 商品の属性情報を管理するクラス。(Itemクラス)
- 商品自体を管理するクラス (Cartクラス)
- 各クラス実装
Itemクラス/** * 商品情報を管理する. */ public class Item { /** * 商品名. */ private String name; /** * 単価. */ private int price; /** * コンストラクタ. * @param name 商品名 * @param price 単価 */ public Item(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public int getPrice() { return price; } }
Cartクラス
import java.util.ArrayList; import java.util.List; /** * 購入商品を管理する. */ public class Cart { /** * 購入商品. */ private List<Item> items; /** * コンストラクタ. */ public Cart() { items = new ArrayList<>(); } /** * 商品リストを取得する. * @return 商品リスト */ public List<Item> getItems() { return items; } /** * 商品を items に追加する. * @param item 追加する商品 */ public void add(Item item) { items.add(item); } /** * 商品の合計金額を取得する. * @return 金額 */ public int sum() { int sum = 0; for(Item item : items) { sum += item.getPrice(); } return sum; } }
- テンプレート(bill.vm)作成
$name 様 以下商品を購入されたことを通知します。 =========================== #foreach ($item in $cart.getItems()) $item.getName() $item.getPrice() 円 #end -------------------------- 合計 $cart.sum() 円 =========================== #if($isMember) いつもお買い上げ頂きありがとうございます! #else 会員登録するとポイントがもらえますよ! #end
今回は紹介のため様々なものを使っているが、
デザインの全体像が分かり辛くなるため分岐(#if ~ #else ~ #end)はあまり使わない方が良い。なお、上記にある特殊な意味を持つ文字について説明すると。
$name
setメソッドで渡したキー名'name'に対応した値を表示。
#foreach ($item in $cart.getItems()) (処理) #end
(処理)を集合個数($cartにはいったアイテム分)実行する。
各繰り返し処理において、カートの中の各要素がループごとに $item にセットされ、処理に利用できる。#if($isMember) (処理1) #else (処理2) #end
条件によって、(処理1)か(処理2)の結果が表示。
- ロジック記述
ロジックにテンプレートに対する処理を記述。- 使用するテンプレート指定 (OutputBuilder コンストラクタ)
- $name, $isMember, $cart に使用するデータ設定(setメソッド)
- テンプレートの指定箇所にロジックで得たデータを組み合わせた結果を得る(build メソッド)
public class Main { public static void main(String[] args) { Cart cart = new Cart(); cart.add(new Item("タマネギ", 100)); cart.add(new Item("じゃがいも", 150)); cart.add(new Item("人参", 100)); cart.add(new Item("肉", 500)); OutputBuilder template = new OutputBuilder ("bill.vm").set("name", "hoshi-kouki").set("isMember", true).set("cart", cart); System.out.print(template.build()); } }
出力結果は以下。
hoshi-kouki 様 以下商品を購入されたことを通知します。 =========================== タマネギ 100 円 じゃがいも 150 円 人参 100 円 肉 500 円 -------------------------- 合計 850 円 =========================== いつもお買い上げ頂きありがとうございます!
bill.vm の所定の箇所にデータが埋め込まれた形で出力できていることが分かる。
- ロジック記述
参考文献
Start up the Velocity Template Engine | JavaWorld
http://hack.aipo.com/archives/8360/
更新履歴
2016/01/20 変な表現多すぎたので、記事修正。
TemplateBuilderクラス → OutpuBuilderクラス(テンプレート作るわけじゃないし)
2016/06/13 説明が冗長すぎて分からなかったので修正。
Lombok ライブラリ - フィールド関連
主記事
Lombok ライブラリ - Status Code 303 - See Other
この記事では、フィールドに定義可能なアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)
(更新中)
アクセッサ自動生成
指定されたフィールドのアクセッサを提供する。
@Getter
どのようなフィールドに対しても、Lombok が標準的なゲッタを提供する。
標準的なゲッタとは、おそらく以下の内容だと思われる。
- JavaBeansの命名規則を満たすゲッタ
- 処理はフィールドをそのまま返す
例:
private @Getter int foo;
上記を設定することにより、下記のコードが生成される。
public int getFoo() { return this.foo; }
オプション
- lazy(デフォルト:false)
- 明示的に書かれていない。遅延初期化?
- onMethod
- Lombok により生成されたゲッタに設定するアノテーションをリスト形式で記述する。
- value(デフォルト:public)
- アクセスレベルをゲッタに設定する。
@Setter
指定されたフィールドにセッタを提供する。
オプション
- lazy(デフォルト:false)
- 明示的な説明がない。遅延初期化?
- onMethod
- Lombok により生成されたセッタに設定するアノテーションをリスト形式で記述する。
- value(デフォルト:public)
- アクセスレベルをセッタに設定する。
コレクションフィールドに対するデータ格納メソッドの自動生成
null チェックの自動生成
@NonNull
Lombok から生成された全てのメソッドにおいて、該当のフィールドに値を割り当てる処理をする前に nullチェックを行う。
もし、 割り当てる値が null だったなら NullPointerException をスローする。
オプション
なし。
Lombok ライブラリ - その他
主記事
Lombok ライブラリ - Status Code 303 - See Other
この記事では、クラスとフィールド以外に関連するアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)
(更新中)
データ同期
型推論
val
ローカル変数にのみ使用可能であり、初期化の式から型を推測する。
また、val を定義したローカル変数は、final になる。
例:
val x = 10.0; val y = new ArrayList<String>();
と記述すれば、 val は以下と同等とみなされる。
final double x = 10.0; final ArrayList<String> y = new ArrayList<String>();
なお、あまりそうは見えないかもしれないが val はアノテーション型である。(正確にはシンタックスシュガーらしい)
なぜなら、 val x = 10.0; と記述すれば、Lombok は @val final int x = 10; にコードを変換するから。
詳細はこちら(val)
オプション
なし。
ツールに対する適用外設定
@Generated
いずれは、Lombok が生成したメソッドやクラスに自動的に付加するアノテーション。
全てのコードスタイルチェックツールやバグ発見ツールに対して、これらのコードを無視させるために使用する。
オプション
なし。
メソッドパラメータの null チェック自動生成
リソース解放処理の自動生成
@Cleanup
何が起こったかに関係なく、ローカル変数がそれらの close メソッド呼び出しによってリソースの解放を保証する宣言。
ローカル変数の有効範囲にある全ての命令文を包括した try 文を実装することにより、リソースを解放する。
詳細なドキュメントこちら(@Cleanup)。
例:
public void copyFile(String in, String out) throws IOException { @Cleanup FileInputStream inStream = new FileInputStream(in); @Cleanup FileOutputStream outStream = new FileOutputStream(out); byte[] b = new byte[65536]; while (true) { int r = inStream.read(b); if (r == -1) break; outStream.write(b, 0, r); } }
これらの宣言が Lombok によって以下の処理に変換される。
public void copyFile(String in, String out) throws IOException { @Cleanup FileInputStream inStream = new FileInputStream(in); try { @Cleanup FileOutputStream outStream = new FileOutputStream(out); try { byte[] b = new byte[65536]; while (true) { int r = inStream.read(b); if (r == -1) break; outStream.write(b, 0, r); } } finally { if (out != null) out.close(); } } finally { if (in != null) in.close(); } }
チェック例外 → 実行時例外変換
@SneakyThrow
Java では、メソッド処理内にチェック例外を生成し得る命令が記述された場合、その例外に対処しなければならない。
(try-catch 文で例外をキャッチするか、メソッド呼び出し元に伝播させるなら throws 宣言を使ったりする)
しかし、このアノテーションをメソッドに記述すると、本来行うはずのチェック例外に対処しなくても良くなる。
このアノテーションは、内部的にオプション(value)に指定されたチェック例外(複数可)を
RuntimeException もしくは他の実行時例外にラップしている。こうして、チェック例外が実行時例外内に隠れるため、
本来 JVM が行うはずだった、チェック例外における一貫性チェックを回避している。
例:
下記のバイト列を特定の文字コードから String インスタンスを生成するコンストラクタはチェック例外である
UnsupportedEncodingException をスローするため、本来は何かしら対処をしなければならない。
しかし、以下の例では、これを省略してもプログラムは正常に動作する。
@SneakyThrows(UnsupportedEncodingException.class) public void utf8ToString(byte[] bytes) { return new String(bytes, "UTF-8"); }
これは、Lombok が上記コードを内部的に下記のように変換しているから。
public void utf8ToString(byte[] bytes) { try { return new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException $uniqueName) { throw useMagicTrickeryToHideThisFromTheCompiler($uniqueName); // This trickery involves a bytecode transformer run automatically during the final stages of compilation; // there is no runtime dependency on lombok. } }
なお、元のコードと同じ動きを保証するために発生する条件では、本来スローされるべき例外がスローされる。
import java.text.SimpleDateFormat; import lombok.SneakyThrows; public class Main { @SneakyThrows public static void main(String[] args) { System.out.println(new SimpleDateFormat("yyyyMMdd").parse("aaaaa")); // 意図的に例外(ParseException)を発生させる } }
出力結果。
Exception in thread "main" java.text.ParseException: Unparseable date: "aaaaa" at java.text.DateFormat.parse(DateFormat.java:366) at Main.main(Main.java:8)
詳細はこちら(@SneakyThrows)
Lombok ライブラリ - クラス関連
主記事
Lombok ライブラリ - Status Code 303 - See Other
この記事では、クラスに定義可能なアノテーションを記述。
(記事執筆時のバージョン :v1.16.6)
クラス性質
ある性質を有するクラス構造を提供する。
@Data
全てのフィールドに対するゲッタ、全ての transient ではないフィールドを利用した便利な toString, equals, hashCodeメソッドを提供する。
また、全ての final ではないフィールドに対するセッタと必要パラメータ用コンストラクタを提供する。
オプション
- staticConstructor
- 生成インスタンスを指定した名前のstaticメソッドでのみ提供できる。
import lombok.Data; import lombok.NonNull; @Data(staticConstructor="of") public class Book { private final String id; // final なので引数に追加される @NonNull private String title; // @NonNull なので引数に追加される private String author; private int page; public static void main(String[] args) { // 使用方法 Book b = Book.of("ID", "Title"); } }
@Value
不変クラスの表現に合致したコードを生成する。具体的には、下記がクラスに設定される。
オプション
- staticConstructor
- (@Data参照)
注釈
フィールド宣言に final がなくても、Lombok による自動生成コード上では
final が付与されるため以下のコードは、コンパイルエラーとなる。
import lombok.Value; @Value public class Book { private String id; // 宣言は final ついていない private String title; private String author; private int page; public static void main(String[] args) { Book b = new Book("ID", "Title", "Author", 5); b.author = "AAA"; //コンパイルエラー finalなので変更できない } }
また、不変クラスとして扱うとあるが、フィールドに可変インスタンスが存在していた場合、
ゲッタはその参照を返してしまうので、そのフィールドの内部は変更されることに注意する。
@Builder
「指定したクラス」もしくは「指定されたメンバーメソッドを含むクラス」に対して、
いわゆる Builder パターン機能を付加する。
指定可能な要素
なお、既にクラスに private な全フィールドのパラメータを持つコンストラクタが存在しても、上記が作成される。
指定クラスの影響
private コンストラクタをメンバーに持つ「(指定クラス名)Builder」という
インナークラス(以後 Builder クラス)が生成される。
Builder クラスのインスタンス(以後 Builder インスタンス)を生成するメソッドとして、
static の builder メソッドが生成される。
なお、Builderクラスのインスタンスメソッドに似たものがあるが、
こちらは「 build 」メソッド( er がない)で全くの別もの。(詳細は下記)
Builder クラスの構成
- 指定クラスのコンストラクタ / static メソッドの各パラメータに相当するフィールドを持つ。
- インスタンスメソッドに生成フィールド名と同じものが生成され、それらはパラメータを自身に格納した後インスタンス自身を返す。
- 上記メソッドから得たパラメータを指定クラスの private コンストラクタ / static メソッドに渡し、完全に初期化された指定クラスのインスタンスを返す。
なお、上記 builder メソッドの返す型は、static メソッドに指定されていなければ、指定クラスの型が返る。
詳細な説明はこちら。(@Builder)
例:
@Builder class Example { private int foo; private final String bar; }
このように設定したクラスは Lombok によって次のように変換される。
class Example<T> { private T foo; private final String bar; private Example(T foo, String bar) { this.foo = foo; this.bar = bar; } public static <T> ExampleBuilder<T> builder() { return new ExampleBuilder<T>(); } public static class ExampleBuilder<T> { private T foo; private String bar; private ExampleBuilder() {} public ExampleBuilder foo(T foo) { this.foo = foo; return this; } public ExampleBuilder bar(String bar) { this.bar = bar; return this; } @java.lang.Override public String toString() { return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")"; } public Example build() { return new Example(foo, bar); } } }
オプション
- builderClassName(デフォルト:builder)
- ビルダークラス名を変更する。デフォルトでは、@Builder 指定クラスとコンストラクタから「(指定クラス)Builder」という名前になる。
- builderMethodName
- Builder インスタンスを作成する static のメソッド名を変更する。
- buildMethodName(デフォルト:build)
- @Builder が指定されたクラスのインスタンスを作成する Builder クラスのメソッド名を変更する。
- toBuilder(デフォルト:false)
- @Builder が指定されたクラスのフィールドに Builder インスタンスの初期化に関わるデータがあり、そのデータから Builder インスタンスを生成するインスタンスメソッドを生成する必要がある場合は true を指定する。
なお、このオプションが指定可能なのは、
の場合である。
コンストラクタ自動生成
コンストラクタを自動生成するアノテーションについて記述する。
@AllArgsConstructor
全てのフィールドに対する初期化データを必要とするコンストラクタを提供する。
ユーティリティメソッド自動生成
@ToString
関連フィールドから構成される、継承クラスによって構成される toString メソッドを実装する。
詳細ドキュメント(@ToString)。
オプション
- callSuper(デフォルト:false)
- 生成された toString メソッドに、スーパークラスのメソッド出力を含める場合。
- doNotUseGetters(デフォルト:false)
- 生成された toString メソッドにフィールドを直接参照させる場合。 デフォルトでは、ゲッタが利用可能であれば、それを通じてアクセスする。
- exclude
- 生成される toString メソッドの出力に特定フィールドを除外したい場合。of とは一緒に記述できない。
- includeFiledNames(デフォルト:true)
- 生成される toString メソッドの出力にフィールド名を含める場合。
- of
- 生成される toString メソッドの出力に含めるフィールドを明示的に設定したい場合。exclude とは一緒に記述できない。
@EqualsAndHashCode
関連フィールドに基づいた、全ての継承クラスに対する equals と hashCode メソッドを生成する。
オプション
- callSuper(デフォルト:false)
- このクラスのメソッドを実行する前に、スーパークラスのequals/hashCodeメソッドを呼び出す。
- doNotUseGetters(デフォルト:false)
- 生成メソッドにフィールドを直接参照させる場合には true を設定する。 デフォルトでは、ゲッタが利用可能であればそれを通じてアクセスする。
- exclude
- 生成される equals/hashCodeメソッドの同値条件から除外したいフィールドを指定する。of とは一緒に記述できない。
- onParam
- 生成される equals/hashCodeメソッドにアノテーションを設定したい場合指定する。
- of
- 生成された equals/hashCodeメソッドの同値条件に含めるフィールドを明示的に指定する。 デフォルトでは、全てのstatic, transient でないフィールドが使われ、exclude とは一緒に記述できない。
アクセッサ自動生成
@Getter
クラス内の @Getter が既に設定されていない static でないフィールドに対して、ゲッタを提供する。
詳細は、フィールド関連 - @Getterを参照。
オプション
フィールドと異なる点として onMethod は使えない。
- lazy
- フィールド関連 - @Getterを参照
- value
- フィールド関連 - @Getterを参照
@Setter
クラス内の static でなく @Getter も設定されていないフィールドに対してセッタを提供する。
オプション
フィールドと異なる点として onMethod は使えない。
- lazy
- フィールド関連 - @Setterを参照
- value
- フィールド関連 - @Setterを参照