投稿者「admin」のアーカイブ

VSCodeでTomcatを使ったWebApp開発 (cargo編)

VSCodeのJava Web開発をTomcatでやる方法のcargo-maven3-pluginを使ったやり方。

cargoを使ったやりかたはGUIは整っていないが、mavenだけで完結してできるのですぐに環境を整えられるのが良い。

  • VSCodeタスクからTomcatの起動
  • VSCodeタスクから起動中のTomcatに再デプロイ
  • VSCodeのリモートデバッグでアタッチしてステップ実行などの確認

Exampleソースコードはこちら。
https://github.com/civic/maven-single-module-webapp-cargo

Tomcatでの開発が多いためTomcatを使用しているが、Cargoは様々なWebコンテナをサポートしているため、Tomcat以外のJettyなどのコンテナも同様に設定可能。

Webアプリケーション

WebアプリケーションとしてはHello World的なServletを例として作成してある。

//Main.java
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class Main extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        var out = resp.getWriter();
        out.println("Hello Servlet! /" + new Date());
    }
}

pom.xml

mavenプラグインとして、cargo-maven3-pluginを設定。
これによりvscode関係なく、mavenから mvn war:war cargo:runするだけでwarを作成してデプロイしてtomcatを実行できる。

cargoにより自動的にTomcat9をダウンロードしてtargetディレクトリに展開し起動してくれる。
設定いらずなのは楽だがcleanすると消されてしまうので予め自分でダウンロードしたパスを選択したりできる。ダウンロードするzipファイルも指定できるので明示的に特定のバージョンのTomcatを入れることもできる。https://codehaus-cargo.github.io/cargo/Installer.html#Installer-Maven3Installer

        <plugin>
          <groupId>org.codehaus.cargo</groupId>
          <artifactId>cargo-maven3-plugin</artifactId>
          <version>1.9.13</version>
          <configuration>
            <container>
              <containerId>tomcat9x</containerId>
            </container>
            <configuration>
              <properties>
                <cargo.jvmargs>
                  -Xdebug
                  -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
                  -Xnoagent
                  -Djava.compiler=NONE
                </cargo.jvmargs>
              </properties>
            </configuration>
            <deployables>
              <deployable>
                <groupId>com.example</groupId>
                <artifactId>single-module-webapp-cargo</artifactId>
                <type>war</type>
                <properties>
                  <context>/example</context>
                </properties>
              </deployable>
            </deployables>
          </configuration>
        </plugin>

task.json

task.jsonはVSCodeのための設定ファイルで使用することは必須ではないが、mavenコマンドで mvn cargo:run を実行する手間を省ける。またタスク経由から実行することで環境変数の指定ができる。
exampleコードでは、.vscode/task.json.exampleとして配置してあるので、それをtask.jsonにリネームすればすぐに使える。

2つのタスクを準備してあり cargo-run でcargoの起動と、cargo-redeploy で起動したままの再デプロイを実行できる。

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "cargo-run",
            "type": "shell",
            "command": "mvn war:war cargo:run",
            "group": "build",
            "isBackground": true,
            "options": {
                "env": {
                    "MY_VAR": "abc123"
                }
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": true,
                "showReuseMessage": false,
            }
        },
        {
            "label": "cargo-redeploy",
            "type": "shell",
            "command": "mvn war:war cargo:redeploy",
            "group": "build",
            "isBackground": true,
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "showReuseMessage": false,
            }
        }
    ]

}

launch.json

VSCodeでのデバッグ実行のための設定ファイルlaunch.jsonも.vscode/launch.json.exampleとして配置してある。

cargo:runで起動したTomcatに対してリモートデバッグでAttachしてデバッグする。

※preLaunchTaskでcargo-runのタスクを実行することで、デバッグ実行開始時にTomcatが起動するような仕組みにもでき、IDEっぽい実行が可能だが、起動完了の確認などが複雑で不安定なためそのような設定はしていない。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Debug(attach)",
            "request": "attach",
            "hostName": "localhost",
            "port": 8000
        }
    ]
}

VSCodeでJavaフォーマッタの設定

CheckstyleによるJavaコーディングルールの適用と合わせて、フォーマッタ設定をしておくと快適です。

フォーマッタ設定ファイルの取得

VSCodeのJava機能は、内部的にEclipseと同じ機能を使っているため、Eclipseの設定ファイルを入手して設定することで可能です。

https://github.com/google/styleguide/blob/gh-pages/eclipse-java-google-style.xml こちらからeclipse-java-google-style.xmlをダウンロードします。

この設定ファイルをダウンロードせずURLとして指定することも可能ですが、設定をプロジェクトごとにカスタマイズする場合はダウンロードしてローカルに保存したものを使用します。

VSCode上での設定

VSCode上の設定で、検索ボックスに java.formatまで入力すると該当の設定値に絞り込めます。

User設定ではなく、プロジェクトごとの設定という考えから、Workspaceの方のタブ設定します。

Java > Format > Settings: Profile でGoogleStyleを指定。

Java > Format > Settings: Url で eclipse-java-google-style.xml
(ローカルにファイルを置かずに、URLで指定する場合は、https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml というように指定します。)

動作確認

フォーマットが乱れたソースコードです。

メニューから、 View > Command Palette でコマンドパレットを表示して、fomatまで入力すると Format Documentを選ぶと手動でフォーマットを実行できます。

設定でEditor: Format On Saveをチェックすると、ファイルを保存したときに自動フォーマットするようにできます。原則的にははじめからこの設定をしておくのが良いです。

しかしながらすでにフォーマットが乱れたソースコードがある状態で、変更差分を発生させたくないケースでは、範囲指定して手動フォーマットしたほうがいいかもしれません。

VSCodeでCheckstyleによるJavaコーディングルールの適用

VSCodeでのCheckstyleは、Checkstyle for Java で導入できる。

ここでは以下の手順で設定を行う。

  1. Checstyle for Javaのインストール
  2. google_checks.xmlのダウンロードとカスタマイズ
  3. workspace設定を行う
  4. 動作確認

Checstyle for Javaのインストール

拡張機能でCheckstyleで検索して見つかる Checkstyle for Javaをインストール。

google_checks.xmlのダウンロードとカスタマイズ

ベースとなる設定テンプレートしてgoogle_checks.xmlをダウンロードして、プロジェクトのフォルダ直下に保存。

コーディングルールの設定はプロジェクトごとに変わるという考えで、プロジェクト別に設定を持つという想定。

これは個人的な好みですが、google_checks.xmlから、設定を少しカスタマイズしました(インデントの幅など)

@@ -251,12 +251,12 @@
              value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
     </module>
     <module name="Indentation">
-      <property name="basicOffset" value="2"/>
-      <property name="braceAdjustment" value="2"/>
-      <property name="caseIndent" value="2"/>
+      <property name="basicOffset" value="4"/>
+      <property name="braceAdjustment" value="0"/>
+      <property name="caseIndent" value="4"/>
       <property name="throwsIndent" value="4"/>
       <property name="lineWrappingIndentation" value="4"/>
-      <property name="arrayInitIndent" value="2"/>
+      <property name="arrayInitIndent" value="4"/>
     </module>
     <module name="AbbreviationAsWordInName">
       <property name="ignoreFinal" value="false"/>

workspace設定を行う

最後にCheckstyle for Javaがこの設定ファイルを使用するように設定します。

  • checkstyleで検索して、Java > Checkstyle: Configurationに、先程プロジェクト直下に配置したgoogle_checks.xmlを指定。
  • Java > Checkstyle: Versionを適当に最新10.3.1に。

Checkstyleバージョンは、メニューから View > Command Pallete… から、Checkstyle: Set the Checkstyle Versionから設定することもできます。

設定はワークスペースでの設定とします。このようにすることでプロジェクト単位での設定ということにできます。ユーザー単位の設定にするには設定タブでUserを選びます。

動作確認

このようにコーディングルールに沿ってない場合は警告がでます。

Checkstyleの警告は全部なくすべきですが、たまにどうしてもコードはそのまま維持したいときがあります。警告をそのまま残しておくと、「警告が残ってても仕方ない状態」になってしまい、必ずなくすべき警告と、ここでは警告があっても仕方ない警告の区別がつかず不健全になります。

なのでどうしても許容しなければならないときは、「ここは例外としてくれ」とCheckstyleに指定することで警告表示をなくすことができます。そうすると警告は常にゼロの状態を維持できるのでクリーンになります。

警告の例外は、どうしても仕方ないときだけに使ってください。なにも考えずに警告がでたら例外設定して消すわけではありません。

/**
 * クラスJavaDocコメント.
 */
public class CheckStyleDemo {
    /**
     * メソッドJavadocコメント.
     */
    public static void main(String[] args) {
        var Aaa = 1;    //CHECKSTYLE.SUPPRESS: LocalVariableNameCheck

        //CHECKSTYLE.OFF: LocalVariableNameCheck
        var Bbb = 2; 
        //CHECKSTYLE.ON: LocalVariableNameCheck
        var c = Aaa + Bbb;
        System.out.println(c);
    }
}

Checkstyleの例外設定は、いろんな方法があるのですが、ここではコメントを使ったもので2種類紹介です。

//CHECKSTYLE.SUPPRESS: XXX での設定は1行単位で設定し、XXXで指定した警告を除外しています。ここではLocalVariableNameCheckの警告です。警告名は、VSCodeでProblemsのパネルを見ればわかります。

//CHECKSTYLE.OFF//CHECKSTYLE.ON は、開始と終了の範囲を指定して抑制しているものです。OFFしたままONで戻すのを忘れると、それ以降チェックされていないことになるので注意。

CHECKSTYLE.SUPPRESSといったコメント上のキーワードは、google_checks.xmlにあって自由に変更できます。ちょっと長すぎるのでもっと短い名前にしても良いと思います。

Mapの初期化を1行で書く

JavaにはPythonやJavaScriptのようにリテラル {"aaa": 123}のようにでMapを生成する言語仕様はありません。
そのため、以前のJavaでは変数の宣言と同時に初期化するのが煩わしかったです。

現在の一番のオススメは、Map.ofです。

Map.ofで unmodifiable な Mapを生成 (Java9以降)

jshell> import java.util.Map;
jshell> var m1 = Map.of("aaa", 123, "bbb", 456);
m1 ==> {aaa=123, bbb=456}

# 変更可能なMapがほしい時
jshell> var m2 = new HashMap<String, Integer>(Map.of("aaa", 123, "bbb", 456));
m2 ==> {aaa=123, bbb=456}

1要素のみのunmodifiableなMap (Java8も可)

Map.ofはJava8では使えませんが、1要素のみのMapなら同じ機能を持つCollections.singletonMapがJava8でも使えます。

jshell> import java.util.Collections;
jshell> var m3 = Collections.singletonMap("abc", 123);
m3 ==> {abc=123}

StreamとCollectors.toMapを使って強引に (Java8も可)

もしくは強引に、StreamとCollectors.toMapを使えば、なんとか宣言と同時に初期化できます。

jshell> import java.util.Arrays;
jshell> import java.util.stream.Collectors;
jshell> import java.util.Map;
jshell> Map<String, Integer> m4 = Arrays.stream(new Object[][]{
   ...> {"aaa", 123},
   ...> {"bbb", 456},
   ...> {"ccc", 789},
   ...> }).collect(Collectors.toMap(objs->(String)objs[0], objs->(Integer)objs[1]));
m4 ==> {aaa=123, ccc=789, bbb=456}

上記のように無理やりJava8でやるぐらいならば、Map.of相当の関数を自作したほうがいいと思います。

/** 2要素のmap作成 */
public static <K, V> Map<K, V> myMapOf(K key1, V val1, K key2, V val2) {
    HashMap<K, V> m = new HashMap<K, V>();
    m.put(key1, val1);
    m.put(key2, val2);
    return Collections.unmodifiableMap(m);
}

現在時刻 ⇔ ( エポック秒 / UNIX時間 ) ⇔ DateTime

現在時刻を LocalDateTimeで取得

jshell> import java.time.*
jshell> LocalDateTime.now()
$1 ==> 2022-06-01T09:35:16.762675

現在時刻を エポック秒 / UNIX時間 で取得

jshell> import java.time.*
jshell> Instant.now().getEpochSecond() //秒
$1 ==> 1656290265
jshell> Instant.now().toEpochMilli()   //ミリ秒
$2 ==> 1656290309813

特定のZonedDateTimeからエポック秒 / UNIX時間 に変換

ZonedDateTimeはタイムゾーンをもつ日時情報。

jshell> import java.time.*
//適当なZonedDateTime
jshell> ZonedDateTime.of(2022, 4, 1, 0, 0, 0, 0, ZoneId.systemDefault())
$50 ==> 2022-04-01T00:00+09:00[Asia/Tokyo]
jshell> $50.toInstant().getEpochSecond()  //秒
$51 ==> 1648738800
jshell> $50.toInstant().toEpochMilli()    //ミリ秒
$52 ==> 1648738800000

特定のLocalDateTimeからエポック秒 / UNIX時間に変換

LocalDateTimeはタイムゾーンを持たない日時情報なので、UTC基準のUnix時間に変換するには、そのLocalDateTimeをどのタイムゾーンを持つものなのか決定する必要がある。

jshell> import java.time.*
//適当なLocalDateTime
jshell> LocalDateTime.of(2022, 4, 1, 0, 0)
$60 ==> 2022-04-01T00:00
jshell> $60.toEpochSecond(ZoneOffset.ofHours(9))  //JSTの+9時間
$61 ==> 1648738800

LocalDateTimeにZoneOffsetをつけるために、適当に+9時間としていますが、本当はもう少し慎重になるべきで面倒くさい。

Asia/Tokyoが+9時間がであるというのは、当然のようだが、いつでも普遍的な値ではなくて、あるときからサマータイムが導入されたりすると、その期間はサマータイムがあるから+9ではなくなります。

VSCodeでJUnitの実行

VSCodeでのJUnit用の機能は、Test Runtime for Javaの機能で使える。(Extension Pack for Javaに含まれる)

左側のサイドバーにテストエクスプローラーが表示され、全テストの一覧が確認できる。

たまにテストクラスを追加したはずなのに、一覧に表示されないことがあるが、そのときは上の方にある「テストの更新」を押すと改善される。

テストコード上からもテスト実行可能な単位で実行/デバッグのボタンが表示されるので、このコンテキストのデバッグを行うことができる。

VSCodeでmavenプロジェクトの作成

コマンドパレットから、Create Java Project→Maven→任意のArchetypeを選択→バージョン選択

GroupIdの入力(パッケージ名になる)

ArtifactIdの入力(プロジェクト名になる)

プロジェクトを配置するフォルダを選択

mavenのビルドが始まり、プロジェクトのバージョンがターミナル上で聞かれるので、Enterを押すなどして先に進める。

VSCodeでJava開発環境の作成

このブログでは基本的にJShellとVSCodeを使った開発記事を書いていきます。
Java開発においては、IntelliJ IDEAやEclipseを使った開発が多いと思います。IntelliJ IDEAは素晴らしい開発環境で無償で利用できるCommunity版であっても十分実用に足りると思います。

ただブログ主はVSCodeの軽さや、他のプログラミング言語でも同じように使える機能において、VSCodeが気に入っていて、Java開発においてもVSCodeが活発になってきてほしいと思っています。

VSCodeでのJava開発は、IntelliJと比べたらまだ機能面で劣っている面もあると思いますが、十分に実用的だと思います。Java開発環境をこれから整えようという方のためにVSCodeが選ばれていくことを目標にしています。

Extension Pack for Javaのインストール

Extension Pack for Javaは、Java開発の拡張機能の詰め合わせ。とりあえずこれを入れておけばOK。


インストールが完了するとGet Started with Java Developmentというページが開く

Java Runtimeのインストール

JDKのインストールを確認する。すでにインストール済みであればそれをデフォルトのJDKとして使用できるし、ここから推奨されるJDKをインストールすることもできる。

右側にあるcreate a new terminalのリンクをクリックしてterminalを実行し、java -versionのコマンドを実行できるか確認。

これでjava のバージョンが表示できればそれが使用できる。

もしJDKのインストールがされていない場合は、左側の「Install JDK」ボタンから任意のバージョンのJDKをダウンロードできる。

現在のJDKは、8, 11, 17のLTSから選ぶのが無難。Adoptium’s JDKというビルドから選択できるし、Amazon Correttoなども選択可能。

新規プロジェクトの作成

Open Command Paletteから、「Create Java Project」入力。して実行。

今回はNo build toolで特にビルドツールを使わないプロジェクトを作成。

プロジェクトを配置するフォルダを選択して、プロジェクト名を入力。プロジェクト名のフォルダが作られる。

デバッグ実行

少しコードを変更してデバッグ実行する。


画像のように、ステップ実行など基本的なJava開発の機能はVSCodeのExtention Pack for Javaで揃う。

Hello World

Java9からJShellが入っているのでJava9以上であれば、簡単にJavaコードの確認ができます。

$ jshell
|  Welcome to JShell -- Version 17.0.2
|  For an introduction type: /help intro

jshell> System.out.println("Hello World")
Hello World

jshell> 1 + 2
$2 ==> 3