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) | プログラム