SpotBugs 3.1.0 RC2を使用してみた。

けっこうはまったのでメモとして。

SpotBugsはfindbugs本体の開発がアレな状態なのでフォークして生まれたツールです。
生まれてまだ日が浅いため、FindBugsとの互換性がけっこう高いです。
なので、FindBugsを使用していた人がSpotBugsに乗り換えられるようにということで、実行までのメモ。

バイナリとしての配布はされていない?ようなので、自前でビルドをする必要があります。
以下のサイトから落としてきてください。
https://github.com/spotbugs/spotbugs

ドキュメントには
gradlew buildを実行すれば普通に出来るよぐらいに書いてありますが、できません。
一部コマンドがgitに依存しているため、gitをインストールしていない場合(Download ZIPでダウンロードしてきた場合)はspotbugs/build.gradleの155行目〜159行目までをコメントアウトしてください。*1

task distSrcZip(type:Exec) {
  commandLine 'git', 'archive', '-o', "${buildDir}/distributions/spotbugs-${project.version}-source.zip",
    '--prefix', "spotbugs-${project.version}/", 'HEAD'
}
tasks['assembleDist'].finalizedBy distSrcZip

//task distSrcZip(type:Exec) {
//  commandLine 'git', 'archive', '-o', "${buildDir}/distributions/spotbugs-${project.version}-source.zip",
//    '--prefix', "spotbugs-${project.version}/", 'HEAD'
//}
//tasks['assembleDist'].finalizedBy distSrcZip

チケット切った。(修正済み)
https://github.com/spotbugs/spotbugs/issues/178

これでビルドが出来るはずです。

ビルド後にdestributionsに入ってるzipを使用するのが正しいです。
間違えてもlibsに入ってるjarを使用してみようとしてはいけない。(間違えた)

mavenの場合

以下のチケットに書いてあるとおり、maven-findbugs-pluginからfindbugsへの依存をSpotBugsに入れ替えてあげればよいらしい。
https://github.com/spotbugs/spotbugs/issues/8

なお、この場合は上記の自前でのコンパイルは不要。
ちゃんと最新版の3.1.0-RC2もmaven centralにリリースされています。


antの場合

antから使用する場合、たぶん、SpotBugsのantプラグインを利用すればいい(はずな)のですが、findbugsからのお試しということで、findbugsのantプラグインを使用できるように以下のように修正します。

  • FINDBUGS_HOMEをspotbugsのフォルダに変更する。
  • lib/spotbugs.jarをlib/findbugs.jarにリネーム

これだけで、SpotBugsが呼び出せるはずです。

使用した感想

判っていたことですが、本家findbugsがパッチを取り込んでくれなくなってしまったため立ち上がったプロジェクトで本家findbugsが正常に稼動しなおしたらこのプロジェクトも終了するって公言されていることもあり、互換性が保たれたままfindbugsのように使用できる、ただしそれを超えたものを期待するものでもないのかなぁという印象です。
環境周りは本家ツールに対してやはりつらい。。。。。。
cccでのセッションでは20%以上の高速化が測られているとのことでしたので、現状としてはそれがどうしても欲しい人向けではないでしょうか。

    • -

2017/05/26 追記

自分の環境(Windows Server 2012 R2 + JDK8u102)ではspotbugsを使用すると逆に20%ほど動作が伸びてしまう結果となりました。
セッション内では高速化が図られているとの話でしたが、環境によるのかもしれません。ご注意ください。

*1:現在はすでに修正されています。

セキュリティさくら分科会(仮称)第二回に参加してきた

なにやってたかの全容は以下の記事を参照。
http://calmery.hatenablog.com/entry/2017/05/14/221218

Docker上でStruts2アプリケーションを動かしたけど、そもそもTomcatってなによ?ってところの説明が抜けていたので、後ろのほうのメンバーは完全に?????という状態になっていた(ので軽く追加説明はした)
せっかくStruts2のサンプルアプリケーションのwarをデプロイしたのにアプリケーションの画面を見ることなく、Tomcatにアクセスだけして終わってたし・・・・・・
ちなみに、dockerのアプリケーションを動かした時点で、以下のURLにアクセスするとデプロイしたStruts2アプリケーションに繋がります。
http://localhost:8080/struts2-rest-showcase

ただ、初回起動時に、勝手にセッションidがURLについてしまってエラーになるので、2回上のURLにアクセスする必要がある模様。*1

なお、この名前はDockerfileで読み込んでるwarファイルの名前と一緒になります。

私は裏側でTomcatの設定ファイルをDockerfileから追加してTomcatの挙動が変わるよたのしー。とかやってたけど、今回の感じだとStruts2のアプリケーションを脆弱性で壊してみてもDockerを起動しなおすと元に戻っちゃうよみたいなところをやったほうが判りやすかったんじゃないだろうか?とは思った。

ただ、Dockerやらないとなぁと思いつつやれてなかったのを一歩踏み出せたというのはありがたい。

*1:これはたぶんサンプルアプリケーションのバグ。直さないけど

Javaの文字列変換にどれくらい時間がかかってるかを測定してみる

前回の記事で、文字として扱わない状態で改行コードで分割することでパラレル実行向けに最適化されたようだと書きましたが、そういえば、Javaのバイトからの文字列への変換にどれくらいコストがかかるのかを調べたことがなかったので、調べました。

なお、環境は前回と同じ。
PCとしては5年ぐらい前のCore i7だけどHDDなWindows 10のものを使用しています。
使用したJavaのバイナリはjdk-9-ea+167_windows-x64_bin.exeです。

ということで、以下の3通りで調べました。


//文字列として扱う
BufferedReader reader = Files.newBufferedReader(Paths.get(fileName));
while (reader.readLine() != null) {
}
//InputStreamで読む
InputStream inputStream = Files.newInputStream(Paths.get(fileName));
byte[] buff = new byte[8192];
while (inputStream.read(buff) == 8192) {
}
//InputStreamで読んだ後に文字列に自前で変換する。(参考用)
InputStream inputStream = Files.newInputStream(Paths.get(fileName));
byte[] buff = new byte[8192];
while (inputStream.read(buff) == 8192) {
    new String(buff);
}

ちなみに、InputStreamで使用されているバッファサイズ(8192)はBufferedReaderを作成する場合にデフォルトで使用されるバッファサイズです。
BufferedReaderとバッファサイズを併せました。
参考用は前回作成したテストデータだとBufferedReaderのほうがループ回数が極端に多くなってしまうため、ループ回数の差がどれくらいの影響を与えてるのかを見るための参考用のデータです。BufferedReaderの中ではバッファの中からさらに1行ごとに文字列を生成されているはず。

結果

  • BufferedReader->7.2秒ぐらい
  • InputStream->0.9秒ぐらい
  • InputStreamで自前で文字列化->5秒ぐらい

やはりそれなりに時間がかかるんだなぁという印象でした。

使ったコードはこちら
https://gist.github.com/megascus/04cab0e0c90d640c2e575e69c77546c4

Java 9でjava.nio.Files#linesがパラレル実行向けに最適化されたと聞いたので試してみた

さくらばさんから以下のような話を聞いたので。

なお、対応するチケットは以下となっています。

(fs) Files.lines needs a better splitting implementation for stream source

JavaDocは以下の通り。
java.nio.Files#lines

ドキュメント上は文字コードUTF-8, US-ASCII and ISO-8859-1の場合のみサポートとなっていますが、実装を調べたところ、さらにファイルサイズが2Gを超えてもいけないようです。
中身のファイルのFileChannelをByteBufferクラスにダイレクトにリンクをし扱うようにしているためで、Javaの配列が2Gまでしか取り扱えないことによる制限のようです。

なお、高速化の方法ですが、従来はファイルの中身のバイト文字列を文字として変換してから1行を取り出していたのを、バイト文字列のまま1行取り出してから文字に変換するようにしたという感じのようです。
中身をbyteのまま\rとか\nと比べてるので、\rと\nがバイト単位で比べた時にほかの文字としても使われている文字コード(例えばUTF-16)の場合に不具合になってしまうため、確実に問題がない文字コードのみをサポートしている模様。
JavaDocを読む限りFileChannel自体は非同期からのアクセスに最適化されていますが、それを文字列の行として取り扱うBufferdReaderクラスは非同期からのアクセスに最適化されていませんでした。それを取り除くことで、非同期処理の恩恵を受けやすくしたみたいです。

ということで試してみた。

2Gbyteを基準に従来のコードと新しい処理が分かれるため、2Gを基準にシングススレッド、マルチスレッドでどれくらい処理時間に差が出るのかを雑に試してみました。

PCとしては5年ぐらい前のCore i7だけどHDDなWindows 10のものを使用しています。
使用したJavaのバイナリはjdk-9-ea+167_windows-x64_bin.exeです。

使ったコードはこんな感じ。filterを入れてるのは、JITによって処理消えちゃわないよね?ってあたりが心配になっただけで、あまり意味はないです。

//マルチスレッド
Files.lines(Paths.get(fileName), UTF_8).parallel().filter(s -> true).forEach(s -> {return;});
//シングルスレッド
Files.lines(Paths.get(fileName), UTF_8).filter(s -> true).forEach(s -> {return;});

大体以下のような感じ。

  • マルチスレッドで最適化されている(2G以下) -> 2.6秒程度
  • マルチスレッドで最適化されていない(2G超) -> 9秒程度
  • シングルスレッドで最適化されている(2G以下) -> 6.6秒程度
  • シングルスレッドで最適化されていない(2G超) -> 7.1秒程度*1

ということで、ものすごく最適化されているという結果でした。

使ったコードはこちら。
https://gist.github.com/megascus/5bdf8d7d45b06cb639c40ce67d1533a3

コード解説(5/7追記)

もともとのlinesメソッドの中身はFiles#newBufferedReaderを呼び出してBufferedReaderを作成した後にBufferedReader#linesを呼び出しているだけでした。
そこからUTF-8かUS-ASCIIもしくはISO-8859-1であった場合に、新しい処理が呼び出されるようになりました。

(元のコード)
64行目と、110行目〜122行目まで
http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/4472aa4d4ae9#l2.64
http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/4472aa4d4ae9#l2.110

(新しい処理の条件、ファイルシステムがシステムのデフォルトである場合、かつ文字コードが上記の場合のみ)
http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/4472aa4d4ae9#l2.71
(使用できる文字コード自体は下記で定義)
http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/4472aa4d4ae9#l1.71
※つまりZipFileSystem等の場合はこの処理には入らないので、一度ローカルファイルに直す必要がある。

新しい処理の中で、FileChannelを開いた後にファイルサイズを確認し、2Gを超えていた場合は元の処理に戻しています。
http://hg.openjdk.java.net/jdk9/dev/jdk/rev/4472aa4d4ae9#l2.90

実際の処理はFileChannelLinesSpliteratorの中で行っていますが、
ByteBufferとして扱う場所は以下でやっています。
http://hg.openjdk.java.net/jdk9/dev/jdk/rev/4472aa4d4ae9#l1.203
http://hg.openjdk.java.net/jdk9/dev/jdk/rev/4472aa4d4ae9#l1.180

並列処理として扱うときに部分分割するメソッドとして、Spliterator#trySpritが用意されていますが、
そのなかでバイト文字列のまま分割するようになりました。(210行目〜)
http://hg.openjdk.java.net/jdk9/dev/jdk/rev/4472aa4d4ae9#l1.210

分割された後はByteBufferをソースにしたBufferedReaderを作成して処理をしているようです。
http://hg.openjdk.java.net/jdk9/dev/jdk/rev/4472aa4d4ae9#l1.121

BufferedReaderができてしまえばあとは変わりません。

trySpritの処理まではシングスルレッドで行うしかなかないので、そこでのバイトから文字列への変換をせずに、マルチスレッドで処理をする時に初めて文字列へ変換することで、処理を分散しているということでしょうか。

文字列変換にどれくらいのコストがかかっているかは次の記事で。
http://d.hatena.ne.jp/megascus/20170507/1494171105

*1:途中でなんかへんな処理が入ったかブレが大きかった

Java 8でのProcess#destroyとProcess#destroyForciblyの環境差異について

Java 8のJavaDocでProcess#destoryとProcess#destroyForciblyメソッドを調べてみるとすごい怪しい記述がされています。

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Process.html


destroy

サブプロセスを終了します。このProcessオブジェクトが表すサブプロセスが強制終了されるかどうかは、実装によって異なります。

destroyForcibly

サブプロセスを終了します。このProcessオブジェクトが表すサブプロセスは強制終了されます。
このメソッドのデフォルト実装では、destroy()を呼び出すため、プロセスが強制終了されない場合があります。このクラスの具象実装では、このメソッドを準拠した実装でオーバーライドすることを強くお薦めします。ProcessBuilder.start()およびRuntime.exec(java.lang.String)から返されたProcessオブジェクトに対してこのメソッドを呼び出すと、プロセスが強制終了されます。

注: サブプロセスがただちに終了しない場合があります。つまり、destroyForcibly()が呼び出された後ほんのしばらくの間、isAlive()がtrueを返すことがあります。このメソッドは、必要に応じてwaitFor()にチェーンされることがあります。


実装によって異なりますとか、デフォルト実装ではとか・・・・・・

ということで調べた。

調べたら以下の通りでした。

  • WindowsではdestroyでもdestroyForciblyでもSIGTERM(kill -15)が呼ばれ、必ず強制終了される。
  • LinuxUnix系OSではdestroyではSIGKILL(kill -9)が呼ばれ、destroyForciblyではSIGTERM(kill -15)が呼ばれる。

なお、WindoesでSIGKILLを呼び出したい場合は、自分でnative実装を作成してJNIで呼び出すか、Processのインスタンスからプライベートフィールドとして定義されてるhandleを取得し、別コマンドとしてkilltask /PID pidを呼び出すのが良さそうです。

しかし、JavaDocでうたってるデフォルト実装通りなのがWindowsのみってどういうことなの・・・・・・・





Windows参考資料
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/windows/classes/java/lang/ProcessImpl.java#l475
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/native/java/lang/ProcessImpl_md.c#l435
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/windows/classes/java/lang/ProcessImpl.java#l309

WindowsでもSIGKILLを送れないのかというとそんなことはあるわけがなくて、JavaではなぜかTerminateProcess関数が呼び出されているだけです。ExitProcess 関数というのも用意されています。
https://msdn.microsoft.com/ja-jp/library/cc429376.aspx

LinuxUnix系OS参考資料
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/solaris/classes/java/lang/UNIXProcess.java.bsd#l293
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/solaris/classes/java/lang/UNIXProcess.java.linux#l295
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/solaris/classes/java/lang/UNIXProcess.java.solaris#l247
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/solaris/native/java/lang/UNIXProcess_md.c#l715

log4jdbcで設定したはずなのに有効にならない場合

log4jdbcのjarを依存ライブラリに入れて、jdbc urlにlog4jdbcを入れたのにlog4jdbcが有効にならない場合がある。
DriverManagerがDBへのコネクションを作成するときにlog4jdbcで使用されているロガー類がすべて無効化されている場合にはlog4jdbcがラッパーを作成せずに、log4jdbcでのログ出力が行われない。
https://github.com/arthurblake/log4jdbc/blob/master/src-jdbc4/net/sf/log4jdbc/DriverSpy.java#L740-L760
https://github.com/arthurblake/log4jdbc/blob/master/src-jdbc4/net/sf/log4jdbc/Slf4jSpyLogDelegator.java#L90-L95

これは、起動時にログ設定を無効化していて、起動後に改めてログ設定を正しいものに変更するかつコネクションプールを使用して初期にまとめてコネクションが取得される場合に問題となることがある。というか、なった。

なお、この挙動はlog4jdbcではSlf4jSpyLogDelegatorクラスで定義されているが、外側から入れ替える手段が存在しないため、問題がある場合はlog4jdbc互換のほかのラッパーを使用する必要がある。

たとえば、log4jdbc-log4j2は入れ替える手段があるらしい。
http://qiita.com/ksby/items/7a2cb97215b252bf41b1

それにしても、log4jdbc系はいろいろ亜流があって何がなんだかわからないですな・・・・・・