クラスメソッド株式会社に転職しました!

都元ダイスケ(当時34)は、クラスメソッド株式会社に転職しました。

こんだけJavaJavaしてた都元が、なんとAWSエンジニアになっております。世の中どうなるかわからんですね〜。まぁとは言え、ちょいちょいJava触ってますが。

そんなわけで、今後共よろしくお願いします!












って1年以上前の話やけどな。

またな、山城。

「第一回チキチキjava-ja ymsr送別会」に行ってきた。
ちょっと湿っぽくなった瞬間もあったけど、笑いの絶えない良い会だった。

あいつは絶対準備して待ってる。
というわけで、その時が来たら、「第一回チキチキjava-ja ymsrによる歓迎会」に参加しようと思う。
しばらく待ってろ。

日経ソフトウエアにて、Eclipse逆引きポケット事典を執筆しました

ごぶさたです。都元です。
日経ソフトウエア2012年8月号が昨日発売となりました。都元が特別付録の文庫サイズ別冊「Eclipse逆引きポケット事典」に寄稿しました。この原稿は、息子誕生の混乱のさなかに脱稿したものですw その節は編集者さまにはご迷惑をお掛けしましたごめんなさい。

まぁ、日々Eclipseを使っているような皆様におかれましては全部知ってる内容な気はします。が、まぁ私が日頃Eclipseを使うにあたってのノウハウを書いて行ったものなので、まだそこまでEclipseに慣れていない、という方にはオススメな内容です。

日経ソフトウエア 2012年 08月号 [雑誌]

日経ソフトウエア 2012年 08月号 [雑誌]

ちなみに、ブログで報告しませんでしたがこの2号前、6月号でも特集の「プログラミングでコレがやりたい! 40選」にて1つ、JavaでのXMLの扱い方(StAX)についてさらっと寄稿させていただいてます。

日経ソフトウエア 2012年 06月号 [雑誌]

日経ソフトウエア 2012年 06月号 [雑誌]

父親になりました♪

なんか、気づいたら半年近くブログ放置してましたw この半年色々ありましたけど、それはまた、別のお話。

ってことで、えーー、先ほど 5/22 22:04 に息子が産まれました! ワーーイ。産まれた瞬間、ベタですが、涙出ました。嬉しい! 良い人生を送って欲しいな、と心から思いました。

っていうかね。悪いけど、マジかわいいぜ、ウチの息子!! うはwww これは、毎日風呂に入れたくなるってもんですわーーww ベビーバスまだないけどなwwww っつーか、まだ名前も付けてないけどなwwwwwww 早く考えてやんないと。名前呼びてーーーけど、ないwww だめだ、テンションがアレだww

まぁそれはともかく、とりあえずwishlist 貼っておきますね :) http://www.amazon.co.jp/registry/wishlist/3U2NT3SPZNGP

追記

https://p.twimg.com/Atyby-BCEAAPIO6.jpg

というわけで本当にたくさんの方からお祝いをいただきました。メールアドレスが分かる方にはメールでもご挨拶させていただきましたが、送り主不明のお祝いが約半分を占めており、この場であらためてお礼申し上げます。ありがとうございました!

まぁ、いまだにバタバタしていますが、先日、無事に一ヶ月検診も終え、家族3人元気に過ごしています。
そうそう、Facebook等ではご報告しましたが、名前も決まりました。えーと、都元キョウスケですw

しばらくは寝不足の日々が続きそうですが、キョウスケともども、都元家を今後ともよろしくお願いします :)

細かすぎて伝わらないJava7の変更点

本日のエントリーはJava Advent Calendarの25日目です。昨日は @mike_neck さんのmike、mikeなるままに…: hamcrestを拡張してmoreThanとか作ってみたでした。本日はクリスマスですが、Advent Calendarはまだまだ続きます。明日はt.ogisawaさんのhttp://webinter.sakura.ne.jp/pbd/210です。

さて、今年はJava7がリリースされましたね。try-with-resources、diamond operator、invoke dynamic、Folk/Join framework…魅力的な新機能の数々が、多くの人によって紹介されています。が、Java7の変更はそれだけじゃないはず。小粒だが、キラリと光る変更がきっとあるはず。ということで、Java6とJava7のsrc.zipを比較してみました。小ネタなのでサラっと読んで頂ければと思います。

まず、比較に用いたのは以下の通り。


で、まず普通にdiffを掛けてみたのだが、意外とdocコメント上で頑張っていることが判明。docコメント内のcodeタグやttタグ、preタグの多くを{@code}や{@link}で書き直してある部分が目立つ。まぁ、未だにcodeタグのままの部分も多いのだが、頑張ったよね。個人的にはHTMLタグよりも読みやすいと思っているので、嬉しいです。

あと、Java6までのソースは、インデントがTABだったりSPACEだったりまちまちだった。そしてどうやらTAB幅8としているようで、TAB幅4の環境で見るとそれはそれは残念な感じになってしまう。もうこの辺りのコーディングスタイルに関しては、標準APIであるにも関わらずもう壊滅的でしたよね。自分のコーディング規約やJavadocの書き方ルール策定の参考にしようと思って愕然とした記憶がある。しかし、Java7では、インデントがSPACEに直っているのが目立つ。未確認だが、全部統一されているのかもしれない。喜ばしい。

といった所の差異までdiffで見えてしまうと、もはやノイズでしかないので、ここは思い切って両者のソースをいじってしまいます。

find . -type f | xargs sed -e 's/\<code\>\([^<]*\)\<\/code\>/{@code \1}/g' -i ""
find . -type f | xargs sed -e 's/\<tt\>\([^<]*\)\<\/tt\>/{@code \1}/g' -i ""

両者のソースに対して、上記の置換を掛けた後、俺俺コードフォーマッタを掛けてからdiffに挑みました。で、com.sunパッケージなんかの差を見始めてもアレなので、ひとまず java.* パッケージに絞って、そして都元が個人的に気になったポイントを中心に、以下にご紹介しまーす。

スペルミスや細かいバグフィックス

まずは軽く。Java6と7の比較、という視点ではありませんが、結構色々直していますね。

 * @throw new NullPointerException

なんていうdocコメントがあったり。勢いでnewって書いちゃったんだろうなーw

public Foo {
  // …
  private void Foo() {}
}

なんてやっちゃってるクラスもありました。

古いコーディングスタイルの刷新

Java6のソースにはstatic publicという語順(?)や、char foo[]; のような配列宣言、rawtype型など、古い書き方が内部に随分残ってます。こういった所がちょいちょい直してありました。あと、ダイアモンドオペレータもきっちり使われてましたよ。

各Exception実装クラスにserialVersionUIDが追加

ExceptionはSerializableのサブタイプです。従って、例外の実装クラスには全てserialVersionUIDを記述するのが望ましいんですね。まぁ、実装の詳細の話ではありますが、各ExceptionにserialVersionUIDが追加されてます。

Byte/Integer/Long/BigIntegerの文字列paese

int i1 = Integer.parseInt("3");
int i2 = Integer.parseInt("-6");
int i3 = Integer.parseInt("+2");

皆さん、このコード実行するとどうなると思います? 実は、Java6だと "+2" は NumberFormatException になってしまうのです。文字列の整数parseにおいて、プラス記号は今まで使えませんでした。これが、Java7からは普通に通るようになります。

ちなみに、DoubleやFloatのparseでは、Java6でもプラス記号が使えます。

primitive wrapper class

Integer.compare(10, 15);

というような、比較ロジックがstaticメソッドとして提供されるようになりました。プリミティブラッパー型にそれぞれ定義されています。比較ロジックを引き算で実装してバグを出してしまう位なら、このユーティリティメソッドに委譲してしまうのがよいですね。

比較ロジックの引き算実装については下記参考書籍の「パズル65」を参照。

Java Puzzlers 罠、落とし穴、コーナーケース

Java Puzzlers 罠、落とし穴、コーナーケース

ComparableとComparator

従来、Comparable#compareToやComparator#compareの引数にnullを渡した時の仕様は「未定義」でした。また、私は基底型のjavadocに明示してある例外しか投げないようにしているため、「nullとは比較できないComparable」を実装したい時、とても気持ち悪い思いをしていました。

そんな中、Java7ではこれらの比較メソッドのjavadocに、NPEの記述が追加されました。

@throws NullPointerException if the specified object is null

引数がnullな時はNPEを投げてよくなったのですね。

Collections

以前から、Collectionsクラスには emptyList() 等のメソッドがありましたが、似たような感じで以下のメソッドが追加になりました。大したことではありませんが、使う機会があれば使った方が良いですね。

  • emptyEnumeration()
  • emptyIterator()
  • emptyListIterator()

Objets

新しいユーティリティクラスです。requireNonNullやnull-safeなequals/hashCode/toStringなど、小粒ながら使い勝手は良さそうです。

ReflectiveOperationException

従来、リフレクションAPIを利用しようとすると数多くのチェック例外を処理する必要がありました。NoSuchMethodException, InvocationTargetException, ClassNotFoundException, IllegalAccessException… 実際は発生しないと踏んでいる例外をこんなに大量にキャッチさせられるのはストレスでしたね。しかし、Java7からはReflectiveOperationExceptionという基底クラスが定義されました。これによって、リフレクション操作時の例外をまとめてキャッチできます。

AutoCloseable

try-catch-resources用に出て来た新しいインターフェイスです。Closeableはもちろん、Connection/ResultSet/Statementもこいつのサブタイプに。

従来はJDBC API用の closeQuietly 的なユーティリティを、I/O とは別に書かなければなりませんでしたが、今度からまとめられますね。

ThreadLocalRandom

っていうクラスが追加になっています。まぁ、内容は名前から想像できる通りです。

Scanner

ScannerがCloseableのサブタイプになりました。今までCloseableじゃなかったのは、単に忘れてただけなのでしょうかw

Integer, Short, LongのvalueOf

Sun(Oracle)のJavaにおいて、Integer型のインスタンスは-128〜127の値がキャッシュされる、というのは有名な(?)話でした。しかし、このキャッシュは「Sun(Oracle)の実装がたまたまそうなっている」だけであり、Javaの仕様ではないものでした。つまり、別のJava実装(例えばIBM Javaとか?)ではキャッシュをしていないかもしれません。キャッシュの範囲が違うかもしれません。

という状況だったのですが。Java7のjavadocには以下のような記述が追加されています。

【Integer#valueOf及びShort#valueOfより】
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
【Long#valueOfより】
     * Note that unlike the {@linkplain Integer#valueOf(int)
     * corresponding method} in the {@code Integer} class, this method
     * is <em>not</em> required to cache values within a particular
     * range.

これって…。Javadocに明示されたということは、キャッシュの挙動も含めて仕様化したということで良いんでしょうかね。

System

System#lineSeparator() っていうメソッドが追加。プラットフォーム依存の改行文字(列)をさくっと手に入れられて便利ですね。

また、Systemクラス内の in, out staticフィールドの初期化方法が変わったようです。Java6までは、staticフィールドが参照元クラスにインライン展開されないように、ちょっとしたハックがされていましたが、Java7では直接null初期化しています。これはインライン展開されなくなったんですかね。そこまでは追いきれませんでした。

暦システム関連

week yearのサポートが手厚くなってます。GregorianCalendarに以下の3メソッドが追加になりました。

  • getWeeksInWeekYear()
  • getWeekYear()
  • isWeekDateSupported()


って何の事だかわからんですね。えーと。「1週間は月曜〜日曜である」として、「2011年の第1週」って何日から何日だと思いますか? まぁこのネタはこちらが詳しいので参照してください。 → 今が年の何週目か - 気付いたとき、気が向いたとき。by ykhr

で、結論としては、ISO8601的には1/3〜1/9が「2011年の第1週」ということになります。ということは、2010/12/27〜2011/01/02は「2010年の第52週」なんですね。デフォルトのGregorianCalendarの挙動は「1週間は日曜始まりで、1/1を含むのが第1週」ということにになっていますが、それをISO的に「1週間は月曜始まりで、1/4を含むのが第1週」という設定をするのが先ほどの id:ykhr-kokko 氏のエントリです。

で、ここで注目したいのは「2011/01/02は、2011年の日付であるにも関わらず、週レベルで見ると2010年に属する」ということです。めんどくさいですねw 今日は何年の第何週なのかが知りたいとします。第何週かは cal.get(Calendar.WEEK_OF_YEAR) で良いでしょう。では「ある日付を与えた時、それは週レベルでは何年に属するのか?」をどうやって取るのか。これが getWeekYear() です。

とは言え、大抵 Calendar.YEAR と一致してますから、年末年始だけ気をつけておけば簡単に計算できるんじゃね? …と思いきや、正確にやり出すと結構大変みたいです。1582年以前は、今のグレゴリオ暦ではなくユリウス暦が…詳しくは getWeekYear() の実装をご覧下さい。

んでまぁ、上記に関連してSimpleDateFormatに新しいパターンが追加になりました。Y, u, X の3つですが、このYってのがweek yearです。uとXは大した事ないので気になったら各自ググってくださいw

ちなみに「1週間は月曜始まりで、1/4を含むのが第1週」というルールを知っているのはCalendarですから、下記の通りSimpleDateFormatにcalを与えてやらないと、ISOのルール通りには動きませんので注意しましょう。

Calendar cal = new GregorianCalendar();
cal.setMinimalDaysInFirstWeek(4);
cal.setFirstDayOfWeek(Calendar.MONDAY);
SimpleDateFormat f = new SimpleDateFormat("yyyy/MM/ddはYYYY年の第ww週です。");
f.setCalendar(cal);
System.out.println(f.format(new Date()));


…と、まぁdiffをしてみて気づいたあれやこれやを並べてみました。明日以降も引き続き、Java Advent Calendarでお楽しみください :)

JIRAの課題検索とフィルターを使いこなそう

JIRA Advent Calendarってのに巻き込まれました。巻き込まれサイコーw

昨日は@oota_ken氏のJIRAのスキーマーをastahのクラス図で書いてみる ユーザー編 – テスト自動化他何かのメモでした。確かに、JIRAのデータ構造って、ぶっちゃけ理解するの大変ですよね…。なんちゃらスキーム苦手ですw

で。@yusukeyサンからの「オマエのJIRAへの愛を語れ」っていう振りは完全スルーして、普通に活用Tips書きますw

自分はJiemamyプロジェクトにOSSライセンスを頂いて以来、随分長いことJIRAユーザをしています。なので、もうちっとディープな話もできなくもないかなぁとは思ったのですが、周囲を見ていると、玄人向けの難しい機能を紹介するよりも「願わくば全てのJIRAユーザが活用して欲しい、けどもなかなかこれを活用している人はいないんですよね」的な機能の紹介をしたほうがウケがいいよね、と思ってそんなネタを書きます。

JIRA Advent Calendarに参加するような人たちは既知のお話だと思いますが、活用歴が浅めのユーザさんに向けて。

まず課題検索を理解する

JIRA上には様々なチケットが蓄積されていきます。まず、そのチケットを「探す」ことにフォーカスを当ててみましょう。

検索と言えば、まずは普通にキーワード検索が浮かぶと思います。また、チケットID*1でチケットを探すこともあると思います。このような検索は、通常画面の右上にある「クイック検索」を利用します。

とりあえず、Jiemamy ProjectのJIRAに行ってみて、右上のクイック検索欄に「FileNotFoundException」とか入力してみましょう。この記事の執筆時点では ISM-18 とCORE-128 という2つのチケットが引っかかるはずです。続いて、同じくクイック検索に「ECL-100」と入力してみると、該当チケットに直接ジャンプします。って、普通ですね。

ここまでは敢えて説明するまでもない、普通の検索です。しかしこの「クイック検索」ボックスは、実は恐るべき機能を備えています。例えばここに「ECL unresolved」と入力してみましょう。そうすると「スマート クエリが起動しました。」というメッセージと共に「未解決で、かつECLプロジェクトに属するチケット」が表示されます。そう、unresolvedなど、いくつかの単語は普通にキーワード検索されるのではなく、特殊な意味として解釈されるのです。

その他、「CORE bug created:-10w」(COREプロジェクトで過去10週間以内に作成されたバグチケット)など、色々な検索を手軽にできるんですが、その中でも私が一番使うのは「my unresolved (プロジェクト名)」(ex: my unresolved ECL)ですかね。このスマートクエリは、是非知っておきたい機能です。

あわせて読みたいhttp://confluence.atlassian.co.jp/pages/viewpage.action?pageId=31523399

色々なフィルターを紹介

クイック検索を使って、簡単にチケットを検索できることがわかりました。しかし、前述のような何度も使う簡単な検索条件は、覚えてしまえば良いのですが、覚えるのには向かない程度に複雑であるものの、よく使う検索条件、というのも存在します。そういったものは「検索条件を保存」しておいて、必要な時に呼び出せば良いのです。このように「よく使う検索条件を保存したもの」のことを「フィルター」って呼びます。


少し話が飛びますが、検索には「簡易検索」と「高度な検索」というものがあります。前者はフォームに検索条件を細かく指定するタイプ(右図)の検索なのですが、私はほぼこれを使いません。まどっろっこしい! まぁ多分、エンジニアだからでしょうけどw フィルターに保存するくらいの複雑さを持つ検索条件は、「高度な検索」を使うと良いと思います。というわけで、これを高度な検索に切り替えてみましょう。

まず、クイック検索で適当に「my unresolved」等と検索してみましょう。その上で、検索結果の左側にあるエリアの上方「高度な検索に切り替える」をクリック(左図)します。そうすると、検索結果の上に「クエリ」というテキストエリアが表示され、ここに検索条件を「式として」記述できるようになります。この条件式の記述言語はSQLに似た形になっており、「JQL (JIRA Query Language)」と呼ばれています。JQLを活用すれば、簡易検索では表現できない複雑な検索条件を記述できるようになります。

で、JQLの細かい書き方はドキュメントに任せることにして、ここでは私が普段使っているフィルターを、成り立ちと共に具体的に紹介したいと思います。(エンジニアの方は普通にJQLを読めると思います)

報告者にアサインして解決確認を依頼すべき担当課題

JIRAのチケットは、特にカスタマイズしていない状態では、「オープン→対応中→解決済み→クローズ」というライフサイクルを辿ります。ただ、私の経験上、デフォルトのままのJIRAを運用すると、解決済みのステータスで止まってしまってクローズに至らないチケットがあまりにも多いのです。しかしこれはある意味当然の事。実施者が満足して解決済みとしたタスクは、多くの場合特に問題はなく、完全に終わったタスクだとみなされてしまいます。従って、それ以上、そのチケットに対してアクションをする必要はない、と考えてしまうのです。そもそも「クローズ」という状態の存在さえ忘れてしまう。だって、解決したんだから良いでしょう? という気持ちです。

ただ、より一層厳密に仕事を進めるために、対応者が行った仕事の結果を報告者*2に見てもらい、チケットを作った意図通りの結果になっているかどうか最終確認をしてもらう、というプロセスを踏んだ方がトラブルが少ないでしょう。従って、まずチーム内で「対応者が満足して解決済みにしたチケットは、報告者にアサインを返し*3、クローズを依頼する」というルールを作り、コンセンサスを得ます。(このルールを厳密化するために、私が管理するJIRAでは「報告者でなければチケットをクローズできない」ように設定しています。この方法は今回の話から脱線するので、また別の機会にでも。)

しかし、人間というのは基本的にミスをする生き物です。前述のルールでは、対応者がチケットを「解決済み」にする際に、報告者にアサインバックすべきなのですが、担当者の変更を忘れてそのままにしてしまう、というミスが頻発します。こんなエントリを書いている私でさえ、よくやります。このようにアサインバックを忘れられたチケット*4は、ダッシュボードの「自分の担当課題」には表示されなくなるため、ほぼ確実に "忘れ去られた存在" になります。

そこで効果を発揮するのがこのフィルター。

status = Resolved AND assignee = currentUser() AND reporter != currentUser()

「ステータスが解決済み かつ 担当者が自分 かつ 報告者が自分ではない」チケットですね。この条件に引っかかるチケットは、前述の「アサインバック忘れ」ですので、見つけ次第、報告者にアサインを返す操作をしましょう。

解決を確認の上クローズすべき担当課題

次に、上記のシナリオでアサインバックされた側の話。このような経緯でアサインバックされたチケットは既に「解決済み」のチケットです。従って、このチケットも「自分の担当課題」には挙ってこないんですね。というわけで、確認しなければならない立場の人にとっても同様、"忘れ去られて然るべき存在" なのです。同じように、フィルターで検出してあげましょう。

status = Resolved AND assignee = currentUser() AND reporter = currentUser()

解決済みで、担当者と報告者がどちらも自分のもの、というフィルターです。この条件に引っかかるチケットは、対応内容を確認して、問題が無ければクローズ、問題があれば再オープンして対応者に差し戻す必要があります。

期限超過の担当課題

JIRAのチケットには「期限」という項目があります。が、別に期限を過ぎたからって、デフォルトのJIRAは何をしてくれる訳でもありません。でも、期限をオーバーしてしまったチケットには早めに気づきたいですよね。そこでコレ。

resolution = Unresolved AND assignee = currentUser() AND due <= now() ORDER BY due

このように、期限の値が現在より小さい(つまり過去)で、解決されていない自分担当のチケットを検出します。この条件に引っかかったら、早急に対応するか、または期限の見直しをすべきでしょう。

長期放置の担当課題

とは言え、チケットの期限設定というのは意外と難しいものです。チケットを作る人が勝手に期限を設定したとしても、実際の対応者が対応しきれなければ全く無意味です。対応者の忙しさを度外視して、チケット作成者の都合だけを押し付けた期限を設定しても印象悪いだけですしw 従って、よほど明確なデッドラインがあるチケットでなければ、基本的に期限を付けずにチケット発行することが多いようです。経験的に。

しかし、期限がないままチケットをアサインすると、そのチケットをそのまま握り続けられてしまう場合があります。期限がないので「期限超過の担当課題」にも引っかかりません。他にもっと優先度が高いチケットがある、という錦の御旗の下、黙殺されるチケットになってしまうのです。

しかし私は、そういった優先度の低いチケットであっても、定期的にレビューし、放置されている状態が適切かどうかを考える必要があると考えています。もしかしたら状況が変わっていて、もっと優先度が上がっているかもしれません。GTDでも、定期的にタスクをレビューしますよね。

というわけで、長期に渡って放置されている課題をフィルターで検出します。

updated < -14d AND status != Closed AND assignee = currentUser()

ここでは「長期」を「14日間」としていますが、まぁここは各自調整してみてください。この条件に引っかかるチケットは、引き続き放置することが適切であるかをレビューしましょう。放置してはいけないと判断した場合はチケットを編集して優先度を上げましょう。編集すればupdatedの値が更新されるのでこのフィルターには引っかからなくなります。また、引き続き放置する場合は、一言コメントしましょう。コメント操作でもupdatedの値が更新されます。

フィルターを購読する

と、まぁ私が普段使っているフィルターを色々紹介してきましたが、フィルターを作るだけではあまり効果がありません。このフィルターは「フィルターの結果」ウィジェットを使ってダッシュボードに常設しておくと良いでしょう。ウィジェットの追加方法は、説明し出すとまた長くなるので、@Sean_SFさんのブログ Something went wrong. を参照してください(逃)

ここではもう一つ。定期的にフィルターの条件を満たすチケットが無いかをチェックし、条件にhitするチケットが検出されたらメールを送る、という技です。

JRIAの課題メニューの一番下に「フィルターの管理」という項目がありますので、このページに飛んでください。作ったフィルターが列挙されているはずです。そのそれぞれのフィルターの右方に「配信登録」というリンクがあるはずです。これをクリックし、適当なスケジュール(例えば毎朝9時など)で配信登録してみましょう。これで、フィルターに引っかかった課題リストを毎日メールしてくれるようになります。

朝出社したら、このメールを見ながらチケットを整理することによって、クリーンな状態で仕事を始めることができますね。

番外編:今週解決した課題

resolution = Fixed AND resolutiondate >= startOfWeek() AND resolutiondate <= endOfWeek()

私が最近作ったフィルターです。週の頭には1つも引っかかりませんが、週末になるに連れて課題が増えていきます。あー、ウチのチームは今週これだけの仕事を進めたんだなぁ、って眺めながらニヤニヤする用のフィルターです。

実は私はもう一工夫して「自分が今週解決した課題」として、より一層ニヤニヤしているのですが。ただしこの場合、単に assignee = currentUser() ではダメなんですね。というのも、前述のルールでは、自分が解決したとしても報告者にアサインバックしてしまうから。そこで「 lastResolutionUser = currentuser() 」っていう条件(最後に「解決済み」にしたユーザ、つまりほとんどの場合「タスクの実施担当者・対応者」を表す)を使っています。しかしこの lastResolutionUser っていうのはJIRAの標準機能ではありません。Jira Enhancer Pluginというプラグインをインストールし、設定しています。頑張れる人は各自頑張ってみてください。


さて JIRA Advent Calendar 4日目は以上です! みなさんも、便利なフィルター条件があったら教えてください。明日はエロ元上司 @j5ik2o (id:j5ik2o)…かと思いきや、どうやら@yusukeyの無茶振りに対してバックレを決め込んでいるようです。俺はそんな上司に育てた覚えはないぞっ!!w 観念してエントリーするように。

というわけで、明日は @inda_re 氏のエントリーです。お楽しみに!

*1:正確にはキーって呼ぶみたいですね

*2:チケットを作った人=仕事を振った人

*3:担当者をチケット作成者に割り当てる

*4:つまり「ステータスが解決済み かつ 担当者が自分」のチケット

Spring 3.1 の Cache Abstraction(キャッシュの抽象化)

しばらくコード付きのエントリ書いてないなぁ、と思ったので。Springの新機能についてひとつ。

Spring3.1は、まだリリース版は出ていないのだけど、RC1が出ている。(参考 Spring 3.1 RC1リリース

その新機能にCache Abstraction(キャッシュの抽象化)ってのがあって、色々調べてみた。例えばWebAPIなんかを叩いて情報を取ってくるようなメソッドは、情報があまり変化しないものであればキャッシュしちゃえばいいよね。例えば Amazon API で、ASINから商品名やら何やかんやを取ってくるメソッドとか。

下準備

package jp.xet.sample;

public interface EntityRepository {
    
  String get(int id);
    
  void put(int id, String value);
    
}

例えばこんな(↑)インターフェイスがあって、このgetのコストが高いとしましょう。で、今回使うサンプルの実装がコレ(↓)。コストの高さをThread.sleepで表現してみました。実際は単なるMapストレージなんだけども。

package jp.xet.sample;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
public class EntityRepositoryImpl implements EntityRepository {
    
  private Map<Integer, String> storage = new HashMap<Integer, String>();
    
    
  @Override
  public String get(int id) {
      
    // simulate slow operation
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      throw new AssertionError(e);
    }
      
    return storage.get(id);
  }
    
  @Override
  public void put(int id, String value) {
    storage.put(id, value);
  }
}

で、こいつ(↑)には@Repositoryアノテーションがついている。まぁ@Componentと全く一緒*1らしい。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd">

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

で、jp.xet.sampleパッケージ以下のアノテーション付きのコンポーネントをcontext.xmlで読んでもらう、と。まぁ、EntityRepositoryImplしかないんですが。そんなわけで、Mainクラスいきましょう。

package jp.xet.sample;

import org.apache.commons.lang.time.StopWatch;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    
    
  public static void main(String[] args) {
    ApplicationContext ctx =
      new ClassPathXmlApplicationContext("/context.xml");
    EntityRepository repos = ctx.getBean(EntityRepository.class);

こうして出来たコンテナからは、EntityRepositoryが取り出せる。あとは、適当にデータを置いて、時間をはかってみる。

    repos.put(1, "one");
    repos.put(2, "two");
    repos.put(3, "three");
    
    StopWatch sw = new StopWatch();
    sw.start();
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
    
    repos.put(1, "壱");
    repos.put(3, "参");
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
  }
}
time=0:00:01.002, value=one
time=0:00:02.012, value=two
time=0:00:03.014, value=one
time=0:00:04.014, value=two
time=0:00:05.015, value=three
time=0:00:06.016, value=壱
time=0:00:07.018, value=two
time=0:00:08.018, value=壱
time=0:00:09.019, value=two
time=0:00:10.020, value=参

だいたい、1getにつき1秒掛かってますね。

このget処理をキャッシングして、高速化しよう

まずcontext.xmlに手をいれましょう。キャッシュの設定。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:cache="http://www.springframework.org/schema/cache"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">

 <cache:annotation-driven />
 <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
   <set>
    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
     <property name="name" value="default"/>
    </bean>
   </set>
  </property>
 </bean>

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

この設定は、裏ではシンプルに普通のConcurrentHashMapに値をキャッシュする設定です。この他に、springではehcacheにも対応したりしてるらしい。で、ここでは「default」という名前のキャッシュを1つ作りました、ということになります。次にEntityRepositoryImplに @Cacheable アノテーションをつける。

  @Cacheable(value = "default")
  public String get(int id) {
    // ...
  }

こんな感じ。このメソッドの結果はキャッシュして、キャッシュヒットした場合は中身は実際には呼ばずに済ますよ、という意味です。この状態でもっかいMainを実行してみよう。

time=0:00:01.004, value=one
time=0:00:02.016, value=two
time=0:00:02.017, value=one
time=0:00:02.017, value=two
time=0:00:03.018, value=three
time=0:00:03.018, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=three

最初のone,twoには1秒ずつ掛かっているけど、その次のone,twoは一瞬で終わっている。threeは初アクセスなので再び1秒掛かってる。で、残りはもう全部キャッシュに載っているのでずばーーーっと。計約3秒。すばらしい。

漢数字はどうした

    repos.put(1, "壱");
    repos.put(3, "参");

途中でoneとthreeの値を書き換えたのだが、無視してキャッシュを返しちゃってますね。これを何とかしたい。ならば@CacheEvictアノテーションだ。

  @CacheEvict(value = "default", key = "#id")
  public void put(int id, String value) {
    // …
  }

このアノテーションがついたメソッドが呼ばれた時は、特定のキャッシュエントリを無効にします、っていうアノテーションだ。で、どのキャッシュエントリを無効にするの? ってのが key パラメータ。ここでは「idって名前の引数を利用します」ってことです。この表記法はSpEL(Spring Expression Langage)参照。

ちなみに、@CacheEvict(value = "default", allEntries = true) とすると、全キャッシュエントリをクリアしてくれる。

という対策を施し、再びMainを回す。

time=0:00:01.001, value=one
time=0:00:02.011, value=two
time=0:00:02.012, value=one
time=0:00:02.012, value=two
time=0:00:03.013, value=three
time=0:00:04.014, value=壱
time=0:00:04.014, value=two
time=0:00:04.015, value=壱
time=0:00:04.015, value=two
time=0:00:05.015, value=参

キャッシュのevictが上手く動いていることが分かります。

*1:ざっくりとしたコンポーネントには@Componentを付ければいいんだけど「こいつはコンポーネントだけど、特にリポジトリなんだよ」っていう気分で@Repositoryってのが用意されている。あとは@Serviceなんてのもある。気分で使い分ければよろし。