読んだ : RESTful Web Services with Dropwizard / Alexandros Dallas 著
Dropwizard に関わる仕事をしているので読んでみました。
RESTful Web Services with Dropwizard
- 作者: Alexandros Dallas
- 出版社/メーカー: Packt Publishing
- 発売日: 2014/02/19
- メディア: Kindle版
- この商品を含むブログを見る
Dropwizard について
Dropwizard は Java の web アプリケーションフレームワークです。 基本的には既存ライブラリの組み合わせで web アプリケーションを構築するというもので、Dropwizard 固有の仕組みはさほど多くありません。 (例えば Web リクエストを受け取るのは Jersey で、DB アクセスには Hibernate か jDBI が使われる。)
特に Java EE 系の知識を持っている人であれば、とっつきやすい感じです。
本書について
本書は、Dropwizard を使って web アプリケーションを構築するための方法を説明するものです。 プロジェクトの準備や、HTTP リクエストを受けるエンドポイントの記述、DB アクセス、ユーザー認証、HTML を返す View テンプレートについてなど、基本的な要素について、サンプルコードを交えながら仕組みが解説されます。
Java EE などについてある程度わかっている人が読むと Dropwizard の公式ドキュメントを読むのと大差ないと思いますが、初心者の人が読むと結構わかりやすいんじゃないかなと思います。 (ある程度わかってる人にとっても、Dropwizard についてざっと知ることができて良いかもしれませんが、そういう使い方だとちょっと値段は高めに感じる気がします。)
本書での学び
参考のリンクとしては現時点での最新バージョンのドキュメントへのリンクです。 閲覧時の最新バージョンのドキュメントは各自探してください。
- Dropwizard では、maven-shade プラグイン を使って、単体で web アプリケーションとして実行可能な JAR ファイル (uber-jar) を作る。
- Hibernate Validator によるアプリケーション設定のバリデーションが可能。
- jDBI で DB から取得した結果をオブジェクトにマップする方法として、
@MapResultAsBean
アノテーションを使うという方法もある。- 参考 : MapResultAsBean (jDBI 2.48.2 API)
- とはいえ公式的なドキュメントは何もなく、ドキュメント化されていない挙動に依存することになるので不安。 (JDBI の機能の多くがドキュメント化されてないのでまあこれに限った話ではないのだけど。)
- HTTP リクエストパラメータのバリデーション周り。
- JAX-RS のリソースメソッドのパラメータのバリデーションを明示的に実行することも可能。 (
@Valid
アノテーションでのバリデーション実行しか知らなかった。) - 複数フィールドにまたがるバリデーション。
- 参考 : Dropwizard Validation | Dropwizard
- JAX-RS のリソースメソッドのパラメータのバリデーションを明示的に実行することも可能。 (
- Dropwizard には HTTP クライアント用モジュールも含まれている。
- 認証周り。
- Basic 認証用のクラスが用意されている。
- オプションの認証も可能。 (認証されたユーザーの場合はそのユーザー専用のコンテンツを表示し、さもなければ一般ユーザー向けのコンテンツを表示する、みたいな。)
CachingAuthenticator
によるキャッシング。- 参考 : Dropwizard Authentication | Dropwizard
- Fixtures for Easy Software Testing (FEST) というプロジェクトがある。 TestNG や JUnit と一緒に使える。 ソフトウェアテストを書きやすくするライブラリ。
感想
Dropwizard をそこそこ使ってて公式ドキュメントも (全部ではないけど) 読んでいたので、本書での学びはあんまりなかったです。 とはいえ上に書いたように新たに知れたこともいくつかあったので読んでよかったです。 (そんなに時間もかけずにざっと読めましたし。)
とはいえ紙の本だと 30 ドル以上するので、ちょっと高い感じはしますね。
Dropwizard + JDBI で SQL オブジェクトの返り値に Optional を使うときには SingleValueResult アノテーションが必要
Dropwizard で SQL ライブラリ JDBI を使うときのおはなし。
OptionalContainerFactory
dropwizard-jdbi ライブラリは OptionalContainerFactory
クラスを提供してくれていて、JDBI の SQL オブジェクトで返り値に Java 8 で導入された Optional
指定することができます。
ちなみに普通に dropwizard-jdbi の DBIFactory#build
メソッドを使うと自動的に OptionalContainerFactory
を DBI
に登録してくれるので、自分で登録する必要はありません。 (Dropwizard 1.0.5 で確認。)
SingleValueResult
アノテーションが必要 (単に Optional
を指定するだけでは動かない)
SQL オブジェクトのクラス定義で返り値に Optional
を指定すればそれだけで動くのかと思いきや、実はそんなことはありません。 Optional
を返すメソッドに SingleValueResult
アノテーションを付ける必要があります。
import info.vividcode.app.web.example.dropwizard.domain.Person; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; import java.util.Optional; public interface PersonDao { @SqlQuery("SELECT * FROM person LIMIT 1") @SingleValueResult(Person.class) Optional<Person> findOne(); }
ここら辺のドキュメントがないので、ソースコードを読んで確認しました。
- 返り値に応じた
ResultReturnThing
を生成する箇所 : jdbi/ResultReturnThing.java at 3fe1480fcc3c42be94d355f8c7335bd784dbbc13 · jdbi/jdbi · GitHub SingleValueResult
アノテーションがあるかどうかを検査している箇所 : jdbi/ResultReturnThing.java at 3fe1480fcc3c42be94d355f8c7335bd784dbbc13 · jdbi/jdbi · GitHub
Optional
を使う場合に限らず、Iterable
じゃなくてコンテナを使いたい場合は SingleValueResult
アノテーションを付ける必要がありそうですね。
関連ページ
MySQL Connector/J 5.1 系では useLegacyDatetimeCode=false にしよう
JDBC で MySQL に接続するときに使用する MySQL Connector/J (mysql:mysql-connector-java) の話。 サーバー・クライアントのタイムゾーン設定が違っている場合にどう対応するのがいいか。
結論
- MySQL Connector/J 6 (まだ開発版だけど) 以降は自動でやってくれるので気にする必要はない。
- MySQL Connector/J 5.1 では URL に
useLegacyDatetimeCode=false
を入れて、時刻周りの新しい処理が動くようにしろ。- 新しい処理では、タイムゾーンの変換を一貫性をもってやってくれるようになる。
- 『Use code for DATE/TIME/DATETIME/TIMESTAMP handling in result sets and statements that consistently handles time zone conversions from client to server and back again』
- 参考 : 5.1 Driver/Datasource Class Names, URL Syntax and Configuration Properties for Connector/J
- 5.1 系ではデフォルトでは互換のために新しい処理は動かないようになっているので、明示的に新しい処理を使うように URL で指定する必要がある。
- バージョン 5.1.6 で導入された機能なので、それより古いものでは使えない。
問題
そもそもどういう問題に遭遇したのか。
- Java のアプリケーションサーバーのタイムゾーンが JST。
- MySQL サーバーのタイムゾーンが UTC。
- Java アプリケーションから MySQL サーバーには MySQL Connector/J 5.1 系で接続。
- タイムゾーン周りのオプションは何も指定せず。
- SQL 文の
NOW()
関数やDEFAULT CURRENT_TIMESTAMP
で設定された時刻を Java アプリケーション側で取得すると、現在時刻から 9 時間前の時刻が返ってきた。 - → MySQL Connector/J がサーバー・クライアント間のタイムゾーン差を扱ってくれてない。
MySQL Connector/J とタイムゾーン
- もともとは
useTimezone
プロパティやserverTimezone
プロパティを使って対応する必要があった。 - MySQL Connector/J 5.1.6 で時刻周りの処理が書き直されて、
useLegacyDatetimeCode=false
することでタイムゾーン変換などを自動で扱ってくれるようになった。 - MySQL Connector/J 6 では
useLegacyDatetimeCode
プロパティを含め、古いタイムゾーン周りのプロパティは全部削除される。
おわり
タイムゾーンはライブラリ側がちゃんと面倒見てくれるだろう、と思って気にしなかったら、環境を変えて Java アプリケーションと MySQL サーバーのタイムゾーン設定がずれたときにいきなり想定しない動作になったりするので気を付けましょう。
ISO 8601 DateFormat 1.0.0 (Java 向けライブラリ) をリリースしました
2016 年 5 月 3 日に ISO 8601 DateFormat の 1.0.0 をリリースしました。 ISO 8601 形式 (もしくは RFC 3339 や W3C-DTF 形式) の日付時刻文字列のパースとフォーマットのための DateFormat
のサブクラスを提供するライブラリです。
Bintray の JCenter リポジトリで公開しています。
現在は、時刻オフセット付きの拡張形式の日付時刻文字列のみをサポートしています。
- 2016-01-01T00:30:21Z
- 2016-01-01T09:30:21+09:00
動機
Java では、ISO 8601 形式の日付時刻文字列をパースする方法がいろいろあります。 例えば、SimpleDateFormat
で “yyyy-MM-dd'T'HH:mm:ssX
” というフォーマットを使う (Java SE 7 以降) とか、Date and Time API (JSR 310; Java SE 8 以降) を使うとか、Joda-Time ライブラリを使うとか、Apache Commons Lang ライブラリを使うなどです。
しかし、Java SE 6 環境や Android プラットフォームでは、大きなライブラリを導入することなく ISO 8601 形式の文字列をパースすることが簡単ではありませんでした。 そのため、このようなライブラリを公開しました。
使い方
Gradle を使っている場合、以下のようにリポジトリと依存を追加します。
repositories {
jcenter()
}
dependencies {
compile 'info.vividcode:date-format-iso8601:1.0.0'
}
あとは、以下のように使うだけです。
import info.vividcode.time.iso8601.Iso8601ExtendedOffsetDateTimeFormat; DateFormat f = new Iso8601ExtendedOffsetDateTimeFormat(); Date d1 = f.parse("1970-01-01T00:00:00Z"); Date d2 = f.parse("1970-01-01T09:00:00+09:00");
最新の情報はリポジトリの README ファイルを見てください。
ISO 8601 関連の情報
小さなライブラリが欲しいのでなければ、Joda-Time や Apache Commons Lang、JSR 310 のバックポートライブラリなどを使うのが良いかもしれません。
- Android プラットフォームと Java SE 環境における
SimpleDateFormat
のパターンの違い : Android と Java では SimpleDateFormat の書き方がこう違う - Qiita- Android プラットフォームでは
Z
で 「±HH:MM」 が解釈されるけど、「Z」 が解釈されないという……。
- Android プラットフォームでは
- ThreeTen ABP を使う方法 : AndroidでJSR310 - Qiita
- Apache Commons Lang を使う方法
- 色々な情報 : JavaでのISO 8601形式の日時の処理 - drambuieの日記
Android の Java で時刻を扱う (Date、Calendar、DateFormat クラス)
Java エンジニアの皆様は Java SE 8 で導入された Date-Time パッケージ (Date and Time API; JSR 310) を便利に使っていることと思います。 残念ながら Android プラットフォームにはそれらの API がありませんので、Android アプリ開発時に時刻を扱う場合は、古くからある API を使用することになります。
この記事では、Android アプリ開発時にお世話になる Date
クラス、Calendar
クラス、DateFormat
クラスについて、それぞれの役割や使い方、気を付けるべきことをまとめます。
追記
(2020-06-03) Android での Date-Time パッケージ
Android の API Level 26 からは java.time
パッケージが使えるようになっています。
Android 8.0 (API level 26) adds support for several additional OpenJDK Java APIs:
Android 8.0 Features and APIs | Android Developers
java.time
from OpenJDK 8.java.nio.file
andjava.lang.invoke
from OpenJDK 7.
また、Android Gradle Plugin 4 からは、Java 8+ API desugaring support により、API Level 26 未満のプラットフォームでも java.time
パッケージを使用するアプリをビルドできるようになったようです。
If you're building your app using Android Gradle plugin 4.0.0 or higher, the plugin extends support for using a number of Java 8 language APIs without requiring a minimum API level for your app.
(略)
The following set of APIs are supported when building your app using Android Gradle plugin 4.0.0 or higher:Use Java 8 language features and APIs | Android Developers
- (略)
- A subset of
java.time
(2016-02-18) JSR 310 の Android バックポート
JSR 310 の Android バックポートライブラリの存在を知りました。 Android ではこれを使うのがいいかもしれないですね。
- GitHub - JakeWharton/ThreeTenABP: An adaptation of the JSR-310 backport for Android.
- ThreeTenABP と ThreeTenBP の関係について (Android における JSR-310 バックポート) - ひだまりソケットは壊れない : ThreeTenABP がどういうライブラリなのかという説明を書いたので、こちらもあわせてご参照ください。
ところで Time
クラスは?
Android APIs には Time
クラスってやつが含まれています。 API level 22 で非推奨 (deprecated) になったのでそっとしておきましょう。 (これまでありがとうございました。)
気を付けるべきことまとめ
記事が長いので、気を付けるべきことを最初に書いておきます。
Date
オブジェクト自体はタイムゾーンを持ちません。DateFormat
を使って Web API などから取得した時刻の文字列を解析する際は、必要であれば適切なタイムゾーンを設定しましょう。 (解析対象の文字列にタイムゾーンが含まれているのであれば必要ない。) ロケールはLocale.US
が良さそうです。- Android *2 の
SimpleDateFormat
は、ISO 8601 や RFC 822 で定義されていて実際にしばしば使われる 「Z」 というタイムゾーン表記を解釈できない (Java SE 7 ではX
というフォーマット文字が導入されて解釈できるようになった) ので、気を付けましょう。 - ユーザーに表示するための時刻の文字列を生成する際は、
andorid.text.format.DateFormat
オブジェクトを使うことで端末の設定を反映させると良いでしょう。- あるいは
android.text.format.DateUtils
も便利に使えるでしょう。
- あるいは
Date
、Calendar
、DateFormat
、それぞれの役割
簡単にまとめると、Date
クラスが 「時間軸上の特定の瞬間」 を表すもので、Calendar
クラスが年月日や時、分、秒といった情報を扱うクラス、そして DateFormat
が時刻を表す文字列と 「時間軸上の特定の瞬間」 の相互変換を行う、という感じです。 詳細は以下に述べます。
Date
クラス
Date
クラスは、時間軸上の特定の瞬間をミリ秒の精度で表現するクラスです。
純粋に 「時間軸上のある瞬間」 を表現するためのものなので、タイムゾーンを持ちませんし、自身が表現する時刻が何月何日の何時何分なのかを計算したりもしません。 時刻を表現する文字列をパースしたり、逆に時刻を表現する文字列を出力したりもしません。 (歴史的経緯によりそのような機能のメソッドが Date
クラスに定義されていますが、非推奨です。)
Calendar
クラス
Calendar
クラスは、次の 2 つの機能を提供するクラスです。
- 「時間軸上の特定の瞬間」 と 「何月なのかや何日なのか、何時なのかといった数値情報・カレンダー情報」 の相互変換
- 何日なのかや何時なのかといった数値情報・カレンダー情報に対する計算。 (例えば、1 週間後の日時を取得する、など。)
Calendar
オブジェクトで時刻を表現させようと思えば実際上は可能ではありますが、そういう使い方には Date
オブジェクトを用いるべきみたいですね。 年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります。
GregorianCalendar
クラス
Calendar
クラスは抽象クラスで、変換や計算の処理は各種サブクラスが提供することになっています。 世界のほとんどの地域で使用される標準的な暦体系に対する実装として、GregorianCalendar
が提供されています。
DateFormat
クラス (java.text
パッケージ)
「時間軸上の特定の瞬間」 を表す値から日付・時刻を表す文字列を生成したり、逆にそれらの文字列を解析する機能を持つクラスです。
年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります。
SimpleDateFormat
クラス
DateFormat
のサブクラスです。 自分で日付・時刻の文字列のフォーマットを指定する場合はこのクラスを使用することになります。
DateFormat
クラス (android.text.format
パッケージ)
めちゃくちゃややこしいのですが、Android APIs には android.text.format.DateFormat
というクラスも含まれています。 このクラス自身もフォーマット機能を持っていますし、java.text.DateFormat
オブジェクトを返すファクトリメソッドも持っています。
API level 23 の実装だと、java.text.Date
クラスの getDateInstance
メソッドや getTimeInstance
メソッドも端末の設定を見てくれるようですが、API level 10 では android.text.format.DateFormat
の getDateFormat
メソッドなどを使わないと端末の設定が反映されないようです。 (どこで変更されたかは調べてない。)
なので、端末の設定を反映させて日時や時刻を表示したい場合は、android.text.format.DateFormat
クラスを使ってフォーマットを決める必要があるようです。
使い方
各種ユースケースにおいて、上で紹介したクラスをどのように使うかを簡単に紹介します。
Web API でやり取りする時刻の文字列と Date
オブジェクトを相互変換する
Web API のレスポンスで "2016-01-01 00:00:00+0900" みたいな時刻を表す文字列を受け取ることがあると思いますが、そういった文字列を Date
オブジェクトに変換するにはフォーマットを指定して SimpleDateFormat
オブジェクトを作成しましょう。
java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US); // タイムゾーンが含まれていないような日付文字列を解析する場合は、適したタイムゾーンを指定しましょう。 // df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); Date d = df.parse("2015-01-01 00:00:00+0900");
ロケールはタイムゾーンの処理にも関わってるぽいです (まじか……) し、Locale.US
にしておくのが良さそうです。 例えば、パターン Z
を使用してタイムゾーンを解析する場合、Locale.US
ならば 「EDT」 を処理できますが、Locale.ROOT
や Locale.JAPAN
だとパースエラーになります。 (API level 23 で確認。 ロケールに関係なく RFC 822 で定義されているタイムゾーンは解析して欲しい……。 厳しい。) でも Locale.US
でも 「Z」 を処理できないから、数値でなく名前で表現されるタイムゾーンの解析周りについてはそもそも信用しないのがいいかもしれないですね。
Web API などに送るための時刻文字列を生成する場合も、同様に SimpleDateFormat
を使いましょう。 タイムゾーンは、受け取り側が期待するであろうものを設定しておくのが安全だと思います。
java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US); // タイムゾーンを出力するにしても Web API 側が期待するタイムゾーンを設定しておいた方が安全でしょう。 df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); String currentTime = df.format(new Date());
ユーザーに表示するために Date
オブジェクトから時刻の文字列を生成する
ユーザーに表示するための時刻文字列は、ユーザーの端末のロケールやタイムゾーンを使用すべきでしょう。 Java では DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
という感じでデフォルトロケールの DateFormat
を取得できますが、Android アプリの場合は、端末の設定を反映したフォーマットの DateFormat
を取得するために android.text.format.DateFormat
を使用するのが良さそうです。
詳細は以下のページを見てください。
- http://qiita.com/glayash/items/508304558078203fe24b
- Y.A.M の 雑記帳: SimpleDateFormat ではなく android.text.format.DateFormat を使おう
(API level 23 では、DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
みたいな感じで取得した DateFormat
オブジェクトも端末の設定を反映しているようでしたが、API level 10 だと反映してなかったので、古い端末もサポートするなら android.text.format.DateFormat
を使うべきっぽいです。)
Date
オブジェクトが表す日の 1 年後、を計算する
例えば今日の 1 年後を表す日を計算したいとします。 そういうときは Calendar
を使います。 *3
// 操作対象の Date オブジェクト。 // 日本時間で 2016 年 2 月 29 日の 00:00:00。 Date d = new Date(145667160_0000L); // Calendar オブジェクトを使って 1 年後を計算して Date オブジェクトを取得。 Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"), Locale.JAPAN); c.setTime(d); c.add(Calendar.YEAR, 1); // 年に 1 加算。 Date dateOneYearAfter = c.getTime(); // 2017 年 2 月 28 日の 00:00:00 (日本時間)。
上の例では、タイムゾーンを Asia/Tokyo に設定しています。 タイムゾーンを適切に設定しないと、渡された Date
オブジェクトが表す日付が変わる可能性がありますので、適切にタイムゾーンを設定しましょう。 ロケールも週の開始曜日などに影響するので必要に応じて設定しましょう。 (デフォルトタイムゾーン・デフォルトロケールが良い場合はもちろんデフォルトの値を使えば良いです。)
*1:ドキュメントにも 『The main reason you'd create an instance this class directly is because you need to format/parse a specific machine-readable format, in which case you almost certainly want to explicitly ask for “US” to ensure that you get ASCII digits (rather than, say, Arabic digits).』 って書かれてる。
*2:少なくとも API level 23 までは。 それより後のバージョンは不明。
*3:単純に 365 日間に相当するミリ秒を加算する、みたいな方法だとうるう年などが考慮できないのでだめですよね。 「1 ヶ月後」 とかだと何日分加算すればいいのかわかんないし。