daybreaksnow's diary

私を私と呼びたい

[Java]JavaプログラムをWindowsでサービスとして起動する

Java Service Wrapperを使って、前回作成したjarをWindowsサービスにして実行する。
(コンソール出力を行うだけのプログラムなのでサービスにする意味は全くないが。)

以下のページを参考にした
http://wrapper.tanukisoftware.com/doc/japanese/qna-service.html
※公式トップのサポート→質問と回答→JavaアプリケーションをWindowsサービスの1つとして動かすには?

1.Java Service Wrapperのダウンロード

以下からWindows x86 スタンダード版 64bitをダウンロード
http://wrapper.tanukisoftware.com/doc/japanese/download.jsp

2.ディレクトリの準備

サンプルプログラムを以下に配置した

App
  bin
    springSample.jar

3.wrapperの必要なファイルのコピー

1.でダウンロードしたzipを展開し、必要なファイルを以下の通りコピーする。
また、ログファイル用にlogsディレクトリを作っておく

bin/wrapper.exe			→ App/bin
lib/wrapper.jar			→ App/lib
lib/wrapper.dll			→ App/lib
src/conf/wrapper.conf.in	→ App/conf/wrapper.conf
src/conf/wrapper-license-time.conf→ App/conf/wrapper-license-time.conf
src/bin/App.bat.in		→ bin/App.bat
src/bin/InstallApp-NT.bat.in	→ bin/InstallApp-NT.bat
src/bin/UninstallApp-NT.bat.in	→ bin/UninstallApp-NT.bat

最終的なディレクトリ構成は以下の通り

ServiceWrapperSample
  bin
    springSample.jar
    wrapper.exe
    App.bat
    InstallApp-NT.bat
    UninstallApp-NT.bat
  lib
    wrapper.jar
    wrapper.dll
  conf
    wrapper.conf
    wrapper-license.conf
  logs

4.wrapper.confの修正

クラスパスの追加

実行するJarファイルを指定

wrapper.java.classpath.2=springSample.jar
エントリクラスの設定

完全修飾子で指定

wrapper.app.parameter.1=spring.sample.SpringSample
サービス名の設定

以下を適当に設定

wrapper.name=SpringSample_servicename
wrapper.displayname=SpringSample_dispname
wrapper.description=SpringSample_description

5.動作確認

bin/App.batを実行
出力はlogs/wrapper.logに吐かれる

6.サービス化と解除

bin/InstallApp-NT.batを実行。
自分の環境では以下のメッセージが出力されたが、サービスには登録されていた。

Failed to write to console: ハ

サービス化の解除はbin/UninstallApp-NT.batを実行すればOK

その他Tips

Javaプログラム引数の追加

wrapper.app.parameter.2=spring-conf_test.xml

VM引数は以下

wrapper.java.additional.1=

Javaのヒープメモリ最大値設定(※これはVM引数に-Xmx指定するのと同じなのだろうか?)

wrapper.java.maxmemory=1024


なお、前回のjar出力方法で、以下のどちらでも上記の設定で動作した。

1.生成されるJARに必須ライブラリーを抽出
3.生成されるJARの隣のサブフォルダーに必須ライブラリーをコピー

3.については、bin以下にlibフォルダを追加し、エクスポート時と同じディレクトリ構成にした。
もしlibフォルダの場所を変える場合は、以下のようにwrapper.confにクラスパスを追加する事で対応できる。

wrapper.java.classpath.3=../lib/spring.jar
wrapper.java.classpath.4=../lib/commons-logging.jar

[Java]Eclipseで実行可能JARファイルでエクスポートした時のディレクトリ構成の違い

エクスポートするプロジェクトのディレクトリ構成

SpringSample
  src
    spring
      sample
        SpringSample.java
  conf
    spring-conf.xml
  lib
    spring.jar
    commons-logging.jar

1.生成されるJARに必須ライブラリーを抽出

SpringSample.jar
  META-INF
    MANIFEST.MF
    LICENSE(commons-loggingのMETA-INFの中身)
    spring.tld(spring.jarのMETA-INFの中身)
  org
    springframework
    ...(spring.jarの中身が展開されている)
  spring
    sample
      SpringSample.java
  spring-conf.xml
MANIFEST.MFの中身
Manifest-Version: 1.0
Class-Path: .
Main-Class: spring.sample.SpringSample

2.生成されるJARに必須ライブラリーをパッケージ

SpringSample.jar
  META-INF
    MANIFEST.MF
  org
    eclipse/jdt/internal/jarinjarloader
      JarRsrcLoader.class
      ...
  spring
    sample
      SpringSample.java
  commons-logging.jar
  spring.jar
  spring-conf.xml
MANIFEST.MFの中身
Manifest-Version: 1.0
Rsrc-Class-Path: ./ spring.jar commons-logging.jar
Class-Path: .
Rsrc-Main-Class: spring.sample.SpringSample
Main-Class: org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader

3.生成されるJARの隣のサブフォルダーに必須ライブラリーをコピー

SpringSample.jar
  META-INF
    MANIFEST.MF
  spring
    sample
      SpringSample.java
  spring-conf.xml
SpringSample_lib
  commons-logging.jar
  spring.jar
MANIFEST.MFの中身
Manifest-Version: 1.0
Class-Path: . springSample_lib/spring.jar springSample_lib/commons-logging.jar
Main-Class: spring.sample.SpringSample

構成の違いのまとめ

・生成されるJARに必須ライブラリーを抽出
 依存するjarの中身を全て展開する。そのため、クラスパスはカレントのみ

・生成されるJARに必須ライブラリーをパッケージ
 jar内のルートディレクトリに依存するjarをコピー、指定したMainクラスではなく、JarRsrcLoaderが実行される。

・生成されるJARの隣のサブフォルダーに必須ライブラリーをコピー
 依存するjarの中身は「プロジェクト名_lib」にコピーされ、クラスパスはそちらを参照する

[声優イベ]11/2芝浦祭 内田彩・斎藤楓子トークショーログ

細かい口調とかは適当。

司会:内田さんの出身は群馬ですが、群馬といえばウィンタースポーツが盛んですね。ちなみに私も群馬出身です。
内:どこ出身なの?
司:高崎です
内:敵だね (キリッ
内:(斎藤さんに向かって)高崎はね、JRが通ってるんだよ!新幹線も!
斎:都会じゃん。
司:いやJR通ってないところに住んでる人いるんですかね
(会場ちらほら挙手)
内:敵に回すと怖いんだよ~
司:たくさん電車が通っててすみません
内:むかつく~

・声優になったきっかけ
内:セーラームーン。変身願望がある
斎:中学のころのゲームで、ボイス付きので最初に魔法を詠唱するのがあって、私も詠唱したい!って思った(#時期的にインデグネイションか?)

・当時新設だったJTBエンタテイメントアカデミーに入った理由は
内:新設だから、人欲しがってるだろうし、ここでダメだったら諦めようと思ってた。あと緒方恵美さんがいたから、ちゃんとしたところだろうって・。
斎:完全に同じ。全部言われた
内:ごめんねー。代わりに緒方恵美さんの好きなところ10個言っていいよ
斎:それは無茶ぶりじゃない…?

内:セーラーウラヌスと蔵馬が好きで、両方とも緒方恵美さんがやっているんだけど、ウラヌスの方は最初知らなくて、運命的だった

・斎藤さんが演じる椎名はクールだけどかわいいもの好きですが、演じるのは大変ではなかったですか
内:ふーちゃんはねー、全然そんなことないよねー
斎:なんで割り込んでくるのw
内:この前の飲み会でもねー、あ、言わないほうがいい?
斎:リハでやってないこと言うのやめようよw

・斎藤さんは女子校出身ですが
斎:みんながイメージしてる女子校は幻だよ。
斎:演劇をやっていたけど、常に男役だった。バレンタインデーには後輩からチョコをもらってホワイトデーのお返しをしたりしたけど、ガチ告白はなかった。
内:もし告白されてたらどうしてた?
斎:えっ…相手次第、かな…

・寸劇
司:この内容は実在の人物とも、実在する非実在人物とも関係はありません。
#基本ラブライブネタとABネタ他。

内:今度の文化祭どうー?私ダンス部でステージやるんだけど、センターの子ががんばりすぎちゃって倒れちゃいそう。
斎:私も忍者役で逆さ吊りになったりするし。衣装もリバーシブルで表が町娘、裏が忍者でどう作ったらいいのか
内:私裁縫得意だからやろうか?

斎:え、生徒会長ダンス部なの?
内:そうなの、かしこいしー、かわいいしー。

内:なんか今度放送も任されちゃって。先輩がFX?ってのでお金溶かしちゃってそれどころじゃないって

・ロボットを操縦してタイムアタック
内:これ買ったほうが何かないんですか?負けたほうが罰ゲームとか
司:じゃあ勝ったほうが無茶ぶりとかでいいんじゃないですかね
斎:何にしようかな…何がいい?
会場:ものまねー
内:ものまね?私できるのない(すごいちっちゃな声で)
斎:その顔はなんか持ってるだろー
#結局決まらず
斎:じゃあさ、彩が一番かわいいと思う言い方で、来てくれてありがとうって言ってよ
彩:私が一番かわいいって思えばいいんだよね、よし
斎:ニーズには答えてね…

[Java]zipファイルの解凍ではまる(ファイル末尾にヌル文字が1バイトできる)

Javaからzipファイルを解凍したい。
java.util.zipパッケージのクラスを使うと、文字コードUTF-8固定で、Windows環境で圧縮された日本語ファイルの解凍時に文字化けが発生する。
そのため、apache-antに入っているorg.apache.tools.zipパッケージのクラスを使うとよい。
参考:Java Zipファイルメモ(Hishidama's java zip Memo)


以下のサイトでjava.util.zipを使ったサンプルがあり、これをorg.apache.tools.zipを使うように書き換えた。
Java Sample ZIP解凍サンプル


しかし、利用するクラスを変更した結果、解凍したファイルの末尾1バイトにヌル文字が入ってしまうようになった。(java.util.zipを利用する場合は問題なし)
これにより、解凍したxlsファイルをPOIで読み込む際、以下の例外が発生した。

java.io.IOException: Unable to read entire block; 1 byte read; expected 512 bytes

そこでapacheの方を使っているサンプルを探し、以下を用いるようにしたところ、正常に解凍できるようになった。
JavaでZipファイルを解凍(コピペ用) - よんちゅBlog

出力処理を行っているコードの差異は以下の通り。

http://www.ra13.org/java/ZipDecompresser.html

InputStream is = zipFile.getInputStream(ze);
bis = new BufferedInputStream(is);
bos = new BufferedOutputStream(new FileOutputStream(outFile));

int ava;
while ((ava = bis.available()) > 0) {
   byte[] bs = new byte[ava];
   // 入力
   bis.read(bs);
   // 出力
   bos.write(bs);
}

http://yonchu.hatenablog.com/entry/20100727/1280239709

 fos = new FileOutputStream( outFile);
 is = zipFile.getInputStream( ze);

 byte[] buf = new byte[1024];
 int size = 0;
 while ( (size = is.read( buf)) != -1) {
   fos.write( buf, 0, size);
 }
 fos.flush();

Javaメモリ使用量調査(Windows)

前回Linuxでやったのと同じようなことをWindowsでもやったのでメモ。
Linux版→Javaメモリリーク調査ログ(Linux) - daybreaksnow's diary

非効率な検索処理があり、将来的にデータが増えたときにOOMEが発生しないか調べる必要があった。
そのため、Eclipse上でブレークポイントを張って任意の時点のメモリ使用量を調べすることとした。

  1. 調査対象プログラムをEclipseからデバッグ実行
  2. コマンドプロンプトからjpsコマンドで対象のプロセスIDを取得(tasklistだとjavaw.exeでどのプロセスだか特定できない)
  3. 対象の処理前後でブレークを張ってダンプを取得
jmap -dump:format=b,file=XXX.hprof プロセスID

・ダンプを取るのに使ったバッチファイル

@echo off
setlocal

if "%1"=="" (
	goto usage
)

if "%2"=="" (
	goto usage
)
set DUMPFILE=%1
set PID=%2

"C:\Program Files\Java\jdk1.7.0_25\bin\jmap" -dump:format=b,file=%DUMPFILE%.hprof %PID%
goto end

:usage
echo usage: localdump.bat ^<dumpFileName^> ^<pid^>

:end

endlocal

Javaメモリリーク調査ログ(Linux)

CentOS6.4で動いているプロセスを対象に、以下の2点を調査した。

  • 起動直後、取込後、再取込後のヒープ使用量を取得し、どのオブジェクトが増えている(残り続けている)のか調べる
  • GCのログを取り、メモリリークがないか調べる

ヒープダンプ調査手順

  1. linux-x64用のJDKをダウンロード(jmapがJDKに含まれるため)
  2. 検証環境にコピー、JDKのtar解凍
  3. psで調査対象のプロセスIDを取得
  4. jmapでダンプ取得

    jdk/bin/jmap -dump:format=b,file=XXX.hprof プロセスID
    

    Dumping heap to /home/hoge/jdk/XXX.hprof...
    と表示されるのでしばらく待つ。


  5. WinSCPでダンプファイルを受け取り、Eclipse Memory Analyzerのヒストグラムでメモリ上のインスタンスを確認する

Java実行オプション

調査対象のVM引数に以下を追加

 -XX:+PrintGCTimeStamps
 -Xloggc:gc.log
 -XX:+PrintGCDetails
 -XX:+HeapDumpOnOutOfMemoryError

tomcat

以下のファイルを作成
・ /usr/local/tomcat7/bin/setenv.sh

CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails -Xloggc:/home/tomcat_gc.log"

※-XX:+PrintGCTimeStampsは認識されなかったが、自動で付加されていた


ダンプを取るのに利用したシェル

#!/bin/sh

# 引数チェック
if [ $# -lt 2 ]; then
 echo 引数に出力先ファイル名、PIDを指定してください。
 exit 1
fi

OUT_FILE=$1
PID=$2
OPT=$3
DATE=`date +%y%m%d`_`date +%H%M`

/home/hoge/tool/jdk1.7.0_21/bin/jmap ${OPT} -dump:format=b,file=${OUT_FILE}_${DATE}.hprof ${PID}

[GWT]Seleniumで非同期処理のテストを行う

GWT(gwt-dev)とSelenium(selenium-server)を同じビルドパスに入れると、内部jarでバージョンの競合が起きた([GWT]Seleniumを同一プロジェクトで動かそうとして諦めた調査ログ - daybreaksnow's diary)ので、別プロジェクトでSeleniumを動かすことにした。

ソースコードの準備

参考URL:java - GWT id element is changing every time in selenium - Stack Overflow
Seleniumから簡単に部品を取得するため、部品にIDを設定する。

1.IDを付けたい部品のensureDebugIdメソッドを呼び、IDを指定

sendButton.ensureDebugId("sendButtonId");

2.gwt.xmlで以下の要素を追加

<inherits name="com.google.gwt.user.Debug"/>

上記により、対象部品のIDが"「UIObject.DEBUG_ID_PREFIX(gwt-debug-)」+指定したID"となる。
(上記例ではgwt-debug-sendButtonIdになる)

このIDはgwt.xmlからinherits Debugタグを抜くと生成されなくなるので、本番環境への影響はない。

Seleniumの準備

参考URL:MograBlog: Running Selenium With GWT Dev
FireFoxでテストをする。
通常のSeleniumと同じようにnew FireFoxDriver()などとやっても、GWT-DEVプラグインが読み込まれず、開発モードで動作しない。

以下からFireFoxのバージョンに合わせたプラグインをダウンロードして、適当な場所に配置。
https://code.google.com/p/google-web-toolkit/downloads/list?can=2&q=xpi

ドライバ作成時にプラグインのパスを指定したFireFoxProfileを渡すようにする。

FirefoxProfile profile = new FirefoxProfile();
final String gwtPluginPath = "lib/gwt-dev-plugin-1-19-rc.xpi"; 
profile.addExtension(new File( gwtPluginPath));
driver = new FirefoxDriver( profile );
サンプル(同期実行)

普通のSeleniumと同じ

@Test
public void testSync() {
	//本番モードでテストしたいなら?以降を除去
	String pathToGwtApp = "http://127.0.0.1:8888/StockWatcher.html?gwt.codesvr=127.0.0.1:9997";
	driver.get(pathToGwtApp);

	WebElement sendButton = driver.findElement(By.id("gwt-debug-sendButtonId"));
	sendButton.click();

	WebElement sendResultLabel = driver.findElement(By.id("gwt-debug-sendResultLabelId"));
	assertThat(sendResultLabel.getText(), equalTo("fail"));
}
サンプル(非同期実行)

参考URL:Selenium - Google Web Toolkit Examples
ボタンクリック時に非同期処理がある場合(RPCコール)、コールバックメソッドが終わるまで待つ。

@Test
public void testAsync(){
	driver.get(pathToTest);
	
	WebElement sendButton = driver.findElement(By.id("gwt-debug-sendButtonId"));
	assertThat(sendButton.isEnabled(), is(true));
	//リスナでRPC呼び出しが行われるボタンをクリック
	sendButton.click();
	//レスポンスが返ってくるまではボタンが無効化される想定
	assertThat(sendButton.isEnabled(), is(false));

	// DEFAULT_SLEEP_TIMEOUT(500ms)毎にapplyが評価される。10秒立っても終わらなったら例外を投げる
	new WebDriverWait(driver, 10).until(new ExpectedCondition<Boolean>() {
		public Boolean apply(WebDriver d) {
			// ボタンが有効化される(レスポンスが返ってくる)まで待つ
			WebElement sendButton = driver.findElement(By.id("gwt-debug-sendButtonId"));
			return sendButton.isEnabled();
		}
	});
	
	//レスポンスによって変化した部品を評価
	WebElement sendResultLabel = driver.findElement(By.id("gwt-debug-sendResultLabelId"));
	assertThat(sendResultLabel.getText(), is("fail"));
}