Author Archives: susumuis

About susumuis

メイドカフェによく居るWebエンジニア

10年間Javaを書いていた僕が Effective Java 第2版を読み返して新人に薦められるのかを考えてみた

これは Java Advent Calendar 2014 の13日目です。昨日は @skrb さんの「Duke で Swing」、明日は @bitter_fox さんです。

いきなりですが、テーマ変更します。ごめんなさい。Mayaaのことは書きません。

実は登録時には「Mayaaのことを書く」みたいなことを書いていました。確かにMayaaを扱って通算5年、かなりマニアックなことを書くこともできますが、マニアックなことを書いても誰も相手してくれないと思うので、そういうのは別の日にひっそりと書こうと思います。

そこで方針を変えようと思った時、僕の勤務先では開発者同士のコードレビューが盛んに行われ、特に現場に古くからいた僕は後から入ったほぼ全員の人のソースコードのレビューを求められて、日々の時間の多くをレビューに費やしていることを思い出しました。レビューを行う際はどうしてもこのコードは良いとか悪いとか自分なりにジャッジしなければなりません。それはその場の感覚で行っていましたが、何らかの指針を持っているはずです。そこで、それについて書いてみようと思います。

ネタとしては、駆け出しの頃読んで感動した、ある本を再読し、それを元に自分の今のコーディングに対する考え方を書いてみたいと思います。ある本とは、そうです。Effective Javaです。

よく「新人Javaエンジニアは Effective Java を読め」と言われてきました。

Effective Javaは一時絶版になりましたが丸善出版が再版してくれたため、また新人プログラマーにすすめることができるようになりました。以前からJavaで仕事するなら読んでおいた方が良いと言われる定番の一冊です。

僕は2004年ごろ初版を、2008年に第2版が英語で出たときは毎日少しずつ訳しながら読みました。(結局全部訳し終わる前に日本語版が出ちゃったのですが。。。)

初版は2001年、第2版は2008年の出版です。さすがに2014年現在では古いと言わざるをえません。しかし、プログラミングで大事なことは文法よりもその後ろにある思想にあると思います。

なおここから先は、あくまで僕の視点からのコメントです。立場の違う方からは違う意見も出ると思います。注意して書きますが、明らかに間違ってる場合はご指摘いただければ、幸いです。また、時間の都合上全部は無理で、はじめの方のちょっとだけになると思いますがご了承ください。

2014/12/15 追記:この記事に対して下記のブログから反響をいただいています。

この記事を公開後、Otchyさんがブログに記事を書いてくれました(ありがとうございます)。こちらも併せて読んでいただくことをおすすめします。

Indeed.comのJavaコード

僕の立ち位置

個人的な感想を述べる都合上、予め自分の立場を明かします。
* 2004年よりJavaを使ったECサイト開発に従事し本格的にJavaコードを書き始める
* Java EEサーバーのようなエンタープライズ向け製品は使用していない
* 開発者2名という小さなチームで自社プロダクトを開発していた
* 今は10数名の開発者がコードを書いていて、自らもコードを書きつつ、レビューをしている

ちなみにタイトルで10年やってるとは言っていますが、Java7ですら今年になってから使い始めたレベルなので偏っていると思います。もっと短い経験でも僕よりJavaに詳しい方はいっぱいいると思います。

項目別コメント

項目 1 コンストラクタの代わりに static ファクトリーメソッドを検討する

項目 2 数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

これら2つを組み合わせると「流れるようなインターフェース」と呼ばれたかっこいいAPIを実現できます。しかしこのようなAPIは使用者に対して、例えば「Hogehogeクラスのサブクラスは必ずコンストラクタを使わずにnewInstance()メソッドを使うこと」などの情報を徹底周知する必要があります。それを行う明確な理由があるなら採用するべきです。

OSSライブラリではインターフェースダサダサでは使ってもらえないので、より美しいインターフェースにすることで、利用者の同意を得るというプロセスになると思います。

JavaBeansパターンは暗黙に使い方を示しているところがあるので、可変性に注意して使えばカジュアルで良いと思います。

型パラメータの省略ができるというファクトリーメソッドのメリットは、ダイヤモンド演算子によってJava7ではなくなりましたね。

項目 3 private のコンストラクタか enum 型でシングルトン特性を強制する

enumシングルトンを見たことがありません。そもそも純粋なシングルトンが必要なケースってあまり良いイメージがないですがどうなのでしょうか?

項目 4 private のコンストラクタでインスタンス化不可能を強制する

個人的にはRhinoで

var Util = new Packages.my.domain.HogehogeUtil;

では何故か上手く行かず、

var Util = new Packages.my.domain.HogehogeUtil();

と書く都合上、この方法は使いません。別にユーティリティクラスのインスタンス化を制限しなくてもいいんじゃないかなあと思います。

項目 5 不必要なオブジェクトの生成を避ける

new String("hoge")や、new Boolean(true)のような明らかな悪手は取るべきではないですが、ストイックにインスタンス化を削減する努力が必要なのであればある程度は許容しても良いと思っています。

項目 6 廃れたオブジェクト参照を取り除く

項目 7 ファイナライザを避ける

同意

項目 8 equals をオーバーライドする時は一般契約に従う

項目 9 equals をオーバーライドする時は、常に hashCode をオーバーライドする

ほんとこれ、理解できない人はequalsをOverrideしないでくださいレベルです。

項目 10 toString を常にオーバーライドする

欠点の一つとして掲げられている「一度形式を明示すると未来永劫形式を変更できない」がまさに怖いことと、文字列化することが嬉しくないクラスもあるので、何も「常に」toStringを記述する必要はないのではないかと思います。一方で文字列表現が妥当(例えばHTMLパーサで要素型など)な場合は迷わずtoStringが書けると思います。

項目 11 clone を注意してオーバーライドする

タイトルは注意してと書いてありますが、本文を読むと「使うな」と言わんばかりに読めます。これを読んだあとのJava初級者は可変恐怖症、継承恐怖症を患うと思います。実際は、Java標準ライブラリでも開発しない限り問題に当たることはまれなので、cloneを絶対に作るなとは思いません。cloneの用途としては、そのオブジェクトの型が分かっていない時も、同じ型のインスタンスを作ることができるもっとも簡単な方法だと思います。

やはりタイトル通り「注意してオーバーライドする」が妥当なのだと思います。

項目 12 Comparable の実装を検討する

ほんとこれ、昔ドハマりしたので、イコールじゃないもののcompareToに0なんて返さないように気をつけてください。

項目 13 クラスとメンバーへのアクセス可能性を最小限にする

項目 14 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う

項目 15 可変性を最小限にする

この章を読んだ学習者は、明日からprivateばかり書く可能性があります。privateにしておけば安全だと言わんばかりです。そんなとき僕は「なぜprivateなのか考えてprivateにするべき」といいます。例えば、継承されることを前提としたクラスのスーパークラス側にこんな記述があったとします。

class A {
    protected void foo() {
        hogehoge(1);
    }
    private void hogehoge(int x) {
        // ~省略~
    }
}

サブクラスでfooをOverrideしようとすると、どうなるでしょうか?

class B extends A {
    @Override
    protected void foo() {
        hogehoge(2);
    }
}

これはコンパイルエラーになります。hogehogeがprivateだからです。ではこの時「とにかくprivateにすべし」メンタルを持ったプログラマーはどんな対応を取るでしょうか?こんなコードを書いたりします。

class B extends A {
    @Override
    protected void foo() {
        hogehoge(2);
    }
    private void hogehoge(int x) {
        // ~省略~ (スーパークラスのhogehogeのコピペ)
    }
}

そして、後に、別のプログラマーがhogehogeはをprotectedに変更したらどうなるでしょうか?privateからスコープを広げたのだから通常は問題がないと思うはずです。しかし、これはコンパイルエラーになります。「サブクラスがスーパークラスのメソッドを隠蔽している」ということになるためです。

実際はこの場合、Aというクラスを定義した時点でhogehogeはprivateにするべきではありませんでした。

このようにprivateは、不用意に使いすぎると、コードのコピペを促し、また、隠蔽されているからと品質の悪いコードがリリースされる可能性さえあります。なので自分は「どうしてもprivateにするべき事情がある場合に限って、privateにするべきだ」と言う言い方で、後輩たちを指導しています。

可変性についても同じです。僕も一時、不変性バンザイ厨になりました。でも実際はシリアライズ・デシリアライズ・リフレクションあらゆる方面からその不変性を揺るがしてくるので、今は諦めて「可変性に注意する」という立場にシフトしています。

項目 16 継承よりコンポジションを選ぶ

これは、なんというか、個人的にはJavaが悪いと思っています。ラッパークラスは僕もよく作りますが、これを作りたい場合、Eclipseなどの開発環境を使えば自動的に生成することもできますが、微調整するときに、大量のメンバーをポチポチコピペしてラッパークラスを作る必要があり、時間がもったいないしミスも起こりやすいです。そんなことをしているから、Javaは生産性が悪いと言われても、返す言葉がないのですよね。

個人的には、ライブラリ開発者はコンポジションを使って良いが、一アプリケーション開発者は、なるべくこのような努力をしなくても開発ができるようにフレームワークを設計するべきだと思っています。

項目 17 継承のために設計および文書化する、でなければ継承を禁止する

個人的には継承の禁止は継承を禁止するべき必要がある時にのみクラスにfinalを付けるべきだと思います。

まとめ

このあと項目 78までありますが、概ね言いたかったことを書けたと思うので、割愛します。
読み返してみるとこの本はかなり防御的に偏っている気がしました。この本の指示を従って書くと良いのは次のような事例では良いかと思いました。

  • 一人又は少数のJava言語に精通した人が開発し、多くの人が利用しているライブラリ
  • 仕様がほぼ確定し、リリース後に試行錯誤はあまりしない

一方で、多くの人は次のケースの現場でコードを書いているのではないでしょうか?

  • 複数の人が開発し、全ての人がEffective Javaを読んでいるわけでもなく、かつ、全てのコードを一人の人がレビューしているわけでもない、ゆるやかに共同作業しているケース(Web系の現場でありがち)
  • それが最終的なアプリケーションとしてリリースされるのみでjarファイルが静的にどこか別のプロジェクトでライブラリとして再利用されるわけではない
  • 一回のリリースで完成させるのではなく、反復的にリリースすることでフィードバックを得ながら開発方針を修正している

つまり多少厳密でなくても高速に開発サイクルを回すことを優先したい場合は、ここで推奨されているスタイルをある程度崩す勇気が必要だと思いました。どこまで崩すかの判断には崩すことのメリット・デメリットを理解していなければなりません。それには、多くの現場を経験し、多くのコードを書いた経験が必要でしょう。たった一冊の本を読んだだけで体得できるものではないでしょう。

では、Effective JavaはJava学習者にとって良書なのでしょうか?

この本はJavaでコードを書く上の罠をよく示しています。罠は知らないで踏むと大変危険です。そういった意味で、Effective Javaを読んだレベルになって初めて議論に参加できるステージに立てるのだと思います。新人プログラマーの方々にはやはり、早く一緒に議論がしたいという願いを込めて、Effective Javaを推薦したいと思います。

追記

検索していたら訳者の柴田さんがこんなことを書かれているのを見つけました
『Effective Java 第2版』は、やはり初心者向けではない [プログラミング言語Java教育]

この「クラスが適切にパラメータ化されていれば、ClassCastException がスローされます。」の1文を読んですぐに理解できる受講生はいません。この1文を理解するのに必要な知識はすでに学んでいるのですが、その知識を応用して、すぐに理解するのは非常に難しいようです。

ああ、確かにこういうところわかりにくいかもしれないですね。Genericsの章はずっと先にあるので、なるほど、言われてみればそうだなあと思います。むしろ、こういうレベルまで細かく見れる人は全然問題無いと思います。多くの場合、枝葉のことは分からず、とにかく「Effective Javaに書いてあった」という理由で判断してはいけないと思うだけであり、かつて自分はそうであったという戒めでもあるのです。

多くのハウツー本は、斜め読み+気になった箇所だけ精読というスタイルの読み方が良いと思っていますが、Effective Javaに関しては、斜め読みだけでは不十分で、何度も読み返して良い本だと思いました。

ねこ踏んじゃった系エントリ:Tomcatいじめたら意外と強かったこと #javaee

これは Java EE Advent Calendar 2014 の6日目です。昨日は tq_jappy さんの「Java SE 8とJava EE 7によるアプリケーションのモダナイゼーション~中間ふりかえり~」、明日は yamadamn さんです。

ServletだってJava EE、だったらTomcatだって。。。

Java EEと言えばWildFlyかGlassfishかあるいは商用アプリケーションサーバーで、CDIとかEJBとかJSF....を使うかっこいいやつを言うらしいです。。。でも僕はまだ使ったことがありません。

僕はまだTomcatを使っています。
Tomcatといえば「可愛くないねこ」
_人人人人人人人人人_
> 可愛くないねこ <
 ̄Y^Y^Y^Y^Y^Y^Y^Y ̄
「もっともかわいくないねこ」と言われる公式ロゴ
など、散々な言われ方をしています。。そんないじめなくてもいいのに!たしかに拡張性がありそうで意外とそうでもなかったり、ソース読むと袋小路に陥るなど、開発者に服従しないあたり、そう、とてもねこっぽい。挙動萌えじゃないですか。Mな僕は満足です!
※このエントリの筆者はねこを飼ったことがないので正しくないかもしれませんのでご容赦ください

Java EEのカレンダーにTomcatってどうなのかってことですが、ほら、Servetだって立派なJava EEの一部です。そして2日目のirofさんが

全部を使わなきゃいけないわけじゃありません。

っておっしゃってるじゃないですか!(Java EEを説明してみる #javaee - 2014-12-02 - 日々常々

Java EE全部使おうとしたら実はServletとJSPくらいで良かった、だから僕はJava EEを使っているのであって、TomcatがJava EEアドベントカレンダーに出てきても許してにゃん!

2014年の僕の中でのトピックは、Tomcatの戦闘力の高さ

今年の個人的なトピックは、わけあってTomcatの底力を思い知ったことでした。半端ないです。いじめて反撃食らった心境です。童謡にもあるでしょ踏んだらひっかくって!

ということで、試しに踏んでみたいと思います。

実験概要

この実験では、大量のwebアプリケーションを作ります。Webアプリケーションはこんな簡単なServletだけでいいです。

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        writer.println("こんにちは世界");
    }
}

これをなんとかしてHello.warというwarファイルにしたとします。次にこんなスクリプトを書いて、大量生成します

> jrunscript -e "for(var i=0;i<1000;i++)cp('Hello.war','Hello' + i + '.war');"

実験:1000個のWebアプリケーションを一気にデプロイするのに必要な時間と消費メモリ

Tomcatはwarファイルをwebappsディレクトリ以下に配置すれば自動的にWebアプリケーションとしてロードします。なので大量に作ったwarファイルを一気にwebappsに移動してみましょう。

その時のログが以下の通り。

情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello992.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello992.war has finished in 47 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello993.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello993.war has finished in 31 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello994.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello994.war has finished in 31 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello995.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello995.war has finished in 31 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello996.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello996.war has finished in 31 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello997.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello997.war has finished in 32 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello998.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello998.war has finished in 31 ms
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Webアプリケーションアーカイブ H:\java\apache-tomcat-7.0.54\webapps\Hello999.war を配備します
12 04, 2014 12:07:33 午前 org.apache.catalina.startup.HostConfig deployWAR
情報: Deployment of web application archive H:\java\apache-tomcat-7.0.54\webapps\Hello999.war has finished in 32 ms

こんな感じで粛々とロードします。(Tomcatの最新安定版は8.x系ですが、これは実験した時のバージョンと同じで7系にしています。8でも同じだと思うけど試してません)

1分以内に全部のロードが終わりました。
その際のリソース使用量はこんな感じです。

徐々に上がって、メモリ使用量750MBあたりで安定

本当にデプロイされているのか、全部のアプリケーションにアクセスしてみましょう。

> jrunscript -e "for(var i=0;i<1000;i++)cat('http://localhost:8080/Hello' + i + '/hello');"

ねこだけにcat関数!サクッと値返してます!

こんにちは世界
こんにちは世界
こんにちは世界
こんにちは世界
こんにちは世界

※Windowsのコマンドプロンプトだと、文字化けします。文字コードがShiftJISじゃないとダメみたいです。

管理コンソールだってちゃんと機能します。ほら!
アプリ1000個くらいデプロイしても管理画面はさくっと機能します

最後に、アンデプロイです。これは、warファイルを消すと、それを検知して勝手に配備解除してくれます。解除の順番はばらばらで、配置よりはゆっくりですが、でも確実に配備解除されました。おりこうですね!

情報: コンテキストパス /Hello465 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:42 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello924 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:43 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello332 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:44 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello152 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:45 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello179 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:45 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello194 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:46 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello528 のWebアプリケーションの配備を解除します
12 04, 2014 12:12:47 午前 org.apache.catalina.startup.HostConfig undeploy
情報: コンテキストパス /Hello370 のWebアプリケーションの配備を解除します

同じ実験をGlassfishでやってみる

同じ実験をGlassfishでやってみようと思います。結果を知ってるんで、やりたくないんですけどね。
Glassfishも同じように下記の場所にwarをコピーすると自動でデプロイされる仕組みがあります。

/glassfish/domains/ドメイン名/autodeploy

それでは、さっきの方法で。。。。warを大量コピーしてみます。

[#|2014-12-04T01:10:31.968+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623031968;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:31.984+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623031984;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:31.984+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623031984;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:32.031+0900|INFO|glassfish 4.0|javax.enterprise.web|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032031;_LevelValue=800;_MessageID=AS-WEB-GLUE-00172;|
  Loading application [Hello234] at [/Hello234]|#]

[#|2014-12-04T01:10:32.062+0900|INFO|glassfish 4.0|javax.enterprise.system.core|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032062;_LevelValue=800;|
  Hello234は、547ミリ秒で正常にデプロイされました。|#]

[#|2014-12-04T01:10:32.062+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.autodeploy|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032062;_LevelValue=800;_MessageID=NCLS-
DEPLOYMENT-00035;|
  [AutoDeploy]自動デプロイは正常に実行されました : H:\java\glassfish4\glassfish\domains\domain1\autodeploy\Hello234.war。|#]

[#|2014-12-04T01:10:32.077+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.autodeploy|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032077;_LevelValue=800;_MessageID=NCLS-
DEPLOYMENT-00027;|
  Selecting file H:\java\glassfish4\glassfish\domains\domain1\autodeploy\Hello805.war for autodeployment|#]

[#|2014-12-04T01:10:32.531+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032531;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:32.546+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032546;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:32.546+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032546;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:10:32.609+0900|INFO|glassfish 4.0|javax.enterprise.web|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032609;_LevelValue=800;_MessageID=AS-WEB-GLUE-00172;|
  Loading application [Hello805] at [/Hello805]|#]

[#|2014-12-04T01:10:32.640+0900|INFO|glassfish 4.0|javax.enterprise.system.core|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417623032640;_LevelValue=800;|
  Hello805は、563ミリ秒で正常にデプロイされました。|#]

Tomcatのときは数10msで1アプリ起動していましたが、こちらはロード数が多いと徐々に遅くなっていきます。最終的には1アプリ7秒ほどの時間を費やしていました。

[#|2014-12-04T01:43:19.334+0900|INFO|glassfish 4.0|javax.enterprise.web|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417624999334;_LevelValue=800;_MessageID=AS-WEB-GLUE-00172;|
  Loading application [Hello200] at [/Hello200]|#]

[#|2014-12-04T01:43:19.422+0900|INFO|glassfish 4.0|javax.enterprise.system.core|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417624999422;_LevelValue=800;|
  Hello200は、7,828ミリ秒で正常にデプロイされました。|#]

[#|2014-12-04T01:43:19.424+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.autodeploy|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417624999424;_LevelValue=800;_MessageID=NCLS-
DEPLOYMENT-00035;|
  [AutoDeploy]自動デプロイは正常に実行されました : H:\java\glassfish4\glassfish\domains\domain1\autodeploy\Hello200.war。|#]

[#|2014-12-04T01:43:19.426+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.autodeploy|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417624999426;_LevelValue=800;_MessageID=NCLS-
DEPLOYMENT-00027;|
  Selecting file H:\java\glassfish4\glassfish\domains\domain1\autodeploy\Hello251.war for autodeployment|#]

[#|2014-12-04T01:43:27.075+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007075;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:43:27.086+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007086;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:43:27.091+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.common|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007091;_LevelValue=800;|
  visiting unvisited references|#]

[#|2014-12-04T01:43:27.280+0900|INFO|glassfish 4.0|javax.enterprise.web|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007280;_LevelValue=800;_MessageID=AS-WEB-GLUE-00172;|
  Loading application [Hello251] at [/Hello251]|#]

[#|2014-12-04T01:43:27.392+0900|INFO|glassfish 4.0|javax.enterprise.system.core|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007392;_LevelValue=800;|
  Hello251は、7,952ミリ秒で正常にデプロイされました。|#]

[#|2014-12-04T01:43:27.392+0900|INFO|glassfish 4.0|javax.enterprise.system.tools.deployment.autodeploy|_ThreadID=119;_ThreadName=AutoDeployer;_TimeMillis=1417625007392;_LevelValue=800;_MessageID=NCLS-
DEPLOYMENT-00035;|
  [AutoDeploy]自動デプロイは正常に実行されました : H:\java\glassfish4\glassfish\domains\domain1\autodeploy\Hello251.war。|#]

glassfishのロード時のvisualvm表示、GCが頻繁に走っているのか、ヒープ使用量のグラフが穏やかでない

結局、約35分にしてロードが終わりました。では、せっかくのアプリケーションサーバーなので管理コンソールを開いてみましょう。。。。

管理コンソールでアプリケーションクリック「長時間実行中のプロセスが検出されました。お待ちください...」と表示されて固まっている

画面が返ってこなくなりました

死んだ魚

勝ち誇るTomcat

でも、TomcatよりもGlassfishはずっと多くの機能を持っているんだから当然といったらそうですね。そもそもこの実験が業務で役立つことって相当レアケースかもしれません。それでも、僕は自分に合ったツールはやはりTomcatなのかなと思い直しました。そう、今なら言える!

_人人人人人人人人人人人人_
> Tomcatかわいい <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

WildFlyは実験を行った当時はまだリリースしてなかったので試していませんでした。機会があったらやってみようと思います。もしかするとUndertowは優秀らしいので、Tomcatよりも更によい結果をだすかもしれませんね!

本題とは関係ないおまけ:スクリプトにjrnscriptを多用したわけ

今回、WindowsでもLinuxでもMacでも同じコマンドラインを流用できるように、jrunscriptのワンライナーで記述しました。これは、JVM内包のRhinoまたはnashornを使ってシェルのように使えるもので、意外と知られていませんが、下記のドキュメントにあるシェル風のグローバル関数が使えます。
GLOBALS
(これ、見つけづらいところにあるので、毎回探しちゃいます。これからはこの記事を参照すれば探さなくて良くなりますね!)

例えば、今回やったように、cpでコピーしたり、catで簡易curlにしたりできます。これはちょっとしたインストーラのようなものを作るときに便利です。困ったらJavaAPIが使えるので、安心です。なので、僕は普段Windowsを使っていてWSHとかPowershellは苦手なので、ちょっとしたスクリプトを書くときに、jrunscriptをよく使います。

JDBCでResultSet.nextを使って逐次処理しているからといって安心しない。setFetchSizeを忘れずに!

JDBCを直接使っている人向けの話。いねえよとか言わない!おれがいる!

try (ResultSet result = statement.executeQuery();) {// このクエリは数百万件とかすごい量のデータを返すとする

とかしたとして、ブロックの中で

while (result.next()) {
    // ここで順次的に処理をする
}

とか書いて置いて、ほらちゃんとresult.nextで順次的に処理してるし、try with resourceでcloseもバッチリ!といっても、俺偉いとか思わない。executeQueryを実行した直後のヒープダンプがコレ。

ResultSetのインスタンスが60%以上取っている

まだresult.next()していない。そんな!ResultSetはデータそのものじゃなくて、接続の管理クラスみたいなやつだから、実際はnextして順次的にDBから値をもらうんだって教わった。なのにどうもメモリを食ってる。

ヒープダンプの中身を解析してみる。

rowsというVectorに全部のデータが格納されている

rowsというVectorがデカイ。

_人人人人人_
> Vector <
 ̄Y^Y^Y^Y ̄

WeakReferenceでもSoftReferenceでもないタダのVectorなので、メモリにガッツリ確保しております。そう、ResultSetはパフォーマンスのために、結果のうち数行をキャッシュするのだ!

そのキャッシュのサイズを制御するのがこのメソッド

https://docs.oracle.com/javase/6/docs/api/java/sql/Statement.html#setFetchSize(int)

setFetchSize

void setFetchSize(int rows)
                  throws SQLException
Gives the JDBC driver a hint as to the number of rows that should be fetched from the database when more rows are needed for ResultSet objects genrated by this Statement. If the value specified is zero, then the hint is ignored. The default value is zero.

なるほど。フェッチする行サイズを(あくまでヒントとして)指定できるらしい。デフォルトでは0、すなわち無指定状態のようだ。

じゃあ、ヒント無しの場合はどうなるのだろう。

http://grepcode.com/file/repo1.maven.org/maven2/postgresql/postgresql/8.4-702.jdbc3/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java#AbstractJdbc2ResultSet.next%28%29

row_offset += rows.size(); // We are discarding some data.
//
// 省略
//
int fetchRows = fetchSize;
if (maxRows != 0)
{
    if (fetchRows == 0 || row_offset + fetchRows > maxRows) // Fetch would exceed maxRows, limit it.
        fetchRows = maxRows - row_offset;
}

rowsが件のVectorであるので、つまり全部のデータをfetchRowsしようと試みてるってこと??

なので、大きめのクエリを流そうとしている場合は、setFetchSizeはほぼ確実に呼び出すべき。ただ、大きめにするとメモリを食うし、小さめだとパフォーマンスが不利になるのでその辺りは統計を取って最適値を模索するしかないかと。

あと、あくまで行のキャッシュであるので、一行にtext型を格納したドデカイカラムがあったりするとやばそうです。