2019年12月08日

Visual Studio CodeでParaya MicroのWebアプリケーションを作る準備をする

☆☆☆はじめに☆☆☆

Java AdventCalendar 2019 8日目の記事だよ。

☆☆☆この記事のゴール☆☆☆

この記事では、Visual Studio Codeで、
Payara Microを使うWebアプリケーションを快適に作る環境を紹介することにしたよ。
ほら、なんか、Jakarta EE 8?って言うのが出たみたいだし。

というわけで、使うのはこの辺。

・Visual Studio Code(以降、VSCode)
・Java11
・Payara Micro
・Gralde

Windows10の環境で作るけど、
後で出てくるrobocopyコマンドをrsyncで置き換えれば、
他の環境でも動くんじゃないかなぁ。

あ、作った結果は、ここ(GitHub)にあるよ。

☆☆☆目標☆☆☆

で、まず、快適ってなんだろう?
ってことで、今回達成したい目標を上げておくね。
目標1.Gradleでビルドできること。
目標2.Gradleから実行できること。
目標3.VSCodeでビルドできること。
目標4.VSCodeから実行・デバッグできること。
目標5.VSCodeで起動したままreloadできること。
目標6.VSCodeで上の作業が楽にできること。

ほら、ソース書き換えて、すぐにコンパイルできて、
reloadできないとやってられないでしょ?
そこまではやらないとね!

☆☆☆本編☆☆☆

☆☆手順〜概要〜☆☆

さて、最終的なものもあるし、
VSCodeにJava11とJavaの拡張機能の設定まで終わっているって言う前提で、
記事としては細かい設定付近がメインに書いていくよ。それ以外はスルーで。

とはいえ、目標達成のための基本的な手順はこんなかんじ。
手順1.VSCodeのワークスペースを作ってGitリポジトリを初期化する。
手順2.Gradleの基本的な設定をする(目標1,2、3)。
手順3.GradleにVSCodeでのWebアプリ用の追加設定をする。
手順4.VSCodeのWebアプリ起動用の設定をする(目標4)。
手順5.VSCodeのWebアプリ更新用の設定をする(目標5)。
手順6.快適にするための設定をする(目標6)。

うん!簡単!って言うか、まぁ、ただの設定だしね。
GitHubのコミットは大隊この手順でコミットしてあるから参考にしてね。

☆☆手順1.VSCodeのワークスペースを作ってGitリポジトリを初期化する☆☆

ここで気をつけるのは、
.code-workspaceのファイルをプロジェクトのファイルと同じディレクトリに置くことかな。

RedHatの提供しているJavaの拡張機能のせいだと思うけど、
VSCodeのマルチワークスペースにうまく対応していないみたいで、
階層構造に制限があるみたいなんだよね。

実際、Javaのプロジェクト複数同じVSCodeのWindowで開くとうまく動かなくなったりするし。
あとは、.gitingnoreに、諸々の自動生成されるファイルを指定すれば完了!
この辺かな。
・Gradleの出力先のbuildディレクトリ。
・VSCodeの出力先のbinディレクトリ。
・Javaの拡張機能が自動生成する.project、.classpath、.settings。
・Gradleの本体の.gradleとローカル設定のgradle.properties。

☆☆手順2.Gradleの基本的な設定をする(目標1,2、3)☆☆

さて、実際に、Graldeの設定ファイルを置くよ。
まだ中身はGradle用のを書いているだけだからふつーだから、特筆するようなことは無いよ。
とりあえず、こんなかんじ。

plugins {
id "java";
id "war";
}

ext.moduleName = project.name;

repositories {
jcenter();
}

dependencies {
// use Payara Micro.
providedRuntime group:"fish.payara.extras", name:"payara-micro", version:"5.194";

// use JUnit test framework.
testImplementation "org.hamcrest:hamcrest:2.2";
testImplementation "org.junit.jupiter:junit-jupiter-api:5.5.2";
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.2";

}

tasks.withType(JavaCompile) {
options.encoding "UTF-8";
options.compilerArgs << "-parameters";
}

test {
useJUnitPlatform();
}

war {
baseName moduleName;
manifest {
attributes (
"Automatic-Module-Name": moduleName
);
}
}

javadoc {
source = sourceSets.main.allJava;
options.charSet "UTF-8";
options.encoding "UTF-8";
options.addStringOption("Xdoclint:none", "-quiet");
}
task javadocJar(type: Jar) {
baseName war.baseName;
classifier "javadoc";
from javadoc.destinationDir;
}
javadocJar.dependsOn(tasks.javadoc);
artifacts {
archives javadocJar;
}

task start(type: JavaExec) {
main = "-jar";
args = [configurations.providedRuntime.find { it.name.contains("payara-micro-") }.absolutePath, "--deploy", "\"${buildDir}/libs/${moduleName}.war\""];
}
start.dependsOn(tasks.war);

Gradleの設定を一通り置いたら、
『./gradlew build』とか、『./graldew start』とかで、
ビルドと実行の確認ができるはず!

あと、code-workspaceのsettingsに、

		"java.configuration.updateBuildConfiguration": "automatic"

を追加することをおすすめするよ。
VSCodeで、build.gradleを変更するたびに右下に更新する?って聞かれるから、
自動反映するならこの設定だね。別に聞かれて、Alwaysって選んでも同じ設定入るけど。

これで、.classpathが作られれば、VSCodeで、補完、コンパイルあたりができるようになるから、
目標1と目標2と目標3は達成だね!

☆☆手順3.GradleにVSCodeでのWebアプリ用の追加設定をする☆☆

さて、ここからが本番だよね。どうやって統合していくのかってところ。
ポイントになるのは、この辺。

・VSCodeのJava拡張がWebアプリに対応していない。
 →Graldeのプラグインで設定されるprovided〜の設定がimplmentionとかruntime扱いになる。
  →VSCodeで実行するには、ライブラリを明示的に指定しないとダメ。
 →webappが出力先にコピーされない。
・build.gradleでライブラリを追加とかしたときには、update projectしないと補完機能に反映されない。
 →でも、反映するときには、.classpathが上書きされて出力先がbin/mainに固定されてしまう。
  →でも、クラスファイルは『デプロイ先の』WEB-INF/classesに無いとクラスローダーに『正しく』読み込まれない。
   ※そうしなくても、一見動くけど、CDIとか(Java EE Security APIとかJerseyの実装とか)さわり始めると悲惨なことになる。
・JavaのDebugプラグインがWebアプリに対応していない。
 →Gradleのプラグインで設定されるprovided〜の設定がimplmentionとかruntime扱いになる。
  →勝手に実行時のクラスパスに追加される。
・JavaのDebugプラグインで起動時に指定するmainClassはclasspathRuntime上に存在しないと起動できない。
 →Gradleみたいに、mainClassを『-jar』とか指定できない。
・GradleでDLされたライブラリのパスはとてもわかりにくい。
 →明確なパスが得られないと困る。

このパズルを解けばok!

このために、Gradleさんには、実行時に必要なライブラリをコピーしてもらうことにするの。
copyRuntimeタスクとcopyLibrariesタスクを用意するよ。

copyRuntimeは、payara-microと、他に必要なものがあればって感じ。
DB接続用のjarとかはWEB-INF/libの方に入れてってどこかに書いてあった気がするから、
payara-micro-*.jarくらいな気がするけどね。だから、こんなかんじ。

task copyRuntime(type: Copy) {
into "${buildDir}/runtime/";
from configurations.providedRuntime - configurations.providedCompile;
}

copyLibrariesは、warを作るときに、warの中に入るライブラリを用意するよ。
implementationとruntimeが対象かな。と言っても、直接は取り出せないから、こんなかんじで……。

def libraries = configurations.runtimeClasspath - configurations.providedRuntime;
task copyLibraries(type: Copy) {
into "${buildDir}/libraries/";
from libraries;
}

で、
Javaのデバッグプラグインがクラスパスの自動生成をするのを抑制しなきゃいけない

上でコピーしたものをクラスパスに入れないといけないんだけど、
Javaのデバッグプラグインだと、ワイルドカードが使えないんだよね。
だから、ファイルに設定を書き出して、それをクラスパスに指定することで、両方の問題を解決するよ。
そうすると、タスクはこんなかんじ。

task createArgfile {
def argfile = file("${buildDir}/argfile/argfile.txt");
def runtimeDirectory = file("${buildDir}/runtime/");
def librariesDirectory = file("${buildDir}/libraries/");
runtimeDirectory.mkdirs();
inputs.dir(runtimeDirectory);
if(libraries.isEmpty() == false) {
librariesDirectory.mkdirs();
inputs.dir(librariesDirectory);
}
outputs.files(argfile);
doLast {
argfile.parentFile.mkdirs();
argfile.write("\"" + (runtimeDirectory.listFiles() + librariesDirectory.listFiles()).join(";").replaceAll("\\\\", "\\\\\\\\") + "\"", "UTF-8");
}
}

これは、コピーの後に実施しなとダメだから依存関係をつけるよ。

createArgfile.dependsOn(copyRuntime);
createArgfile.dependsOn(copyLibraries);

Gradle側の設定はこれでおしまい。

☆☆手順4.VSCodeのWebアプリ起動用の設定をする(目標4)☆☆

続けて、これをVSCode側で受け取りながら実行できるようにするよ。
最終的に動かす構成としては、こんな配置にするね。

・/bin/main ← VSCodeがコンパイルしたクラスファイルとリソースが入る。
・/bin/application ← デプロイ先(名前はわたしが勝手に決めた)。
・/bin/server ← PayaraMicroのサーバーの場所(わたしが勝手に決めた)。

このために必要な設定は、以下の4つ。
・設定1./bin/mainを/bin/application/WEB-INF/classesの下に配置するタスクを作る。
・設定2./src/main/webappを/bin/applicationの下に配置するタスクを作る。
・設定3.GradleのVSCode用タスクと、設定1のタスクと設定2タスクを呼ぶタスクを作る。
・設定4.起動する設定を作る。

☆設定1./bin/mainを/bin/application/WEB-INF/classesの下に配置するタスクを作る☆

/.vscode/tasks.jsonに、こんなかんじで作るよ。

		{
"label": "CopyBinToApplication",
"type": "shell",
"command": "robocopy \"\"\"${workspaceFolder}/bin/main/\"\"\" \"\"\"${workspaceFolder}/bin/application/WEB-INF/classes/\"\"\" /e /purge /xo /nfl /ndl /np /njh /njs /r:1 /w:1; exit $(if($LASTEXITCODE -le 8) { 0 } elseif($LASTEXITCODE -eq 16) { if(Test-Path \"\"\"${workspaceFolder}/bin/application/WEB-INF/classes/\"\"\") { 0 } else { New-Item -Path \"\"\"${workspaceFolder}/bin/application/WEB-iNF/classes/\"\"\" -ItemType Directory } } else { $LASTEXITCODE })"
}

これでディレクトリが同期できる。

☆設定2./src/main/webappを/bin/applicationの下に配置するタスクを作る☆

設定1と同じ感じでこんなかんじ。

		{
"label": "CopyWebappToApplication",
"type": "shell",
"command": "robocopy \"\"\"${workspaceFolder}/src/main/webapp/\"\"\" \"\"\"${workspaceFolder}/bin/application/\"\"\" /e /purge /xo /nfl /ndl /np /njh /njs /r:1 /w:1 /xd WEB-INF /xf .reload; exit $(if($LASTEXITCODE -le 8) { 0 } elseif($LASTEXITCODE -eq 16) { if(Test-Path \"\"\"${workspaceFolder}/bin/application/\"\"\") { 0 } else { New-Item -Path \"\"\"${workspaceFolder}/bin/application/\"\"\" -ItemType Directory } } else { $LASTEXITCODE })"
}

でも、クラスファイルが入るWEB-INディレクトリFと、この後作る.reloadファイルを除外しているよ。
webappの中に、除外したディレクトリとファイルは作れなくなるけど、まぁ、作らないよね。
一応、リポジトリには、WEB-INFのディレクトリを同期するタスクもCopyWEB-INFToApplicationって名前で追加してあるよ。

☆設定3.GradleのVSCode用タスクと、設定1のタスクと設定2タスクを呼ぶタスクを作る☆

ここは迷うことなくこんなかんじ。

		{
"label": "PrepareLaunchApplication",
"type": "shell",
"dependsOn": ["CopyWebappToApplication", "CopyWEB-INFToApplication", "CopyBinToApplication"],
"command": "./gradlew copyRuntime copyLibraries createArgfile"
}

☆設定4.起動する設定を作る☆

ここからはlaunch.json。

		{
"type": "java",
"name": "Debug (Launch) - PayaraMicro",
"request": "launch",
"classPaths": ["@${workspaceFolder}/build/argfile/argfile.txt"],
"mainClass": "fish.payara.micro.PayaraMicro",
"vmArgs": "--add-modules java.se --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/java.util.stream=ALL-UNNAMED",
"args": "--rootdir \"${workspaceFolder}/bin/server/\" --deploy \"${workspaceFolder}/bin/application/;\"",
"preLaunchTask": "PrepareLaunchApplication"
}

classPathsにGradleで作った引数のファイルを指定して、
JavaDebugの拡張機能のクラスパスの自動生成を抑制しながらクラスパスを指定するよ。
mainClassには、Payara Microを-jar指定したときに呼ばれるクラス名を指定する。
あとは、rootdirとdeployを指定すればokだね。
あ、deployは;を最後につけてルートコンテキストに配置されるようにしているよ。

と、ここまでで起動できるはず。VSCodeでF5でデバッグ起動してみてね。

これで目標の4を達成だね。

☆手順5.VSCodeのWebアプリ更新用の設定をする(目標5)☆

ここまできたら後は簡単。Payara Microのreloadの設定をするよ。
デプロイ先に.reloadファイルを作れば良いから、tasks.jsonにこんなかんじに作るよ。

		{
"label": "ReloadApplication",
"type": "shell",
"dependsOn": ["CopyWebappToApplication", "CopyWEB-INFToApplication", "CopyBinToApplication"],
"command": "if(Test-Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\") { Set-ItemProperty -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" -Name LastWriteTime -Value $(Get-Date) } else { New-Item -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" }"
}

うん、これだけ。
サーバーを起動してある状態でこのタスクを動かすと、再配置されるのがログに出るはず。
これで目標5も達成だね。

☆目標6.VSCodeで上の作業が楽にできること☆

最後に、いろいろ楽にできるように設定しようね。

実行は、F5でできるから、reloadも簡単にできるようにしたいよね。
というわけで、VSCodeでのビルドタスクにreloadを設定するよ。こんなかんじ。

		{
"label": "ReloadApplication",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": ["CopyWebappToApplication", "CopyWEB-INFToApplication", "CopyBinToApplication"],
"command": "if(Test-Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\") { Set-ItemProperty -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" -Name LastWriteTime -Value $(Get-Date) } else { New-Item -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" }"
}

デフォルトのビルドに設定したから、Ctrl+Shift+Bでいけるようになるよ。
webappだけにしたいなら、Reloadapplicationタスクじゃなくて、CopyWebappToApplicaitonタスクをビルドタスクにすれば良いと思う。
他のキーとかに割り当てたければ、keybindingsとかに設定するのもありかもね。

後はログの出し方かな。
ここまでの状態だと、タスクのログが前に出てきて、サーバーのログが見にくいよね。

だから、タスクのログをエラーの時だけ出すようにして、
サーバーのログをTerminalじゃなくてDebug Console側に出すようにするよ。
PayaraはVT100風のログを書くみたいだから、そっちの方が見やすいしね。

まず、タスクのログをエラーの時だけ出すようにするには、こんなかんじにするよ。

		{
"label": "ReloadApplication",
"type": "shell",
"presentation": {
"reveal": "silent"
},
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": ["CopyWebappToApplication", "CopyWEB-INFToApplication", "CopyBinToApplication"],
"command": "if(Test-Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\") { Set-ItemProperty -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" -Name LastWriteTime -Value $(Get-Date) } else { New-Item -Path \"\"\"${workspaceFolder}/bin/application/.reload\"\"\" }"
}

これは、ReloadAppliactionの例だけど、
普段は見なくてもいいタスク全部に対して、presentation.revealをsilentに設定するよ。

これで、普段はログがでないけど、エラーの時は表示されるよ。
どうせ最終的にはサーバーのログでデプロイまで終わったか見るだろうから、これで良いと思うの。

で、あとは、サーバーのログだね。
launch.jsonでこんなかんじにするよ。

		{
"type": "java",
"name": "Debug (Launch) - PayaraMicro",
"request": "launch",
"console": "internalConsole",
"classPaths": ["@${workspaceFolder}/build/argfile/argfile.txt"],
"mainClass": "fish.payara.micro.PayaraMicro",
"vmArgs": "--add-modules java.se --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/java.util.stream=ALL-UNNAMED",
"args": "--rootdir \"${workspaceFolder}/bin/server/\" --deploy \"${workspaceFolder}/bin/application/;\"",
"preLaunchTask": "PrepareLaunchApplication"
}

consoleをinternalConsoleにするだけだね。これで、ログがTerminalからDebug Consoleに移って、
色もつくはず。まぁ、この辺は好みかもしれないけどね。

別に、どっちでも、ブレークポイントとかはちゃんと止まるから、
標準入力をどうしたいかとかの好みで設定しても良いかもね。

ってことで、ここまでで、開発環境によらずに、Gradleでのビルドと実行をできるようにしながら、
VSCodeで簡単なキーバインドで起動と更新ができて、ログも確認しやすい状況が完成だよ!

全目標達成かな?(゜▽、゜

ここまででプロジェクトの名前が入っているのは、
ルートとプロジェクトのディレクトリ名と、
settings.gradleとcode-workspaceくらいかな。

他のOS版とかカスタマイズも、ここまで来れば結構簡単にできそうだよね。
robocopyをrsyncにするくらいかな?

以上!お疲れ様でしたなのだ!('-^*/

posted by すふぃあ at 00:00| Comment(0) | TrackBack(0) | プログラム

2017年12月23日

スパゲッティとライフサイクルとドメイン

この記事は、〇〇勉強してみた Advent Calendar 2017の23日目(12/23)の記事だよ。
次の記事はtosssaurusさんの『canvasを1ヶ月勉強してみた』みたい。

☆☆☆スパゲッティコードとの出会い☆☆☆

最近は、Javaじゃなくて、C#とかTypeScriptとか使ってるんだよね。
ほら、最近のアプリケーションって非同期処理が必須じゃない?
そうすると、スパゲッティなコードを割と自然に解消できるasync/awaitってすごいなぁって思うんだよね。

と言うか、ここのところ、いろんな人のコードをいっぱい見る機会があって、
async/awaitじゃなくても、『そういった構成』にしてないからスパゲッティになっているってコードを結構見かけて、
スパゲッティなコードってこういう風に作られていくんだなぁって思ったから、まとめてみようかなって。

Wikipediaとかみても、

・goto文の濫用
・多重継承の濫用
・スコープの拡大
・技術不足

とかしか書いてないし、
これって、特に最後の技術不足って、わかりません。って書いてあるようにしか思えないしね。
少しでも、スパゲッティなコードの理解と回避につながればいいなって思うの。

☆☆☆スパゲッティの概念的な定義☆☆☆

いつものことだけど、わたしの勝手な考えだから、鵜呑みにしておかしなことになっても知らないよ!

ってことで、いきなりわたしの答えから書いちゃうと、
次の2種類の書き方をしているプログラムをスパゲッティって言うんだと思う。
A. ライフサイクルに関する処理をライフサイクルの層から外してプログラムを構成する。
B. ドメイン重視のコンポーネント単位ではなく、アクション(1連の処理の流れ)としてプログラムを構成する。

ちなみに、これはだめな書き方だから、じゃぁ、どう書けば良いの?って観点で行くと、
『ライフサイクルやドメインにそった開発をする』っていうことかな。
そういう意味では、DDDな書き方している人はスパゲッティなコード書くことないのかもしれないね。

まぁ、どう書くべきか、みたいなのは、時間とともに変わっていくと思うけど。

☆☆☆スパゲッティの具体な成長例☆☆☆

じゃ、わかりやすい例を。
例だから、現実的かどうかとかは大目に見てもらえると助かる感じ?
わかりやすさ重視で!

一番わかりやすいのはこう言うのかな……。
『画面のボタンを押して、処理が終わるまで押せなくする』みたいな。
あんまりしない気もするけど例だから気にしないっ。

1.処理の配置間違えた。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething();
}

private void doSomething() {
// なが〜いなが〜い処理。
Thread.Sleep(2000);
this.Button.IsEnabled = true;
}

あ、えっと、一応、UIにボタン(Button)があって、押すとButton_Clickが呼ばれるってことで。
ここの例は、VisualStudioでWPFのプロジェクトとテンプレで作って、MainWindowにButtonを配置してイベントを作っただけだよ。

さて、こんなコード書かないよ!って?いや、まぁ、普通はそうだとわたしも思うんだけど。
と言うか、そもそもボタンの切り替え動かないし……っていうのは置いといて。
特に、メソッドを切り出すような処理がなければ、こういう書き方することはあり得ないね。

これは、定義のAになっちゃっているのがよくわかると思うんだよね。
切り替えている処理が別々のメソッドに分散しちゃってるから。

じゃ、次はもうちょっと、書きそうな例ってことでちゃんとボタンが切り替わるように非同期にするよ。

2.非同期にした(非同期場所間違えた)。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething();
}

private void doSomething() {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
});
}

このくらいになると、書く人がちょっとだけいそうな気がする。

え?あ、うん、いや、まぁ、元の問題と対応があってないのはわかってるんだけど、
この流れだと、こう考えて書く人多そうだなってことでよろしく!
JavaScriptなWebだと、元に戻すのにわざわざDispatcherとかの行はいらないから、シンプルになって、余計に書く人多そう。

#1で書かないって人は、もちろん、#2も書かないと思うんだけど。
じゃぁ、次でやっとUIの処理まとめる気になったことにする。
実際は、doSomethingとかがさらに複雑になるとか。呼び出し元の処理が複雑になると気づくはずだよね。

3.UIの記述場所をまとめたつもり。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.Button.IsEnabled = true;
});
}

private void doSomething(Action callback) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
this.Dispatcher.Invoke(callback);
});
}

さて、これは、さらに書く人多そうな形だと思うんだよね。
コールバックっていうんだっけ。なんかすごく好きな人がいるみたいだよね。
わたしは、あまり使うの好きくないんだけど。

でも、doSomethingのメソッドがUIでの後処理を意識したメソッドになってしまっているね。
もうちょっと、ちゃんとUIを寄せてみようかな。

4.コールバックでUIと分ける。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
});
}

private void doSomething(Action callback) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
callback();
});
}

うん、まぁ、何となく良くなった感じ?
じゃぁ、ここで、なが〜いなが〜い処理、ずっと適当にSleepとか書いてきたんだけど、
まぁ、普通は通信処理とかになったりするわけで、当然例外とかあり得るわけだよね。
そうしたらどうなるかなぁって。

はっ、#2でおかしい対応していたことがばれてしまう!?(いまさら

5.コールバックでUIと分ける。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.doSomething(
() => {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
},
(message) => {
this.Dispatcher.Invoke(() => {
MessageBox.Show(message);
});
}
);
});
}

private void doSomething(Action callback, Action errorHandler) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
try {
Thread.Sleep(2000);
// throw new Exception("hoge");
} catch(Exception ex) {
errorHandler(ex.Message);
} finally {
callback();
}
});
}

もうすでに、これはない……って感じになってきているわけだけど、
たぶん、少しずつ『処理を追加していってる』とか、『全体の構成じゃなくて一部だけ見直している』とかだと、
こうなっちゃう人、割といるんじゃないかなぁ?
ここでは、finallyにcallback置いたけど、finallyじゃなければ、Button_Clickの実装はまた変わってくるよね。
doSomethingの作りに非常に強く影響受けている感じ。
ほんと、ちょっとした変更も一蓮托生で、動き全部把握しておかないと即死って感じ?(物騒

まぁ、もうこのくらいでいいかな。
というわけで(?)、こういう風にコードが成長していくとスパゲッティになっていくって言うのは伝わったかなって。

じゃぁ、どうすればいいのかっていうと、次のような感じ。
まずは、Taskを戻すよ。

6.制御を戻す。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
Task task = this.doSomething();
Task wait = Task.Run(() => {
try {
task.Wait();
} catch(AggregateException exs) {
this.Dispatcher.Invoke(() => {
exs.Handle((ex) => {
MessageBox.Show(ex.Message);
return true;
});
});
} finally {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
}
});
}

private Task doSomething() {
// なが〜いなが〜い非同期処理。
Task task = Task.Run(() => {
Thread.Sleep(2000);
// throw new Exception("hoge");
});
return task;
}

スパゲッティになった状態ではっきりわかる最大の問題は、
『非同期の制御を元に戻していない』ことだから、そこを直した感じ。

Taskを作ってる箇所が複数になって、もう#2あたりでのこともはっきり目に見えちゃうね。

じゃ、ちょっと、見にくいから、async/await使うよ。

7.async/awaitで。

private async void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
try {
await this.doSomething();
} catch(Exception ex) {
MessageBox.Show(ex.Message);
} finally {
this.Button.IsEnabled = true;
}
}

private Task doSomething() {
// なが〜いなが〜い非同期処理。
Task task = Task.Run(() => {
Thread.Sleep(2000);
// throw new Exception("hoge");
});
return task;
}

どうかな〜。だいぶわかりやすくなったかな〜。
async/awaitを使うと、非同期関係のところは、正しいライフサイクルに導いてくれる気がするんだよね。
補助してくれるというか。

まぁ、別にasync/await使おうってことじゃなくて、
非同期はTaskを戻すのがライフサイクルとかドメインを考える上で重要ってこと。
そして、ライフサイクルとかドメインを考えないとスパゲッティになっていくってこと。

どんどん処理を追加していくんじゃなくて、
ちゃんと、ライフサイクルとかドメインを考えて構成しないと、スパゲッティになるよってことっ。

たぶん、スパゲッティコードが生まれる理由は、
コンポーネント単位でプログラムを書かないで、
アクション単位でコードをがんがん追加しちゃうのが原因じゃないかな。
言葉の定義怪しいけど。

今回も、Button_ClickにUIに関する制御&表現を、doSomethingに実際に実行&実現したいことを、って、
分けていれば、最初から#6とか#7の方向に進んでいくはずだしね。

ただ、ライフサイクルは、割と簡単だけど、
実際には、ドメインにそって適宜?分割していくのはすごく難しいと思う。

個人で開発していると、ドメインが1個に見えてきちゃうし、
チームで開発していると、ドメインの内部の関係性を把握しにくい感じがする。

☆☆☆スパゲッティを探す☆☆☆

さて、視点をちょっと変えて、すでにスパゲッティになっているのをどう見つけるか、ちょっと考えてみる。
・引数を、使わずに次のメソッドにただ渡している。
 使われているところから発生源を追うんじゃなくて、変数がnewとかされてから、いったいどこまでそれが持ち運ばれていくかってみていく。
 そうすると、だいたい、さっきみたいにUIの最後の処理をするためーとかで、UIそのものをドメイン側の処理に横流ししているとか見つけることが……ある?
 まぁ、あったのっ。すっごく驚いたけど。
・フィールド経由でたまたまそこにある呼び出し先になるメソッドに値を渡す。
 たまに見かけたコードだと、ドメインのメソッド、さっきの例だとdoSomethingは別のクラスとかに分けたりするわけだけど、
 省略のために規模が小さければクラス分けないこともあると思うんだよね。
 と言うか、最初は小さいから分けないことも多いと思うの。

 で、引数で渡すのはおかしい!みたいなことをすると、フィールド経由で渡したりして……、もちろん、これもアウトだよね。
 あとで、クラス分けるとき困っちゃう。

 他のケースだと、全く別のメソッド呼び出していって、
 コールバックで戻ってきたときに使うから、と言う理由で、フィールドに保存しておくみたいな考えっぽいもの。
 これも結構すごかった。

 かければ何でもいいとはわたしは思わないのだ。

・クラスに関係ないインターフェースを実装している。
 これも結構あるかな。さっきの引数で持ち回るとき用でやっていたりするコードを見かけることがあるよ。
 たぶん、ラムダ式とかがわからなかったり、クラスの責任とかを考えなかったり、っていうことだと思うけど。
 かるく、同じアプリケーションコードの中でVisitorパターン発生してるよねきっと。

 インターフェースをクラスに実装している例がいっぱいネット上に転がっているからなのかな?関係ない?

・Task、Future、Promiseを作って、returnしていない(コントロールが手放されている)。
 定番でわかりやすいよね!例としても使わせてもらったし!
 ちなみに、UIとかのもともと戻り値の型がvoidなところで保持する必要がある場合は、
 フィールドに維持して管理するしかない……よね。

☆☆☆まとめ☆☆☆

長々書いてきたけど、結局、最初に書いた定義のAとBによってスパゲッティはできていくと思うの。
ここまで来たら、定義のAとBの違いは、もうわかっちゃっていると思うんだけど、
プラットフォームとかシステムに依存した仕組みとか制限によるものか、
アプリケーションのドメインによる表現とかによるものかの違いかな。

Aがわかるけど、Bがわからないひとは、BをAに強引に当てはめて恐ろしいスパゲッティを作ることが多い気がする。
両方わからない人はいろいろ警戒するから、そこまで元々巨大なスパゲッティは積極的には作らないかな。

つまり、自分がいったいどんなドメインを扱っているのかしっかりとらえるのが重要ってこと。
プログラムは人によって違う書き方になるっていうけど、わたしはあまりそう思わない。

ドメインの設計は人によって結構大きく変わるけど、そこが落ち着いたら、
それを表現する最適なコードは割と誰が書いても同じになると思っているよ。
まぁ、設計っていうか構成を考えるのとプログラムを一緒にすると、人によって違うっていえるかもだけど。

posted by すふぃあ at 11:35| Comment(0) | TrackBack(0) | プログラム

2017年12月02日

コメントの書き方〜すふぃあ流コメント術〜

☆☆☆はじめに☆☆☆

この記事は、個人開発 Advent Calendar 2017の2日目(12/02)の記事だよ。
前の記事は、kappy0322さんの『個人開発における開発プロセスを公開してみる (2017年冬)』だよ。
次の記事はturanukimaruさんの『ファイアーエムブレムヒーローズの戦闘結果計算ツールをKotlinでDDD的に作ってみた』みたい。

☆☆☆経緯とか☆☆☆

kappy0322さんから、Advent Calendarの招待を受けて、あれ、なんで招待されたのかな〜、とか戸惑いながら、
そういえば、前から書こうかなーって思っていた、コメントの書き方、この際だから書こうかな!と思い立って書くことにしたの。
ここで言うコメントって言うのは、ドキュメントコメントではないコメントのことね。

わたしは、プロの教育とか受けてないから、コメントの書き方も、当然、自己流なんだけど、
最近、いろんな人のソースコードを見る機会があって、
『何でこんなコメント書いてるの?』って思うのをいっぱい見てびっくりしたんだよね。たぶん、プロの人のも結構あったと思うんだけど……。

☆☆☆コメントの書き方の問題☆☆☆

で、どうも教育を受けたことがある人に聞くと、
『そのコードを書いた理由をコメントで書きなさい』
って、教えられているみたい。

実際に、ググってみると、本当に、理由を書きなさいみたいなのがいっぱい出てくる。
うん、いや、そうなんだけどさ、なんでそんな、てけとーな教え方してるの……?って思っちゃった。
いや、だってさ、こういうコメント書かれても困るでしょ。

// データが複数あるのでfor文で回す。
for(int i = 0; i < elements.length; ++i) {
// 略
}
// 処理を分けるために分岐する。
if(something.getID() > 0) {
// 略
}

極端な例ではあるけど、教えられたとおりに、『理由』を書いているよね。
でも、良いコメントだね!……ってならないよね?
だから、『理由を書く』っていうのは不十分だと思うの。

って、こういうところから、何が問題かとか、分析していってもいいんだけど、
わかっている人にはわかっていることだろうし、あんまりおもしろくないから、
その辺は省略して、わたしがコメントを書くときの考え方を紹介していくよ。

別に、書くのがめんどくさくなってきたとかそういうわけじゃない……から、ね?

あと、何度も書くけど、わたしはそういう系の教育受けてないから、これが本当によい方法かどうかはわからない〜。
いろいろ比較とか研究とかできるわけでもないし。
ただ、個人開発で困っている人に、少しでも役に立てば良いな、とは思っているよ。

☆☆☆すふぃあ流コメント術☆☆☆

最大のポリシーは、
『あとで変更するときの判断基準を書く。』
ということ。

だって、システムとかアプリケーションを、作って〜リリースして〜書き換えて〜ってやっていると、
コメントが必要な時って、『そういうとき』だよね?

このポリシーから、コメントを書く、より具体的な方針は、、
『普通ではない書き方をしたところには、その根拠を示す。』
ということになると思っているよ。

コメントを書いていないところは、コードそのままの意図で、おかしな動きをしていたら、それはバグとして直されたり、最適化されたりする。
コメントがあるところは、その妥当性をコメントをみて判断してから、変更や修正ができるってわけ。

たとえば、

// このメソッドの定義より、要素の最後には、○○を含めて返す必要がある。
// ○○は、××の特性を含むため、△△の処理は行ってはならないので除外されるように条件を調整している。
for(int = 0; i < elements.length - 1; ++i) {
// 略
}
return elements;

まぁ、サンプルだから、もっと別のコード書きなよっていうのはあると思うんだけど、そこはそれ。
でも、こうすれば、なんで-1しているの??っていう理由と、それが妥当かどうかの判断ができるはず。
実は、この見解が間違っているとか、見解が変わったとかなら、よりどころとなっているコメントごと直すって感じ。

単に、『理由』ではなくて、変更するときの『判断基準』となることを書いていくのが重要ってこと。
ほんと、ただ、それだけだよ?みんなが知りたいことってそういうことじゃないのかな?

コメントが長くなるじゃん!って言うのもあるかもしれないけど、
必要なことは書くべきだと思うの。
くだらないことじゃないわけだからね。

かなり自分でも困ったソースコードの付近には、15行超えるようなコメント書いたこともあったかな。
まぁ、稀だけど。
読んで判断基準にならないコメントなんて、だからなんなの!って叫びたくなるだけじゃない?

☆☆☆弱点とか☆☆☆

この書き方の最大の弱点は、『普通』っていうのを共有しないといけないこと。
基本的には、必要十分なコードで、設計ポリシーにあっていれば問題は起きないはず。

裏を返すと、実力に大きな差があるチームだと、失敗すると思う。
まぁ、足手まといな人がいると、きっと別の意味で失敗するだろうから、大丈夫な気もするんだけどね。
その辺は想像だから、よくわかんない。

もう一つは、設計が微妙だとコメントがすっごく増えること。
だから、作っているときに気づいた、設計が微妙なところはすぐに直していかないとダメかも。
その設計がおかしいと言うコメントを大量に残すことになるからね。

って、この書き方じゃなくてもコメントが増えること自体は同じかもしれないけど。

☆☆☆よくありそうな質問と回答☆☆☆

Q. 難しいコードに説明コメントつけるのはどうなの?
A.
難しいって、普通じゃないってことでしょ?それは書き換える基準も含めてコメント書かないとだめじゃない?
まさか、難しいのが普通になって……ないよね?
もし、どこもかしこも難しくてコメントだらけになりそうって言うことなら、
それは設計がおかしいんだと思うよ。

Q. 必要なコメントが書かれていなかったら、困ったことになるんじゃないの?
A.
それは、つまり、書き換えるときに、必要な変更の判断基準が書かれてなくて、
さくさく書き換えた結果、間違った書き換えになっちゃうんじゃないか、って言うことだよね。

良いんじゃない?だって、前書いた人(過去の自分含む)さえ、それに気づいてなかったんでしょ?
気づけて良かったじゃない。
と言うだけだと思う。

てけとーに書かれていたのが、たまたま、動いていただけで、それが壊れるだけのこと。
壊れるのは、困るけど、それは、元を書いた人の問題だと思う。

このコメントの書き方をしていると、書き換える元にコメントを残さなかったことが悪いってことになんじゃないかな。
いや、まぁ、どっちが悪いとか言っても慰めにもならないんだけどさ。

なんか、どんなひどいコードでも、動いていれば正義みたいな考え方もあるみたいだけど、
それが『絶対』だとはわたしは思わない。もちろん、動いていることも大事だけどね。
動いていても、手抜きを許すわけじゃないし、ミスはありえることだと思う。

『過去のわたし!しっかりしろ!』って思うこと、よくある。
そこには、きっと、コメント以外の対策みたいなのが必要なんじゃないかと思ってみたりするよ。

Q. 書き直す予定がないときもコメント書くべきなの?
A.
書き直す予定がないって言うのは、バグが絶対にないってことかな。
それとも、バグがあったら、そのソースコードを全く参照しないで作り直すって言うことかな。
というわけで、そんなことは、まず、あり得ないと思うから、書いた方が良いと思う。

ただ、ちょっとしたスクリプトとかまで毎回書くかとかは知らないっ。
でも、やっぱり、あとでスクリプトを発掘すると、
あれ?これなんでこうなっているんだっけ?みたいなことは、わたしもよくあるけどね?(゜▽、゜

☆☆☆まとめ☆☆☆

さぁ、『理由』じゃなくて、『判断基準』をコメントに書いていってみようよ!
コメントを書くときに後の人のことを想像して考えることがやりやすくなるんじゃないかな。
わたしは、何にも責任とれないけどね!('-^*/

posted by すふぃあ at 12:10| Comment(0) | TrackBack(0) | プログラム

2016年12月11日

RoslynのAanlyzerでC#のコードを分析するライブラリ作ってみたよ

これは、C# Advent Calendar 2016の12月11日(11日目)の記事なのだ。

昨日は、@tanaka_733さんのC# 7 on Linux
今日は、RoslynのAnalyzerを使ってみた話を書くよ。

☆☆☆コーディングスタイルを検証する仕組みへの不満☆☆☆

突然だけど、わたしのコーディングスタイルって、C#とJavaの中間みたいな書き方なんだよね。
例えばこんな感じ。

public class Project {
// フィールドだいたいは大文字ではじめるからC#風?
private string Field;
// メソッドはだいたい小文字ではじめるからJava風?
// 『{』は行末に書くからJava風?
public void doSomething() {
string f = this.Field;
}
}

で、VisualStudioってIntelliSenceが優秀だからか、
あまりコーディングスタイルを細かくチェックする仕組みがない気がする。
コーディングスタイルっていうか、静的解析?みたいな分類になるのかな?

いや、ほら、JavaのCheckstyleみたいに細かいチェックをするものって、
StyleCopとかFxCopとかだと思うんだけど、
これって、あんまり自由にカスタマイズする感じじゃない気がするの。

で、いろいろサイトめぐっていたら、
StyleCopがRoslyn使ってる〜とか言う話をとか見て、興味がわいてきた感じ。
ないなら、自分で作っちゃえば良いんだよね。

☆☆☆RoslynなAnalyzerの特徴☆☆☆

細かいこと書き出すと、ホントに書きたいことから遠ざかりそうだから、
基本的な作り方は、neueccさんの記事を参考にしてもらう感じで。
ここでは概要だけにとどめてさくさく具体的な話へいこ〜。

えっと、RoslynのDiagnosticAnalyzerってクラスを使うと、C#のコードの解析を簡単に作れる。
特に、次の点がとっても便利っ。もちろん、もっといっぱい便利な特徴はあるんだけど。
1.字句解析とか構文解析とか諸々省略して、SyntaxTreeがそろった状態から作業が始められる。
2.テンプレートプロジェクト?があるから、すぐ作るのをはじめられる。
3.作ったのは、NuGetで簡単に適用できる。
4.レポートした結果がVisualStudioと連動して見られる。

さぁ、じゃぁ、もう、プロジェクト作った前提で、
どのくらい簡単にかけるのか見てみよ〜。

☆☆☆具体的な例☆☆☆

Initializeメソッドで具体的な検証アクションを登録しておくんだけど、
解析するフェーズはいくつかあって、以下の4つのアクションから選んで解析コードを書く感じ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxTreeAction((c) => { });
context.RegisterSymbolAction((c) => { }, SymbolKind.Assembly);
context.RegisterSyntaxNodeAction((c) => { }, SyntaxKind.None);
context.RegisterSemanticModelAction((c) => { });
}

わたしの場合、使うのは、RegisterSyntaxNodeActionとRegisterSyntaxTreeActionかな。

RegisterSyntaxNodeActionは、名前の通り、読みたいNodeを指定して、そこから解析をしていく時に使うもの。
例えば、if文の書き方を確認したい!とかはこっちを使うといい感じ。

RegisterSyntaxTreeActionは、SyntaxTreeを自分で頭から読んでいくもの。
特に空白系を確認したい場合はこっちを使うといい感じ。

RegisterSyntaxNodeActionは、どうしてもNodeを基準にしないといけないから、Node無関係に空白を解析するのには向かないんだよね。

ちょっとサンプルを書くと、ブロックの手前にスペースが1個あるかどうかの確認はこんな感じ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxNodeAction((c) => { analyze(c); }, SyntaxKind.Block);
}
private static void analyze(SyntaxNodeAnalysisContext context) {
SyntaxNode node = context.Node;
SyntaxToken firstToken = node.GetFirstToken();
SyntaxToken previousToken = firstToken.GetPreviousToken();
SyntaxTriviaList trivias = previousToken.TrailingTrivia;
// 存在しない場合も0になる。
int triviasLength = trivias.FullSpan.End - trivias.FullSpan.Start;
if((triviasLength == 1) && trivias.First().IsKind(SyntaxKind.WhitespaceTrivia) &&
(previousToken.Parent.SyntaxTree.GetText().GetSubText(trivias.FullSpan).ToString() == " ")) {
// ブロックの直前にスペースが1個ある。
} else {
// Ruleはテンプレ生成したときにあるやつなので説明省略で。
Diagnostic diagnostic = Diagnostic.Create(Rule, node.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}

う〜ん、簡単だね!
いや、まぁ、解析のロジック自体は、もちろん、みんなガンバレ!って話になるわけだけど、
構成は……簡単でしょ?

もう一種類も。
これは、行末に空白があったらダメだよっていうやつ。

public override void Initialize(AnalysisContext context) {
context.RegisterSyntaxTreeAction((c) => { analyze(c); });
}
private static void analyze(SyntaxTreeAnalysisContext context) {
SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
IEnumerable trivias = root.DescendantTrivia();
{
SyntaxTrivia previousTrivia = default(SyntaxTrivia);
foreach(SyntaxTrivia trivia in trivias) {
if(trivia.IsKind(SyntaxKind.EndOfLineTrivia) && previousTrivia.IsKind(SyntaxKind.WhitespaceTrivia)) {
if(trivia.FullSpan.Start == previousTrivia.FullSpan.End) {
Diagnostic diagnostic = Diagnostic.Create(Rule, trivia.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}
previousTrivia = trivia;
}
{
SyntaxTrivia trivia = trivias.LastOrDefault();
if(trivia.IsKind(SyntaxKind.WhitespaceTrivia) && (trivia.FullSpan.End == root.FullSpan.End)) {
Diagnostic diagnostic = Diagnostic.Create(Rule, trivia.GetLocation(), "メッセージ");
context.ReportDiagnostic(diagnostic);
}
}
}
}

いける!いける!
StyleCopとか、何を検証しているのかわからないけど、結構カオスなコード書いてあるように見えるんだけど、
ちょっとしたの書くなら、検証する内容がはっきりイメージできれば、結構、簡単にかけるよ!
……はっきり……イメージできれば……ね……。

☆☆☆作ってみたライブラリ☆☆☆

ということで、作ったライブラリは、NuGetにおいてあるから、良ければ試してみてね。
『ECheckStyle』で登録してあるから検索すれば出てくると思うよ。
NuGetパッケージのサイト
プロジェクトサイト
折角だから、今日アップデートしておいたっ。

☆☆☆注意点☆☆☆

作る上での注意点とかを簡単にあげておくと、
1.解析でできるSyntaxTreeが一意ではない。
  解析ロジック考える時に、いろいろなサイトでSyntax Visualizer見ると良いよって書いてあるんだけど、
  これ、実は、エディタ開いた状態で行われる解析と、開いてない状態で行われる解析で、作られるSyntaxTreeが違うの。
  あ、エディタ上の色分けもSyntax Visualizerのとは違うッぽいからこれも参考にするとダメみたいだよ。
  理由は知らないけど、そのせいでだいぶ苦労したっ。特にコメント回りっ!
2.CodeFixを作る難易度が高い。
  Roslynの良いところは、実は、解析の実装が簡単なだけじゃなくて、CodeFixも準備できることにもあると思うの。
  おかしいところをまとめてぽちぽち直せるのは凄いと思う。でも、これ、作るの結構辛いっ。
  解析のロジックを考えるのは、まぁ、変な構文が入ってきても、スルーするだけだと思うんだけど、
  CodeFixはそれもまとめて直してあげないといけないからね。CodeFix作り込むのはかなり難易度高いね。
3.空白の作成で改行あたりが消える。
  CodeFixと言えば、空白の調整とか、自前でやろうとすると、まぁ、空白を作成することになるんだけど、
  SyntaxFactory.ParseLeadingTriviaとSyntaxFactory.ParseTrailingTriviaってあって、
  後者は改行を作れないっぽいって言うか、作ろうとすると静かに消えるから注意なのだ。
  どこにもそのあたりの説明書いてない感じで微妙なんだけど。
4.NuGetで入れたAnalyzerの削除で依存ライブラリが消える。
  StyleCopもそうなんだけど、設定とかの読み込みとかに、Json.NETかな、使っているケースが多いみたい。
  EcheckStyleもそうなんだけどね。
  で、これ、NuGetで入れるときは自動で依存しているライブラリを入れるScriptが動くみたいなんだけど、
  削除する時には、削除するScript勝手に動くみたいで、どうも依存関係が確認されないで消されるみたい。
  うーんと、つまり、StyleCop入れて、ECheckStyle入れて、StyleCop消すと、
  Json.NETのライブラリが消えて、ECheckStyleが動かなくなる。っていうこと。
  この辺は、どうするのがいいのかな……。
5.ファイルの除外について
  これは、ECheckStyleだと設定ファイルでサポートしているから、別に注意って言うわけじゃないんだけど、
  この辺で解析対象の除外について議論されているみたいなんだよね。
  どうなるのかわからないけど、
  解析するライブラリは複数あるわけで、それぞれ、かけたい範囲かけたくない範囲っていうのはあるはずで、
  Roslyn側で全体まとめてかけるかけないって言うのは、ちょっと違うんじゃないかなぁ?とか思ったりしている。
  まぁ、ここでは、ライブラリ作る側でその辺も考えようねって言うだけなんだけどね。

☆☆☆まとめ☆☆☆

いろいろ微妙な点も書いたけど、やっぱり便利だよ。ホントに。
ただ、対象がC#とVBだけなのが淋しいかな。

アプリつくってたらさ、
Webアプリケーションだったら、HTMLとかTypeScriptも書くし、
クライアントだったら、XAMLとかも書くわけじゃない?

この辺もサポートしてくれたら、とっても良いと思うんだけどなぁ(゜▽、゜

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

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) | 雁字