2015年12月02日

GlassFish上で動くWebアプリをGradleとNetBeansで開発するための準備

これは、Java EE Advent Calendar 2015 - Qiitaの2日目(12月2日)の記事だよ。

昨日は、キクタロー(@kikutaro_)さんJava EEでMicroserviceを実現するKumuluzEE #javaee #Qiita ね。
明日は、Kohei Nozaki(@lbtc_xxx)さんが書くみたい。

今日は、Gradleを使って、
GlassFish上で動くWebアプリをNetBeansで開発するときのプロジェクトを作ってみようと思いま〜す。
Webアプリは、CDIとJPA使う想定で、ユニットテストも出来るようにしたいかも。

でも、中身はなしでっ。
開発する環境を用意するのを中心にやることにするのだ。
あと、ユニットテストでCDIとかJPAを使う具体的な方法は別の記事を参照ってことで、

環境の前提はこんな感じでいくね。
1.Windows 8.1(まだ10にしてない)
2.JDK 1.8(手元で使ってるのはupdate66)
3.NetBeans 8.1(JavaEE開発用)
4.GlassFish 4.1(ドメインとデータソースの設定済み)(GlassFish 4.1.1でもほとんどおなじ)

流れは、こんな感じで。
1.GlassFishのサーバーとドメインをNetBeansに設定
2.NetBeansのGradle Supportモジュールをインストール
3.Gradleのプロジェクトを作ってビルドファイルをかきかきφ(。。
4.簡単な拡張とか

と言うことで。

☆☆☆1.GlassFishのサーバーとドメインをNetBeansに設定☆☆☆

ドメインの作成、DBのライブラリの設定、データソースの設定は終わってる前提だから、
NetBeansに登録して使えるようにするところから。

サーバーの追加でGlassFishを選んで……、
ここではGlassFish 4.1って名前にするね。この名前は後で使うよ。
javaee_2015_01.png
javaee_2015_02.png

GlassFishのインストール先と、ドメインを指定して完了〜。
javaee_2015_03.png
javaee_2015_04.png

こんな感じになってるはず。
javaee_2015_05.png

☆☆☆2.NetBeansのGradle Supportモジュールをインストール☆☆☆

いれるのは、これね、これ。
https://github.com/kelemen/netbeans-gradle-project

NetBeansのメニューバーから、ツール→プラグインと選んで、
プラグインの検索とインストール、更新が出来るダイアログを開くよ。
javaee_2015_11.png

Gradleって検索すると『Gradle Support』って言うが見つかるからそれ。
もう1個のはよく知らない。とりあえず今回はいらないのでどっちでも。
何が出来るようになるんだろうね。

インストールしようとすると
『GroovyおよびGrails』も一緒に入れないとなのでまとめて入れちゃう。
GradleがGroovyで動くからだね。
javaee_2015_12.png

NetBeansの再起動を要求されるから再起動すればGradleを使う準備は完了!

☆☆☆3.Gradleのプロジェクトを作ってビルドファイルをかきかきφ(。。☆☆☆

さぁ、新規プロジェクトを作るよ。
今回は、Single Gradle Projectで。
複雑なプロジェクト構成を組む場合は、Gradle Root Projectね。
javaee_2015_21.png

適当な名前でプロジェクトを作成〜。
javaee_2015_22.png

Java用のbuild.gradleファイルが出来ているはず。
javaee_2015_23.png

でもこのファイルは、Webアプリ用じゃないから、
ばっさりと書き換えちゃう。こんな感じに。

// GradleでGlassFishにアクセスするためのプラグイン用意してみたからそれ使うのだ。
// http://memory.empressia.jp/article/167469186.html
buildscript {
repositories {
maven { url "http://www.empressia.jp/maven/"; }
}
dependencies {
classpath group: "jp.empressia", name: "jp.empressia.gradle.plugin.glassfish", version: "0.2.2";
}
}

// warをパッケージする。
apply plugin: "war";
// GlassFish用のタスクを追加する。
apply plugin: "glassfish";
// NetBeansのGlassFish設定を読み込める様にする。
apply plugin: "glassfish.netbeans";

// Java8で。
sourceCompatibility = 1.8;
targetCompatibility = 1.8;

// Mavenな感じと同じようにclassesの中にリソースが配置されるようにする。
sourceSets {
main.output.resourcesDir = main.output.classesDir;
test.output.resourcesDir = test.output.classesDir;
}

// アプリの依存性解決用のリポジトリにMavenセントラル使うよ。
repositories {
mavenCentral();
}

// 依存関係。必要なの追加で。
dependencies {
providedCompile(group:"javax", name:"javaee-web-api", version:"7.0");
}

// NetBeans GlassFishプラグインの設定。NetBeansに登録したGlassFishサーバーを使う設定をしているよ。
// 記事の前半で設定したサーバーの名前を設定するのだ。
// glassfish.netbeans.serverName = "GlassFish Server 4.1";って書いてもokだからね。
glassfish {
netbeans {
serverName = "GlassFish Server 4.1";
}
}

// JavaはUTF-8でいいよね。
tasks.withType(JavaCompile) {
options.encoding = "UTF-8";
}

// リソースは、classes側に移動するようにしたからデプロイでリソースを個別に動かす必要はないからスルーする設定。
tasks.deployResources {
exclude "**/*"
}

これで基本的な設定は終わり〜。

開始ボタン、ビルドボタン、とか普通に動くはず。
ブラウザは自動では開かないから、自分で開くっ。一度開けばいいし、良いよね。

あと、ログ見たいときは、サービスのサーバーからドメイン・サーバー・ログの表示を選べばでてくるよ。
これも一度だけ選べばずっと出てるかな。

あ、メインメニューのツール→オプションの設定に、Compile on saveがあるから、使ってみるといいかも。
javaee_2015_91.png

プロジェクトのプロパティ設定でApply Code Changesを『${project}:deploy』がおすすめかな。
javaee_2015_92.png

☆☆☆4.簡単な拡張とか☆☆☆

☆ユニットテストでCDIとJPAを使う設定

CDIを実際に動かすには、コンテナ作るか用意しないとだと思うけど、
基本的な設定はこんな感じ?

dependenies {
// testRuntimeでも良いのもあるかも。
testCompile(group:"junit", name:"junit", version:"4.12");
testCompile(group:"org.hamcrest", name:"hamcrest-library", version:"1.3");
testCompile(group:"org.jboss.weld.se", name:"weld-se", version:"2.2.2.Final");
testCompile(group:"org.eclipse.persistence", name:"eclipselink", version:"2.5.2-RC1");
testCompile(group:"javax.transaction", name:"javax.transaction-api", version:"1.2");
}

tasks.test {
doFirst {
// test側のbeans.xmlに書かれた内容をユニットテストの時に読み込まれるようにbeans.xmlをmainのclassesにコピーする。
// 要らなければスルーで。
copy { from "${sourceSets.test.resources.srcDirs.find({true})}/META-INF/beans.xml" into "${sourceSets.main.output.resourcesDir}/META-INF/" }
// コネクションはテスト側にあっても取れるけど、Entityはmain側にないと読み込めないから、persistence.xmlをmainのclassesにコピーする。
// あと、persistence.xmlの競合を避けるために、テスト側からpersistence.xmlを削除する。
copy { from "${sourceSets.test.resources.srcDirs.find({true})}/META-INF/persistence.xml" into "${sourceSets.main.output.resourcesDir}/META-INF/" }
delete "${sourceSets.test.output.resourcesDir}/META-INF/persistence.xml"
}
// JPAをJavaAgentで使う〜。
jvmArgs "-javaagent:${configurations.testCompile.files.find({ file -> file.name.equals('eclipselink-2.5.2-RC1.jar') })}"
}

// ビルドする場合は、testをはじめる前にパッケージ(とデプロイ)をしないと、テスト直前でリソースを書き換えているのが問題になっちゃうから、依存関係を追加しておく。
if(this.gradle.startParameter.taskNames.contains("build")) {
// tasks.test.dependsOn(tasks.assemble);っていう方法も。
tasks.test.dependsOn(tasks.war);
tasks.test.dependsOn(tasks.deploy);
}

余談だけど、CDIってなんかパッケージの外から設定上書きできないのかなぁ?
Alternativeとか、外から切り替えたいんだけど、できないよね。
そのうちできるようになるのかなぁ……?それとも、そういうものじゃないのかな……?

☆LESSを使う設定

LESS便利だよね。と言うか、CSSが不便すぎて……。
パスを直接ビルドファイルに書くのいやだし、NetBeansからパスを読み込むのを用意してみたよ。
ビルドスクリプトに追加で。

buildscript {
repositories {
maven { url "http://www.empressia.jp/maven/"; }
}
dependencies {
classpath group: "jp.empressia", name: "jp.empressia.gradle.plugin.netbeans.configuration", version: "0.0.6";
}
}
apply plugin: "netbeans.configuration";

// タスクを追加するよ。
// タスクの定義で『<<』で作ってる例あるけど、タスクの定義に『<<』は追加したらだめだよね。
// タスクの定義ちゃんとしてインクリメンタルビルドしないとね。
task processLess(type: Copy) {
from webAppDir
into webAppDir
include "**/*.less"
eachFile { file ->
exec {
commandLine "${netbeans.configuration.lessPath}", "--compress", "${webAppDir}/${file.path}", "${webAppDir}/${file.path.substring(0, file.path.length() - '.less'.length())}.min.css"
}
file.exclude();
}
}

// タスクの依存関係はこんな感じ?
// コンパイル系の作業の中にLESSコンパイルもいれるよ。
tasks.classes.dependsOn(tasks.processLess);

他にも、netbeans.configuration.***Pathっていう形でいろいろ取れるようにしてあるよ。

  • sassPath
  • gruntPath
  • gulpPath
  • karmaPath
  • expressPath
  • nodePath
  • npmPath

バージョンとか指定したい場合は

netbeans {
configuration {
version = "8.1";
}
}

とか

netbeans {
configuration {
appDataDirectoryPath = "C:/Users/[UserName]/AppData/Roaming/NetBeans";
version = "8.1";
}
}

とか

netbeans {
configuration {
directoryPath = "C:/Users/[UserName]/AppData/Roaming/NetBeans/8.1/config";
}
}

とかで指定できるよ。

☆JavadocとMavenでのアップロードは……。

まぁ、いいよね。
今回は、スルーで。

☆最終的なbuild.gradle

こんな感じになるかな?

buildscript {
repositories {
maven { url "http://www.empressia.jp/maven/"; }
}
dependencies {
classpath group: "jp.empressia", name: "jp.empressia.gradle.plugin.glassfish", version: "0.2.2";
classpath group: "jp.empressia", name: "jp.empressia.gradle.plugin.netbeans.configuration", version: "0.0.6";
}
}

apply plugin: "war";
apply plugin: "glassfish";
apply plugin: "glassfish.netbeans";
apply plugin: "netbeans.configuration";

sourceCompatibility = 1.8;
targetCompatibility = 1.8;

sourceSets {
main.output.resourcesDir = main.output.classesDir;
test.output.resourcesDir = test.output.classesDir;
}

repositories {
mavenCentral();
}

dependencies {
providedCompile(group:"javax", name:"javaee-web-api", version:"7.0");
testCompile(group:"junit", name:"junit", version:"4.12");
testCompile(group:"org.hamcrest", name:"hamcrest-library", version:"1.3");
testCompile(group:"org.jboss.weld.se", name:"weld-se", version:"2.2.2.Final");
testCompile(group:"org.eclipse.persistence", name:"eclipselink", version:"2.5.2-RC1");
testCompile(group:"javax.transaction", name:"javax.transaction-api", version:"1.2");
}

glassfish {
netbeans {
serverName = "GlassFish Server 4.1";
}
}

tasks.withType(JavaCompile) {
options.encoding = "UTF-8";
}

task processLess(type: Copy) {
from webAppDir
into webAppDir
include "**/*.less"
eachFile { file ->
exec {
commandLine "${netbeans.configuration.lessPath}", "--compress", "${webAppDir}/${file.path}", "${webAppDir}/${file.path.substring(0, file.path.length() - '.less'.length())}.min.css"
}
file.exclude();
}
}
tasks.classes.dependsOn(tasks.processLess);

tasks.deployResources {
exclude "**/*"
}

tasks.test {
doFirst {
copy { from "${sourceSets.test.resources.srcDirs.find({true})}/META-INF/beans.xml" into "${sourceSets.main.output.resourcesDir}/META-INF/" }
copy { from "${sourceSets.test.resources.srcDirs.find({true})}/META-INF/persistence.xml" into "${sourceSets.main.output.resourcesDir}/META-INF/" }
delete "${sourceSets.test.output.resourcesDir}/META-INF/persistence.xml"
}
jvmArgs "-javaagent:${configurations.testCompile.files.find({ file -> file.name.equals('eclipselink-2.5.2-RC1.jar') })}"
}

if(this.gradle.startParameter.taskNames.contains("build")) {
tasks.test.dependsOn(tasks.war);
tasks.test.dependsOn(tasks.deploy);
}

これで、割と自然にGradle+NetBeans+GlassFishでWebアプリ作れるんじゃないかなぁって。

そうそう、NetBeansのGradleプラグイン作ってる人に聞いたんだけど、
非同期構成意識して作っているからプロジェクトの構成が古いのと違っていて、
古いNetBeansのNode追加するプラグインがGradleのプロジェクトには追加されないんだよね(Ver.1.3.7.2)。

対応してもらったバージョン作ってもらったから、良ければ直接ダウンロードして使ってみてね。
https://github.com/kelemen/netbeans-gradle-project
NetBeans81ブランチで試してたんだけど、今は、masterにも取り込まれたみたいだよ。

かなり、メールの応答が速かったから、とっても助かったのだ(゜▽、゜感謝だね

posted by すふぃあ at 09:07| Comment(0) | TrackBack(0) | 雁字

2015年11月09日

GradleからGlassFishにデプロイするためのプラグインをつくってみたよ

NetBeansつかってWebサイトつくってたんだけど、
Mavenでビルドしてると遅いよね!

なんか、MavenじゃなくてGradleっていうのを使うと、
インクリメンタルビルドっていうのができて速くなるみたい。

と言うわけで、Webサイト作るのに使ってみたんだけど、
GlassFishへのデプロイはデフォルトではサポートしていないみたい。
で、ないならプラグインで作れば良いよね!ってことでつくってみたよ。

☆☆☆準備編☆☆☆

build.gradleに下のように書けばok。

buildscript {
repositories {
maven { url "http://www.empressia.jp/maven/" }
}
dependencies {
classpath "jp.empressia:jp.empressia.gradle.plugin.glassfish:+";
}
}
apply plugin: "glassfish";

NetBeans使っている場合は、さらに下の1行を追加すると便利になるよ。

apply plugin: "glassfish.netbeans";

☆☆☆定義編☆☆☆

プラグインを適用すると、glassfishの設定をできるようになるよ。
netbeansの部分はglassfish.netbeansを適用したときだけね。
長いけど、別に、全部設定しなくても大丈夫。

glassfish {
// GlassFishの場所
homeDirectoryPath;
// GlassFishの起動に使用するJDKHomeの場所
JDKHomeDirectoryPath;
// ドメインのディレクトリ
domainsDirectoryPath;
// ドメイン名
domainName;
// 管理ポート
adminPort;
// deployでサーバーの開始を試みる時にデバッグにするかどうか
deployWithDebug;
// ビルドの時にデプロイするかどうか(デフォルトtrue)
deployOnBuild;
netbeans {
// NetBeansのサーバー設定の名前
serverName;
// NetBeansのJavaプラットフォーム設定の名前
JDKName;
// NetBeansのサーバー設定ファイル(/config/GlassFishEE6/Instances/.nbattrs)
configFilePath;
// NetBeansのVersion
version;
}
deploy {
// デプロイ用のクラスファイルの場所
String classesDirectoryPath;
// デプロイ用のリソースファイルの場所
String resourcesDirectoryPath;
// デプロイ用のWebPage系のファイルの場所
String webappDirectoryPath;
// デプロイ用のディレクトリ名
String directoryName;
// デプロイ用のコンテキストルート
String contextRoot;
}
}

☆☆☆設定サンプル編☆☆☆

netbeansを適用していないときは、GlassFishのホームディレクトリを設定してね。
gradle,propertiesあたりから設定しても良いかも。

glassfish {
homeDirectoryPath = "C:\\Program Files\\glassfish-4.1\\glassfish";
}

JDKは放っておくとGradleのを使うから、指定したい場合は、↓の感じ。

glassfish {
homeDirectoryPath = "C:\\Program Files\\glassfish-4.1\\glassfish";
JDKHomeDirectoryPath = "C:\\Program Files\\Java\\jdk1.8.*_**";
}

でも、やっぱり設定めんどくさいよね!
と言うわけで、netbeansを適用しておけば、NetBeansの設定から勝手にGlassFish探すようにしてあるよ。
このときは何もしなくても全自動〜。

サーバーの設定を複数しているなら、設定したサーバーの名前を付ければ大丈夫!

glassfish {
netbeans {
serverName = "GlassFish Server 4.1";
}
}

簡単だね!
あ、古いNetBeansに設定がある場合は、versionを指定してね。

glassfish {
netbeans {
serverName = "GlassFish Server 4.1";
version = "8.0.1";
}
}

サーバーのJDK設定がデフォルトだと、Graldeのを使おうとするから注意してね。
こんな風にも設定できるけど、サーバーの設定をちゃんとした方が良いかな。

glassfish {
netbeans {
serverName = "GlassFish Server 4.1";
JDKName = "JDK 1.8";
}
}

☆☆☆実行編☆☆☆

glassfishプラグインを適用すると、5つのタスクが追加されるよ。

  • deployClasses
  • deployResources
  • deployWebapp
  • deployLibraries
  • deploy

他にも、いくつかのタスクが書き換わるよ。
clean、run、debugとか。

これで、↓の感じで行けるはず。

// サーバー停止(デプロイディレクトリ削除)
gradle clean
// サーバー起動
gradle run
// サーバーdebug起動
gradle debug
// デプロイ
gradle deploy

タスクの関係はだいたいこんな感じ。
EmpressiaGradleGlassFishPlugin_Tasks.png

NetBeans使ってるなら、ビルドボタン、クリーンビルドボタン、実行、デバッグボタンがあるから簡単に連動すると思うよ。
ただ、サーバーのログが見たかったら、サービスのサーバーを選んで、ドメイン・サーバー・ログの表示とかやってね。
あ、NetBeans閉じてもサーバー止まらないかも……まぁ、いいよね(゜▽、゜

☆☆☆注意編☆☆☆

なるべく、無駄にdeployコマンドが発行されないようにしているんだけど、
そのせいで、deployコマンド自体で失敗すると、次からデプロイされなくなる……かも?
そんなときは、コンパイルが動くようにプログラムを一回書き換えるとか、cleanするとかがいいかなぁ。

あと、MacとLinuxでの確認はしてないー。

Version 0.2.0は、Javaのソースがないと、
webappにWEB-INFディレクトリが作られなくてデプロイ失敗することがあるんだけど、
そういうことあんまり無いだろうし、すぐ直す予定〜。

posted by すふぃあ at 22:16| Comment(0) | TrackBack(0) | 雁字

2014年12月21日

Maven経由のユニットテストでCDIを簡単に使う

前の二つの記事でCDIとJPAをユニットテストで使う方法について書いてきたから詳細なプログラムの内容はそっちに任せるとして、
まとめたパッケージを簡単に使う方法をまとめると、pom.xmlをこんな感じに。

	<repositories>
……
<repository>
<id>Empressia</id>
<url>http://www.empressia.jp/maven/</url>
</repository>
</repositories>

<dependencies>
……
<dependency>
<groupId>jp.empressia</groupId>
<artifactId>jp.empressia.test</artifactId>
<version>1.0.1-1</version>
<scope>test</scope>
</dependency>
</dependencies>

これでRunner指定すればCDIが使えるよ。さらにbeans.xmlをこんな感じに。

	<beans>
<alternatives>
<stereotype>jp.empressia.test.UnitTest</stereotype>
</alternatives>
<intercepters>
<class>jp.empressia.test.TransactionInterceptor</class>
</intercepters>
</beans>

これでJPAとJTAが使える。一部だけど。

で、Maven使ってるときに気をつけないといけないことがあるんだよね。

beans.xmlにalternatives書くのは、main側じゃないとダメって言うか、main側のパスに入ってないとだめなのね。
でも、main側にテストの要素書いちゃだめなわけで……えーっと、
mainのclassesとtest-classesで別のbeans.xm管理になるって言う感じ?
実際にalternativeしたいのは、main側。設定書いておきたいのはtest側。
有効にするためには、test側にもbeans.xml必要。
うーん……ここ、ポイントだからしっかり整理〜。

  • JavaEEの環境だと、beans.xml無くてもCDIは有効になる。
  • JavaSEの環境だとbeans.xmlが必要。
  • Mavenだとクラスファイルの格納場所が、classesとtest-classesに分かれてる。
  • ユニットテストでalternativeの書いたbeans.xmlを置かないといけないのはclasses側(試して見た感じ)。
  • ユニットテストでCDIを有効にするためにbeans.xml置くのはtest-classes側(中身は何でもよさそう)(試して見た感じ)。
  • テスト固有のbeans.xmlの内容はmain側じゃなくてtest側に書いておきたい。

これを解決しようとすると、
testするときには、テスト側のbeans.xmlをmain側において実行して、
パッケージするときは、メイン側のbeans.xmlで上書きするって手順にしないとダメそう。

あと、persistence.xmlもmain側だけじゃなくてtest側にもおいちゃうと、ユニットテストの時に、EclipseLinkを使った時点で両方読まれちゃう。
test用にRESOURCE_LOCALで細かい接続設定書いたpersistence.xmlを置くだろうから、
ユニットテストの時はmainの方を消すのを忘れずに。packagingの時にこれも気をつけないとね。

具体的にはMavenのビルドをこんな感じに。
良い悪いいろんな意見はあるだろうけど、まぁ、手軽にやるなら良いかなって。

	<build>
<plugins>
……
<!-- process-test-resources:beans.xmlをmainのclassesにコピーする。test側のbeans.xmlに書かれた内容をユニットテストの時に読み込まれるようにする。 -->
<plugin>
<groupId>com.github.goldin</groupId>
<artifactId>copy-maven-plugin</artifactId>
<version>0.2.5</version>
<executions>
<execution>
<id>copy test beans.xml to main for unit test.</id>
<phase>process-test-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<resources>
<resource>
<targetPath>${project.build.directory}/classes/META-INF</targetPath>
<file>src/test/resources/META-INF/beans.xml</file>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- process-test-resources:persistence.xmlをmainのclassesから削除する。mainのpersistence.xmlがユニットテストの時に読み込まれるのを防止する。 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>2.6.1</version>
<executions>
<execution>
<id>remove persistence.xml from classes for avoiding duplicated persistence.xml at unit test.</id>
<phase>process-test-resources</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<excludeDefaultDirectories>true</excludeDefaultDirectories>
<filesets>
<fileset>
<directory>${project.build.directory}/classes/META-INF</directory>
<includes>
<include>persistence.xml</include>
</includes>
</fileset>
</filesets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<!-- テスト用にtargetが書き換えているから、src側からパッケージする。 -->
<webResources>
<resource>
<directory>src/main/resources/META-INF</directory>
<targetPath>WEB-INF/classes/META-INF</targetPath>
<includes>
<include>beans.xml</include>
<include>persistence.xml</include>
</includes>
</resource>
<resource>
<directory>target/classes/META-INF</directory>
<excludes>
<!-- beans.xmlはユニットテストの時にmain側にコピーしてるから取り込まないようにする。 -->
<exclude>beans.xml</exclude>
<!-- persistence.xmlはユニットテストの時に消えるはずだけどincludeと形をあわせておく。 -->
<exclude>persistence.xml</exclude>
</excludes>
</resource>
</webResources>
</configuration>
</plugin>
</plugins>
</build>

ここまでで、@Injectが使いたい放題でJPAのPersistenceUnit1個に対応して、Transactionalにも簡単にだけど対応した状態になっているはず!(゜▽、゜
気に入らなければCDIの力で差し替えも簡単だから、是非、お試しあれ?(゜▽、゜

posted by すふぃあ at 21:00| Comment(1) | TrackBack(0) | 雁字

JPAとJTAをJavaSE環境で使えるようにする

こっちの記事に書いたけど、CDIが出来ちゃえば、後はもう簡単だよね。
CDI使ってるってことは、リソース系は、全部@Produces作ってるはずだから、
それを上書きしちゃえばok!

JPAは普通……かどうか分からないけど、CDI使っているなら、
persistence.xmlに定義しているUnitごとに、Producer作るよね。
たとえば、persistence.xmlにSamplePUが定義されてるとするとこんな感じ。

	@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Sample {
}
	@RequestScoped
public class SampleProducer {

@PersistenceContext(unitName="SamplePU")
private EntityManager em;

@Produces @Sample
public EntityManager getEntityManager() {
return this.em;
}

}

そしたら、テスト側はこんな感じのを用意すれば完成!テスト用のbeans.xmlに追加するの忘れないでね。

	package jp.empressia.test;
// (略)
@Stereotype
@Alternative
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface UnitTest {
}
	@RequestScoped
public class LocalSampleProducer {

private static final EntityManagerFactory emf;

static {
emf = Persistence.createEntityManagerFactory("SamplePU");
}

private EntityManager em;

public LocalSampleProducer() {
this.em = emf.createEntityManager();
}

@Produces @Sample @UnitTest
public EntityManager getEntityManager() {
return this.em;
}

}
	<beans>
<alternatives>
<stereotype>jp.empressia.test.UnitTest</stereotype>
</alternatives>
</beans>

EntityManagerFactoryとEntityManagerの扱い方はこれでいいよね、たぶん。

それから、JTAかな。
CDIとJTAなら、javax.transaction.Transactional使っているという前提で、Interceptorを作っちゃう方向でいいよね。
あと、ユニットテストだし、接続先は共通で1個だけってことにするよ。
まず、ProducerにTransactionを提供する部分を追加するのだ。

	@RequestScoped
public class LocalSampleProducer {
// (略)
@Produces
public EntityTransaction getTransaction() {
return this.em.getTransaction();
}
}

そうしたら、こんな感じのInterceptorでも十分かなっと。
別にもっと作り込んでも良いんだけど、わたしはそんな高度な使い方しないしね。

	package jp.empressia.test;
// (略)
@Transactional
@Interceptor
public class TransactionInterceptor {

@Inject
private EntityTransaction trans;

@AroundInvoke
public Object doTransaction(InvocationContext ic) throws Exception {
Object result = null;
try {
this.trans.begin();
result = ic.proceed();
if(this.trans.isActive()) {
if(this.trans.getRollbackOnly() == false) {
this.trans.commit();
} else {
this.trans.rollback();
}
} else {
String message = "なにかメッセージ";
logger(this).warning(message);
}
} catch(Exception proceededEx) {
try {
if(trans.isActive()) {
trans.rollback();
}
} catch(Exception rollbackEx) {
String message = "なにかメッセージ";
logger(this).severe(message);
}
throw proceededEx;
}
return result;
}
}

で、これを、やっぱりbeans.xmlにっと。

	<beans>
<intercepters>
<class>jp.empressia.test.TransactionInterceptor</class>
</intercepters>
</beans>

これで、@TransactionalもEntityManagerのInjectも動くね。

ちなみに、前の記事のパッケージには、接続先のUnitが1個前提の実装が入ってるよ。
beans.xmlをこんな感じにすれば有効になるようになってる。

	<beans>
<alternatives>
<stereotype>jp.empressia.test.UnitTest</stereotype>
</alternatives>
<intercepters>
<class>jp.empressia.test.TransactionInterceptor</class>
</intercepters>
</beans>

あとはLocalなEntityManagerなProducerをこんな感じに書けばok。
『@Sample』みたいな指定が元々なければ、子のクラスは用意する必要も無い……はず、たぶん。
@Inject EntityManagerが動作してるからね。

	@RequestScoped
public class LocalSampleProducer {

@Inject
private EntityManager em;

@Produces @Sample @UnitTest
public EntityManager getEntityManager() {
return this.em;
}

}

あ、もしかしたら、EclipseLinkをjavaagentに指定して起動すれば、もっと簡単かもしれないけど、その辺は試してないよ。

posted by すふぃあ at 21:00| Comment(2) | TrackBack(0) | 雁字

JavaEE用のスコープをJavaSE環境で使えるようにする

CDIをJavaSEで使えるようにするのはそんな難しいことじゃないよね。
問題なのは、JavaEEのスコープをJavaSEで使うこと。
RequestScopedとか、ConversationScopedとか、SessionScopedとか。

他のカスタムされたスコープは、まぁ、必要な人が対応すれば良いよねってことで。
基本は、同じだし。

まずは、コンテナとしての機能を追加しないとダメだから、JUnitのRunnerを拡張してWeldの機能を有効にするところから。
ホントは、別の方法とかあるのかもしれないけど、JUnitに詳しくなくて……動けば良いよね!

こんな感じ。

	public class JUnit4RunnerWithWeld extends BlockJUnit4ClassRunner {

/** Weld */
private static final Weld Weld;
/** Weld Container. */
private static final WeldContainer Container;

static {
Weld = new Weld();
Container = Weld.initialize();
}

/** 対象となるテストクラス。 */
private final Class<?> TargetTestClass;

public JUnit4RunnerWithWeld(final Class<Object> TargetTestClass) throws InitializationError {
super(TargetTestClass);
this.TargetTestClass = TargetTestClass;
}

@Override
protected Object createTest() throws Exception {
Object test = JUnit4RunnerWithWeld.Container.instance().select(this.TargetTestClass).get();
return test;
}

}

ポイントは2つ。

  1. Weldを作って初期化する。
  2. BlockJUnit4ClassRunnerを継承してcreateTestで対象のクラスをコンテナとして作る。

うん、簡単だよね!Weld大好き!……かも。いや、別にそんな好きなわけじゃないけど。
ちなみに、Runner自体は、テストクラスごとに出来るから、Weldはstaticに作ってるよ。
これで、ユニットテストクラスに@RunWithでCDIを有効にできるようになったのだ。

	@RunWith(JUnit4RunnerWithWeld.class)
public class SomethingTest {

あ、実際に有効にするには、beans.xml忘れないでね。
beans.xmlをどういう風にしたら良いかは別の記事でまとめるよ。

次に、JavaEEのスコープにコンテキストとストレージを設定しないとね。
まぁ、これは、GlassFish3.xのころにViewScopedとか作ってたし楽勝ってことで。
こんなユーティリティを用意する。長いよ。

	public class WeldUtilities {

public static String ExtensionProductName = "Empressia Test Library";
public static String SupportCDIProductName = "Weld";

/** JavaEE向けのスコープをJavaSEでサポートする。 */
public static void supportEEScopes(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Map<String, Object> requestMap = new HashMap<>();
WeldUtilities.activateContextWithStorage(bm, RequestScoped.class, requestMap);
Map<String, Object> sessionMap = new HashMap<>();
WeldUtilities.activateContextWithStorage(bm, SessionScoped.class, sessionMap);
WeldUtilities.activateContextWithStorage(bm, ConversationScoped.class, new MutableBoundRequest(requestMap, sessionMap));
}

/** JavaEE向けのスコープをJavaSEでサポートをリセットする。 */
public static void resetEEScopeContexts(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
AbstractBoundContext<?>[] contexts = {
WeldUtilities.findFirstContext(bm, RequestScoped.class),
WeldUtilities.findFirstContext(bm, SessionScoped.class),
WeldUtilities.findFirstContext(bm, ConversationScoped.class)
};
for(AbstractBoundContext<?> context : contexts) {
context.deactivate();
context.cleanup();
}
WeldUtilities.supportEEScopes(bm);
}

public static <S> void activateContextWithStorage(BeanManager bm, Class<? extends Annotation> scopeType, S storage)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
AbstractBoundContext<S> boundContext = WeldUtilities.findFirstContext(bm, scopeType);
boundContext.associate(storage);
boundContext.activate();
}

/** BeanManagerから指定スコープの最初のContextを抽出する。アクティブかどうかは考慮されない。 */
public static <S> AbstractBoundContext<S> findFirstContext(BeanManager bm, Class<? extends Annotation> scopeType)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Map<Class<? extends Annotation>, List<Context>> contextsMap = WeldUtilities.extractContextsMap(bm);
List<Context> contexts = contextsMap.get(scopeType);
// ContextはScopeの種類に対して必ず1個用意してある前提で。
if(contexts.isEmpty()) {
String message = "なにかメッセージ";
throw new IllegalStateException(message);
}
Context context = contexts.get(0);
if(context instanceof AbstractBoundContext) {
} else {
// java.lang.ClassCastException: org.jboss.weld.context.PassivatingContextWrapper$AlterableContextWrapper cannot be cast to org.jboss.weld.context.AbstractBoundContext
context = PassivatingContextWrapper.unwrap(context);
}
@SuppressWarnings("unchecked")
AbstractBoundContext<S> boundContext = (AbstractBoundContext<S>)context;
return boundContext;
}

/** BeanManagerからScopeTypeごとの?Contextを抽出する。 */
public static Map<Class<? extends Annotation>, List<Context>> extractContextsMap(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
BeanManagerImpl beanManagerImpl;
if(bm instanceof BeanManagerProxy) {
BeanManagerProxy beanManagerProxy = (BeanManagerProxy)bm;
beanManagerImpl = beanManagerProxy.delegate();
} else if(bm instanceof BeanManagerImpl) {
beanManagerImpl = (BeanManagerImpl)bm;
} else {
String message = "なにかメッセージ";
throw new IllegalStateException(message);
}
// getContextだとactiveなものしか取れないから、強引に設定したい今回の用途では使えない。
Method m = BeanManagerImpl.class.getDeclaredMethod("getContexts");
m.setAccessible(true);
@SuppressWarnings("unchecked")
Map<Class<? extends Annotation>, List<Context>> contextsMap = (Map<Class<? extends Annotation>, List<Context>>)m.invoke(beanManagerImpl);
return contextsMap;
}

}

この辺は、解説入れるとめんどくさいから、WeldのソースとかGlassFishのソースとか読むと良いかもってことで。
で、これを呼び出すExtensionを用意する。

	package jp.empressia.test;
// (略)
public class WeldTestExtension implements Extension {

public void afterDeployment(@Observes AfterDeploymentValidation e, BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
logger(this).info("Empressia Test Library Weld Test Extension startup.");
WeldUtilities.supportEEScopes(bm);
}

}

このExtensionは、META-INF/servicesに『javax.enterprise.inject.spi.Extension』ってファイルを作って、その中に、

	jp.empressia.test.WeldTestExtension

とか書いておくよ。こうしておくことで、Weldの初期化がされたときに、@Observesが呼ばれてJavaEEのスコープがすぐに有効になるのだ。
あと、テストごとに、スコープはリセットした方が良いだろうから、Runnerのメソッドをオーバーライドしておこうね。

	public class JUnit4RunnerWithWeld extends BlockJUnit4ClassRunner {

// (略)

/** テストを1個ずつ実行します。順序は保証されないから、コンテキストは毎回リセットする。 */
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
BeanManager bm = JUnit4RunnerWithWeld.Container.getBeanManager();
try {
WeldUtilities.resetEEScopeContexts(bm);
} catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
String message = "なにかメッセージ";
throw new IllegalStateException(message, ex);
}
super.runChild(method, notifier);
}

}

これで完成(゚ー゚)(。_。)(゚-゚)(。_。)ウンウン

この辺のクラスをパッケージにしたのを置いておいたよ。

Mavenのリポジトリを追加して依存関係を書けば使えるよ。こんな感じ?

<repositories>
……
<repository>
<id>Empressia</id>
<url>http://www.empressia.jp/maven/</url>
</repository>
</repositories>

<dependencies>
……
<dependency>
<groupId>jp.empressia</groupId>
<artifactId>jp.empressia.test</artifactId>
<version>1.0.1-1</version>
<scope>test</scope>
</dependency>
</dependencies>

古い実装とかライブラリが混じっていると、JNDIにBeanManagerを登録しておかないとちゃんと動かないかもしれないけど。
このパッケージは、その辺も簡単にだけど対応しておいたよ。

posted by すふぃあ at 21:00| Comment(1) | TrackBack(0) | 雁字

JavaEEのユニットテスト環境を用意する

☆はじめに☆

JavaEEの機能は、去年作ったファイル転送Webアプリだと、WebSocketの機能しか使ってなかったし、
そんなユニットテスト書くくらい難しいの自体作ってないんだよね。
で、今回は、JavaEEをもうちょっとしっかり使おうかなと。

とりあえず、JavaEEが提供しているJAX-RS、CDI、JPAあたりを使ってWebアプリケーションを作ろうと思ったんだけど、
これ、どれもユニットテストがめんどくさい。
まぁ、JAX-RSは、問題領域とは直接関係ないから、ユニットテストの対象からは外すとして、問題はCDIとJPAだよね。

JavaEE自体は、関数の集まりみたいなライブラリと言うよりは、コンテナだから、
ユニットテストするためには、そのコンテナを再現しないといけないんだよね。
特に、CDIはコンテナそのものって感じだしね。

あとは、環境に依存したリソースの表現だよね。
JPAは、永続化先を抽象化してくれるけど、ユニットテストの時は、それが逆にめんどくさいことに。
でも、抽象化されること自体は便利だからやっぱりJPAは使うよね。

なんか、Mockって言うの作って対応したりする例とかよくWebで見かけるんだけど、
DBとかに繋げば良いだけなのに、そんなの作るのめんどくさいよね。
きっとそれが必要な時もあるんだとは思うけど。

☆準備☆

さて、ユニットテスト環境を作るにしても、何か想定はしないとダメだよね。
今回は、こんな条件を付けてみるよ。

  • ゴール:JavaEEのWebアプリ向けに問題領域のユニットテストとしてのコンテナを用意して簡単に使えるようにする。
  • ユニットテストには、JUnitを使う。
  • CDIとJPA(とJTA)を何となく使えるようにする。
  • ユニットテストでJPAは1個の接続先を使う。
  • JavaSE向けの実装は、WeldとEclipseLinkのGlassFish4.1のバージョンを使う。
  • Mock系のライブラリとかは使わない(使うといろいろ楽だけど依存しない)。
  • ビルドにはMavenを使う。

で、ちょっと書き出してから気づいたんだけど、これ、結構長くなる……。
ってことで分割したよ。

ホントいつも流れで書いてごめんなさい(゜▽、゜

posted by すふぃあ at 21:00| Comment(1) | TrackBack(0) | 雁字

Java8の暗号利用モードのGCMって遅すぎない?

去年作ったファイル転送WebアプリをGlassFish4.1とJava8に載せ替えたんだけど、
それだけで、凄ーく遅くなったの。
載せ替える前はGlassFish4だからJava7で動いてたんだよね。
あ、ハードはfit-PC2iのWindows8(32bit)で動いてるよ。

実際に測定するとこんな感じ。

ファイル転送Webアプリの転送速度(インターネットを経由させて測定)
サーバー 速度
Java7 GlassFish4.0 26000〜28000bps
Java8 GlassFish4.1 2600〜2800bps

うん、超遅くなった。あり得ないよね、ほんと。

わたしが作った部分がおかしいのかなーとか思って、プログラムを見たんだけど、
ファイルの書き込みくらいしか重そうな処理がなったんだよね。
ファイルIOが遅いのかなーって思って、ファイルの書き込み処理を外すとこんな感じ。

ファイル転送Webアプリの転送速度(インターネットを経由させて測定:書き込み処理無し)
サーバー 速度
Java7 GlassFish4.0 26000〜28000bps
Java8 GlassFish4.1 2600〜2800bps

変わんないし!全く変わんないし!

ファイル転送のWebアプリだから、ファイルの書き込み取っちゃったら、何も残らないのに……。
と言うことで、JavaとGlassFishに問題がないか探すことに。
まぁ、普通に考えると、GlassFish側かな?Javaなコアなところはみんな触ってるだろうし。

とはいえまずは実測実測。パフォーマンス測定の基本は実測だよね!
GlasFish4.1はJava7でも動くはずだから計ってみた。

ファイル転送Webアプリの転送速度(インターネットを経由させて測定)
サーバー 速度
Java7 GlassFish4.1 26000〜28000bps

あれ……。えーっと……。遅くならない……。
Javaのコアな部分に問題があるってこと……???(゜▽、゜
もうわたしにはわからない、わからないよ。

こうなったらもうプロファイル?を見るしかないよね!
と言うことで、NetBeansのプロファイル機能を見てみる。

Java8で時間がかかってるあたり
call_hierarchy.png

うん、完全にSSLの復号化のところですっごい時間かかってるね。
同じあたりをJava7とJava8で比較してみる。

Java7の呼び出しツリー
call_hierarchy_jdk7.png

Java8の呼び出しツリー
call_hierarchy_jdk7.png

Java8は結構呼び出しを展開してみたんだけど、同じ呼び出しが延々深く続いてるだけみたい。
NetBeansが重くなって最後まで開けなかったけど。

ここから分かるのは、Java7の方は、『AES』とか『CihperBlockChaining』とかが見えるんだけど、
Java8の方は、『AES』の他に、『GaloisCounterMode』とか『GHASH』とかが見える。
使われている暗号化の方式が変わってる……?

Oracleのサイトを見ると、Java7からJava8になったときにGCMって言うのが増えてるんだね。
https://docs.oracle.com/javase/jp/8/technotes/guides/security/SunProviders.html

これか!これが悪さしてるのか!新しいのに遅いなんて!

と、決めつける前に、確認確認。
Java7とJava8とで、それぞれのGlassFish4.1のセキュリティ設定を確認してみる。

Java7のセキュリティ設定
ciphers_jdk7.png

Java8のセキュリティ設定
ciphers_jdk8.png

たしかに、Java8の方が多くなってる〜。
これでJava8の時の暗号化方式をGCM以外から選ぶように設定ッと。
select_without_gcm.png

測定しなおした結果はこんな感じ。

ファイル転送Webアプリの転送速度(インターネットを経由させて測定:GCM除外)
サーバー 速度
Java8 GlassFish4.1 26000〜28000bps

速くなった!原因はGCM、これだね!

GlassFish4.1が関係ないことを確認するために、
Java8で実際に検証コードを書いてみる。

こんな感じ。大きく差が出るだろうから、適当で良いよね。

CBC

	int length = 200000000;
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
Key key = keyGenerator.generateKey();
IvParameterSpec parameters = new IvParameterSpec(new byte[16]);
Cipher CBCEncrypter = Cipher.getInstance("AES/CBC/PKCS5Padding");
CBCEncrypter.init(Cipher.ENCRYPT_MODE, key, parameters);
Cipher CBCDecrypter = Cipher.getInstance("AES/CBC/PKCS5Padding");
CBCDecrypter.init(Cipher.DECRYPT_MODE, key, parameters);
byte[] targetBytes = new byte[length];
long time0 = System.currentTimeMillis();
byte[] encryptedBytes = CBCEncrypter.doFinal(targetBytes);
long time1 = System.currentTimeMillis();
byte[] decryptedBytes = CBCDecrypter.doFinal(encryptedBytes);
long time2 = System.currentTimeMillis();
System.out.println(Arrays.equals(targetBytes, decryptedBytes));
System.out.println("暗号化時間:" + (time1 - time0) + "ミリ秒");
System.out.println("復号化時間:" + (time2 - time1) + "ミリ秒");

GCM

	int length = 200000000;
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
Key key = keyGenerator.generateKey();
GCMParameterSpec parameters = new GCMParameterSpec(128, new byte[16]);
Cipher GCMEncrypter = Cipher.getInstance("AES/GCM/PKCS5Padding");
GCMEncrypter.init(Cipher.ENCRYPT_MODE, key, parameters);
Cipher GCMDecrypter = Cipher.getInstance("AES/GCM/PKCS5Padding");
GCMDecrypter.init(Cipher.DECRYPT_MODE, key, parameters);
byte[] targetBytes = new byte[length];
long time0 = System.currentTimeMillis();
byte[] encryptedBytes = GCMEncrypter.doFinal(targetBytes);
long time1 = System.currentTimeMillis();
byte[] decryptedBytes = GCMDecrypter.doFinal(encryptedBytes);
long time2 = System.currentTimeMillis();
System.out.println(Arrays.equals(targetBytes, decryptedBytes));
System.out.println("暗号化時間:" + (time1 - time0) + "ミリ秒");
System.out.println("復号化時間:" + (time2 - time1) + "ミリ秒");

測定結果は、こんな感じ〜。

# バイト数 時間(ミリ秒)  
CBC GCM GCM/CBC
暗号化 復号化 暗号化 復号化 暗号化 復号化 AVG
1 100000 16 24 160 96 10.0 4.0 7.0
2 100000 16 24 160 96 10.0 4.0 7.0
3 1000000 64 88 1000 915 15.6 10.4 13.0
4 1000000 56 88 1016 912 18.1 10.4 14.3
5 10000000 392 400 8483 7852 21.6 19.6 20.6
6 10000000 376 368 7881 8004 21.0 21.8 21.4
7 100000000 3060 3351 53760 68640 17.6 20.5 19.0
8 100000000 3065 3325 74182 72976 24.2 21.9 23.1
9 100000000 2720 3142 61908 64244 22.8 20.4 21.6
10 200000000 4886 5103 110236 102925 22.6 20.2 21.4
11 200000000 6267 6223 125277 110626 20.0 17.8 18.9
12 200000000 5658 6211 124878 124675 22.1 20.1 21.1

 

うん、サイズが小さいときも遅いけど、サイズが大きくなってくるとだいたい20倍くらい遅いね!これはひどい(゜▽、゜
GCMのWikipedia見た感じだと、速くなりそうなのにね。
遅いにしても限度って言うものがあると思うんだけど……。
もしかして、去年Windowsのアプリが遅くなったのも何か関係あったりするのかな?
暗号化は、WindowsのNativeなAPI使ってるとか……?

まぁ、とりあえず直ったから良いよね(゚ー゚)(。_。)(゚-゚)(。_。)ウンウン

posted by すふぃあ at 21:00| Comment(3) | TrackBack(0) | 雁字

JavaEEなWebアプリケーションを作ろうとしたときのお話

☆はじめに☆

この記事はJava Advent Calendar 2014の12月21日分の記事だよ。

前は、2012年の時にAndroidの話を書いたんだけど、
今回は、Webアプリケーションを作ろうとしたときの話を書くのだ。

☆導入☆

去年の12月くらいに、いつも使ってたWindowsのファイル転送アプリが凄ーく遅くなって、
もう、これは耐えられない!ってことで、代わりにGlassFish4のWebSocketを使ったファイル転送Webアプリを作ったの。

で、先月、他にもなんかWebアプリケーションを作ってみようかなって思って、
ついでだからGlassFishを4.1にバージョンアップ!さらにJavaも7から8にバージョンアップ!したんだけど、
そのとたんに、ファイル転送Webアプリの速度が凄〜く遅くなったんだよね。

と言うわけで前編はその時のお話だよ。

で、遅いのは解決したんだけど、
新しいWebアプリは、ただ、転送するだけのアプリと違って、
いろいろ作りたかったから、
ちょっとがんばってユニットテストも作ろうかな!って思ったんだよね。

と言うわけで後編はその時のお話だったり。

ちなみに、今回作ったWebアプリケーション自体については何も書いてなかったりする(゜▽、゜

それと、今回はホント、ネタのあたりから、検証あたりとか、いろいろ協力してもらえて助かったのだ(゚ー゚)(。_。)(゚-゚)(。_。)ウンウン
間接的にも助けてもらったりしたし、みんなに感謝なのだ(゜▽、゜

posted by すふぃあ at 21:00| Comment(1) | TrackBack(0) | 雁字

2012年12月08日

Androidのjava.util.zipパッケージと戦った記憶

この記事はAdvent Calendar 2012 Javaの8日目だよ。

前の日の記事はryu22e(@ryu22)さんで、
Javaのデータベースマイグレーションツール「Flyway」 #JJUG』だったね。
次の日の記事はHideki Kishida(@quicy)さんがやってくれるみたいだよ。

で、この記事では、わたしが最近作っている『コミみみ』って言うAndroidアプリの動きを確認していたときのことを淡々と書くのだ。
読みやすさは全く考えられていないので覚悟して読んでね。

コミみみ』って言うのは、コミックマーケット用のアプリ……まぁ、えっと、カタログViewerなんだけど、
サークルカット(画像)をZIPで1つのファイルにまとめたものを読み込んでいるんだ。
その読み込みをするときに問題が起こったの。

ZIPファイルをJavaで扱う定番は、java.util.zipパッケージのZipFileクラスだよね。
で、ZipFileクラスで有名な問題といえば、
開こうとしているZIPファイルに含まれているファイル数が65535個を超えていると開けないってこと。
ZIP64っていう拡張仕様までサポートしてないよってことだから、不具合と言うより仕様かな。
これはAndroidに限らずある問題だね。Java7ではサポートしたみたいだけど、AndroidはJava6までだし。

65535個って言うのは、unsigned short型(2バイト)での表現の限界から来ているみたい。
だから、ZIP64をサポートしないと、それより多いファイルを含むZIPファイルは開けない。

Androidでわたしがぶつかった問題も、この『含まれているファイルの数』によるものなの。
この問題をぐーぐるさんで検索すると『多すぎるからだよ!』って言うのがたくさんヒットするよ。

でも、カタログでサークルカットが多いって言っても、35000個くらいしかないんだよね……。
65535個を超えてなくても開けないっ。
試して見ると、だいたい32000個を超えてきたくらいで開けなくなるんだよね。

はい、これでわかっちゃうと思うけど、32767個を超えると開けなくなるみたい。
Javaにはunsignedな型はないから、
unsigned shortで表現されている『含まれているファイルの数』を持つためにはint型が必要になるんだよね。
short型は-32768〜32767までしか表現できないから。
たぶん、short型でファイルの数を持っちゃっておかしくなっているんだと思う。

あれ?でも、ちょっと待って。なんでこんな問題があるの?
java.util.zipパッケージなんて大昔からあるわけでしょ?
そこに、こんな問題があるなんておかしいでしょ!

で、調べてみるとAndorid 2.3.3系では発生しないんだよね。これ。
でも、Android 4.0.xでは発生するの。Android 3.xはどっちかな〜?まぁ、どっちでもいいよね。
つまり、これ、最近……でもないかもだけど、java.util.zipパッケージが書き換えられたことで起こっているみたい。

……え?やっぱり、おかしくない?
拡張仕様をサポートしたわけでもないのに、ZipFileの実装が書き換わってる……?

Android 2.3.3系は……大丈夫っ。でも、その後書き換えられておかしくなっている……。

もしかして、OracleとGoogleの訴訟の問題から来てたりするのかな?
やっぱりGoogle、やっちゃってたのかな?とか想像してみちゃった。
詳しいこと何も知らないけどね!

Android 4系のアプリサポートはこれからだろうから、
ZIPでファイルをいっぱい使うする人は気をつけようね。

そこ!ZIPファイルで数万もファイル扱うことなんて無いなんて言わないでっ!
あ、忘れるところだった。解決方法だけど、
  1. ZIPフォーマットは簡単だから自分で作る。
  2. Android 2.3.3系は動くからそっちから持ってくる。
  3. org.apache.tools.zipパッケージを使う。
って言う感じかな。最後のは、java.util.zipパッケージより20倍〜40倍遅いからやめた方が良いと思うけど。
posted by すふぃあ at 11:34| Comment(2) | TrackBack(0) | 雁字

2012年09月15日

Androidのライブラリを作ってみたよ

Empressia for Androidって

Android用のライブラリを作ってみたよ。
いくつか便利なクラスを用意してあるから、紹介するね。
※Android 2.3.3(API Level 10)以上、r16以上のSupport Library v4を使ってるよ。

DBUnit

JPAみたいな@Idと@Columnのアノテーションをデータクラスにつけて、
簡単にDBのやりとりをするクラスだよ。
SQLかかなくてすむわけじゃないけど、だいぶ楽になるんじゃないかな〜。

基本的には、継承して使ってね。たとえばこんな感じ。
public class HogeDBUnit extends DBUnit {
@Override
protected Class<?>[] getManagedClasses() {
return new Class<?>[] { DataClass1.class, /* DataClass2.class, DataClass3.class */ };
}
}
public class DataClass1 {
@Id @Column
public int ID;
@Column
public String Name;
}

これで、DBUnit使うときに適当なCREATE文が自動で作られてDBが準備されるよ。
アノテーションはsetterとかにつけてもok。

データを全部取ってくる場合は
List list = this.DBUnit.findAll(DataClass1.class, null);
これで全件取得できるよ。

まぁ、JOINとかWHEREとかを書くときは、全部自力で書かないといけないんだけどね。
でもオブジェクトにマッピングするところは自動でできるよ。

SharedPreferences

Androidに用意されてるSharedPreferencesのキーってStringなんだよね。
ローカライズするものじゃないからだと思うけど、キーをリソースにしたくなったりしない?
そういう場合は、このクラス。

SharedPreferences prefs = new SharedPreferences(originalPrefs);
こんな感じでラップすればリソースをキーに使えるよ。
SharedPreferences prefs = SharedPreferencesUtilities.getPrivate(context);
ってやれば、アプリ用の設定を一発で取ってくることもできるよ。
う〜ん、微妙?
キーをリテラルで埋め込んでるよりはましかな〜って思ったんだけど。 

AbstractListAdapter

ListAdapterでなんかViewのリサイクルとか考えるのが面倒だから、AbstractListAdapterって言うのを用意してみたよ。
createViewとupdateViewをそれぞれ実装すれば良いだけだから簡単だよ。

AbstractListPagerAdapter

ViewPager用のクラスを作ってみたよ。
ViewPagerは元々、いろんな見栄えのページを切り替えるために作られたと思うんだけど、
同じ見栄えのページをListViewみたいに連続して表示したいと思うことってない?

コンストラクタで、レイアウトと表示するデータのリストを渡すと、あとは自動で切り替えとかするよ。

updateViewでViewを書き換えるだけで横に1ページごとに切り替えられるListViewのできあがり。
PagerAdateprとか、FragmentPagerAdapter、FragmentStatePagerAdapterから作るよりも
楽に作れてメモリも節約できるんじゃないかな〜。

他にもいろいろクラス入ってるので試してみてね('-^*/
Javadocもあるんだけど、なんかメモとか混入してるからほしい人がいればってことで……(゜▽、゜

posted by すふぃあ at 15:22| Comment(1) | TrackBack(0) | 雁字