torutkのブログ

ソフトウェア・エンジニアのブログ

史跡巡り 桶狭間合戦場・大高城・鷲津砦・丸根砦

先日、遅めの夏休みを取って、名古屋方面に一人旅をしてきました。 織田信長の足跡をたどって、初日は桶狭間合戦場と、大高城、鷲津砦、丸根砦の跡を巡りました。

地図に、大高城と鷲津砦、丸根砦、そして桶狭間合戦場と伝えられる場所をマークしました。また当時は大高城付近まで海となっており、地図で平地(緑色)の大半は海と考えられます。 出展:国土地理院ウェブサイト デジタル標高地形図 濃尾平野周辺をもとに、城・砦跡、合戦場をマークして記載

大高城は織田方から今川方のものになっており、鷲津・丸根の砦は大高城に対抗するため織田方が築いた砦です。桶狭間合戦の日に鷲津砦・丸根砦は今川方の攻撃を受け落城、今川義元は本隊を率いて沓掛城から大高城へ入る途中、桶狭間付近で織田信長の奇襲を受けて受けて打ち取られ、今川方は撤退していきました。

桶狭間合戦場の場所は、実際には特定されておらず、上述地図に記載した場所のほかに、旧東海道沿いの場所があるようです。

桶狭間合戦場公園

  • 小さな公園となっていて、織田信長今川義元の像などがあります。最寄り駅は名鉄有松駅で、徒歩25分またはバス10分ほどになります。

鷲津砦

  • JR東海道線 大高駅のすぐ北側にあります。小山の途中が跡地となっていますが、特に遺構などはないようです。

丸根砦

  • 鷲津砦跡から徒歩10分弱、尾根伝いに歩くと至ります。

大高城

  • 鷲津砦跡から徒歩20分、砦と違ってそこそこの大きさです。
経路

今回は、名古屋駅から名鉄で有松駅に向かい、有松駅からバスで桶狭間寺前で下車、NPO法人が運営する桶狭間古戦場観光案内所に寄ってから桶狭間公園とそこから数十mほどにある今川義元本陣跡(住宅の前に看板だけある)、そこから有松駅方面に徒歩で向かい、旧東海道沿いの有松伝統的建造物群保存地区を見ながら有松駅に着きました。

有松伝統的建造物群保存地区

名鉄有松駅からバスでJR大高駅に向かい、大高駅のすぐそばにある鷲津砦跡から丸根砦跡を回り、そこから大高城へ歩いて向かいました。

Webまわりの技術知識の獲得のための書籍購入

Java読書会BOFで「基礎からのサーブレット/JSP 第5版」を読みすすめていると、Webアプリケーションの技術知識として増やしたいことがいくつかでてきました。

  • Webページの見栄えをよくするには、HTMLに加えてCSSCascading Style Sheets)が必要
  • Webページに動きを伴わせるには、JavaScriptが必要か、Javaだけでもできるかを知りたい
  • HTTP/2やHTTP/3について知識をえておきたい

そこで、2、3冊書籍を買って読み始めました。

まずはHTMLとCSSの本から選んだのが「1冊ですべて身につくHTML&CSSとWebデザイン入門講座」(SBクリエイティブ刊 2024年3月)

JavaScriptをブラウザ上でどのように使うのか、フロントエンド全般に書かれた「フロントエンドの知識地図 ~ 一冊でHTML/CSS/JavaScriptの開発技術が学べる本」(技術評論社刊 2023年11月)

HTTP全般について、HTTP/2、HTTP/3も解説のある「Real World HTTP 第3版」(オライリージャパン刊、2024年4月)

H2 databaseでのレコード登録時間

Java読書会の宿題より

Java読書会「基礎からのServlet/JSP 第5版」を読む会(第3回)の宿題で、int(32bit整数)を超える行のテーブルを作成して、一気にintを超える行の更新をした時の戻り値を調べてみよう、の取り組みです。

H2データベースへのデータ登録に要する時間

primary keyもunique属性もない単一カラムのテーブルを定義

CREATE TABLE bigdata (
    id int
);
単純 insert

32bit符号付整数値の最大値がおよそ21億なので、30億行のレコードを登録します。 まずは、単純に INSERT INTO bigdata(id) values (1); のようにデータをインサートするクエリを30億回実行します。

  • 方法1 自動コミットが有効(デフォルト)
  • 方法2 自動コミットを無効にし、100万行ごとにcommit

方法1は、100万行のinsertに20秒を要しました。30億行のinsertに推定17時間要する見積です。自動コミットが有効だと、insertを実行するごとにコミットが働くので遅いだろうと想定していました。

方法2で、自動コミットを無効として、一定量毎にcommitをしてみました。 ところが、方法2でも、100万行のinsertに20秒を要し、方法1とほとんど処理時間が変わりません。

バッチinsert

次は、バッチ登録を実施します。複数のinsert文をバッチ化してからバッチを実行します。100から1000個のisertをバッチにまとめます。

     PreparedStatement ps = conn.prepareStatement("insert into bigdata values(?)");
       :       
    for (int i = 0; i < 1000; i++) {
        ps.setInt(1, count);
            ps.addBatch();
    }
    ps.executeBatch();

バッチ登録の場合、自動コミットはオフとし、最後にcommitします。*1

さて、期待していた登録速度ですが、なんと方法3でも100万行のinsertに20秒を要し、方法1、2とほとんど同じでした。

ここまでの結果から、H2 databaseはSQLのリクエストのオーバーヘッド、自動コミットのオーバーヘッドがほとんどなく、クエリの処理数がほぼそのまま処理時間になっているものと推測します。

すると、クエリの処理数を減らして登録行数を増やすことができれば処理時間を短縮できるのではと考えます。

複数行を1つのinsertで

ここで、insert文は、複数行のデータを登録することができます。

insert into bigdata(id) values (1), (3), (5), (7), (11), (13);

このinsert文は、6行のレコードを登録します。

  • 方法4 自動コミットを無効にして、10行のデータを1つのinsert文で登録する

100万行の登録が2-3秒と短縮することができました。

  • 方法5 自動コミットを無効にして、100行のデータを1つのinsert文で登録する

100万行の登録が1秒とさらに短縮することができました。これであれば、30億行の登録に50分の見積もりです。

実行環境
実行結果

*1:30億行となると、さすがに小分けにcommitした方が良いかも

Java読書会「基礎からのサーブレット/JSP 第5版」を読む会(第3回)

Java読書会BOF 主催の「基礎からのサーブレット/JSP 第5版」を読む会(第3回)を4月27日(土)に開催しました。

今回は、読書会で読んだ範囲からいくつかトピックを記載します。

第3回を開催して、Chromeの振る舞いを知る

サーブレットが、シングルインスタンスでマルチスレッドでアクセスされる例において、

p.161

1つのブラウザ、たとえばChromeで同じサーブレットに対して2枚のウィンドウ(またはタブ)を開いた場合には、片方のウィンドウの処理が終了するまで、もう片方の処理は待機するので、カウンタの値が不適切になりません。

との記述がありました。試してみたところ、ウィンドウを2枚開いて、同じサーブレットにアクセスしてみましたが、ほぼ同時にアクセスでき、カウンタの値が不適切になりました。(ここで不適切とは、サーブレットクラスのフィールドにカウンタを保持し、アクセス時にフィールドの値を取得し数秒ウェイトしてから値をインクリメントしてフィールドに代入するコードのため、マルチスレッドでは複数が同じ値を取得することがあるという意味)

試したブラウザは、SafariFirefoxでしたが、Chromeを使ったところ不適切な更新は発生せず、2枚目のウィンドウがサーブレットにアクセスするのが遅い動きをしていました。 Edgeでも不適切な更新が発生せず、どうやらChromiumエンジンが関与しているようです。

サーブレットのURLに、リクエストパラメータを追加し、その値を2枚のウィンドウで変えてみたところ、不適切な更新がChromeでも発生しました。このことから、同一のURLに対するアクセスは、先にアクセスした方が完了するまで次のアクセスが待たされているようでした。

デベロッパーツールで調べてみると、2つ目のウィンドウからのアクセスは数秒間Stalledとなっていました。ぐぐってみたところ、Chromeは同一URLへのアクセスが並行して発生した場合、キャッシュコントロール上1つ目のリクエストが復帰するまで2つ目のリクエストの送出を待機する仕組みがあるとのことです。 なので、クエリパラメータを付加してその値をかえることでこの仕組みを回避することができていたのでした。

フィルターがCSSなどのファイルの読み込みを阻害

サーブレットフィルタの解説で、次のようにServletResponseのsetContentTypeを各サーブレットに記述するのは冗長なため、フィルタで記述する例がありました。

@WebFilter(urlPatterns={"/*"})
public class EncodingFilter implements Filter {
    public void doFilter(...) {
        :
        response.setContentType("text/html; charset=UTF-8");

このサンプル(フィルタ)を使用していた環境で、HTMLファイル(hello.html)にcssファイルを適用してみたところ、cssが反映されないという事態が生じました。

ブラウザのデベロッパーツールで調査したところ、次のエラーが発生していました。

[Error] Did not parse stylesheet at 'http://localhost:8080/book/css/book.css' because non CSS MIME types are not allowed in strict mode.

フィルタのURLパターンが /* のため、CSSファイルもフィルタのURLパターンにふくまれてしまい、cssファイルが本来 text/css というMIMEタイプで送信されるべきところ、text/htmlとなってしまったためです。

対応策としては、次が思い浮かびます。

  • サーブレットJSPのURLを、CSSや画像ファイルなどのリソースとは別な要素、例えば/servlet/xxx のように割り振り、フィルタのURLパターンを /servlet/* のように指定する
  • サーブレットのURLを、xxx.servletのように拡張子パターンが適用できるように命名し、フィルタのURLパターンを、.servlet.jsp のように指定する

URLパターンの指定では、ワイルドカードの使用は /パス要素/* のようにスラッシュで区切った後ろに指定することはできますが、/パス要素の途中* のように文字列の途中から後をワイルドカード指定することができません。

拡張子を指定する場合、パス要素を指定することができません。

リソースのクローズ

データベースへのアクセスで、サーブレットからJNDIでデータソースを取得し、JDBCでデータベースへアクセスします。

この際に、JNDIの InitalContext、JDBCのConnection、PreparedStatement、ResultSetを取得しています。これらは使用が終わったらcloseを読んでリソースを解放する必要があります。

書籍のサンプルでは、次のようなコードとなっています。

public void doGet(...) {
    :
    try {
        InitialContext ic = new InitialContext();
        DataSource ds = (DataSource) ic.lookup("java:/comp/env/jdbc/book");
        Connection con = ds.getConnection();
        PreparedStatement st = con.prepareStatement("select * from product");
        ResultSet rs =st.executeQuery();
        while (rs.next()) {
           :
        }
        st.close();
        con.close();
    } catch (Exception e) {
        e.printStackTrace(out);
    }
    :
}

このコーディングでの問題点は次です。

  • tryブロック内の処理途中で例外が発生した場合、たとえば executeQueryの実行中に例外が発生すると、その後のcloseを呼び出すことなく catch節に処理が移行してしまうため、リソースリークが生じる
  • ResultSet、InitialContextに対するcloseの呼び出しがない
    • JDBCAPI規定上は、Statementのcloseを呼び出すと、そのStatementが生成するResultSetもcloseされるとありますが、JDBCの実装が必ずそうなっているとは限らない
    • JNDIのInitialContextは、closeしない場合のリーク発生有無が明示されていませんが、必要と想定

是正案としては次があります。

  • tryブロックではなく、finallyブロックでcloseを呼び出す
  • try-with-resource構文を用いる
try-finallyで実装する案

従来のtry catchで確実にクローズするように finallyブロックでcloseを呼び出す

InitialContext ic = null;
Connection con = null;
Statement st = null;
ResultSet rs = null;
try {
   :
} catch (Exception e) {
   :
} finally {
    try {
        rs.close();
    } catch (Exception e) {
    }
    try {
        st.close();
    } catch (Exception e) {
    }
    try {
        con.close();
    } catch (Exception e) {
    }
    try {
        ic.close();
    } catch (Exception e) {
    }
}    

finallyブロックで、それぞれのリソースにたいしてcloseを呼びます。 それぞれのclose呼び出しをtry-catchで個別に囲っているのは次の理由です。

  • closeメソッドも例外をスローする可能性があるため、例外をスローしたとしても後続のcloseをきちっと呼び出すように制御フローを組んでいる
  • 元のtryブロックで例外が発生し、tryブロックから外へ例外をスローする場合、finallyブロックの中でcloseが例外をスローし、その例外をfinallyブロックから外に出すと、tryブロックの例外がfinallyブロックの例外に上書きされてしまうため

try-catch-finallyで、finallyブロックの中でクローズ対象のインスタンスを参照するには、インスタンス変数が try-catch-finallyの外側で宣言されている必要があります。 その場合、宣言時に実体が存在しないので初期値としてnullを入れています。finallyブロックでは、クローズ対象のインスタンスがnullである場合が想定されるので、上述のようにcloseをtryブロックで実行し、catchでNullPointerExceptionを含めて捕捉するか、次のように nullチェックをしてからcloseを呼び出します。

  • nullチェックをしてからcloseを呼び出し、closeのシグネチャで宣言される例外をcatchする
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
        }
    }
    :

close()を覆うtry文で、SQLExceptionをcatchしても、何かできることは特にないので、これをcatch (Exception e) とすれば、NullPointerExceptionも捕捉できるので、nullチェックをしなくてもよいかなと思います。  

try-witch-resourceで実装する案

try-with-resourceを使うと大分改善できます。

  • close呼び出しの記述が不要(finallyブロックの記述を省略)
  • 複数のリソースを使用しているときに、1つのcloseで例外が発生しても、残りのリソースに対してcloseが呼ばれる
  • tryブロックで発生した例外を tryブロックの外にスローする場合、closeで例外が発生しても上書きすることはない

ですが、クローズ対象のクラスがAutoCloseableをimplementsしている必要があります。JDBCの例では、JNDIのInitialContextがAutoCloseableでないのでやっかいです。

    InitialContext ic = null;
    DataSource ds = null;
    try {
       ic = new InitialContext();
       ds = (DataSource) ic.lookup("java:comp/env/jdbc/book");
    } catch (Exception e) {
        // エラー中断の処理
    }
    try (
        Connection con = ds.getConnection();
        PreparedStatement st = con.prepareStatement("select * from product");
        ResultSet rs = st.executeQuery();
    ) {
        // 
    } catch (Exception e) {
        // エラー中断の処理
    }

PreparedStatementにパラメータをセットする場合、try () の中に記述できないのでもう少し複雑なコードになります。

    try {
        InitialContext ic = new InitialContext();
        DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/book");
        try (
            Connection con = ds.getConnection();
            PreparedStatement st = con.prepareStatement("select ? from product")
        ) { 
            String column = request.getString("column");
            st.setString(1, column);
            try (ResultSet rs = con.executeQuery()) {
                while (rs.next()) {
                    :
                }
            }
    } catch (NamingException | SQLException e) {
        // エラー中断の処理
    }
参考

JPCERT CC FIO04-J. 不要になったリソースは解放する

Java読書会「基礎からのServlet/JSP第5版」を読む会(第1回)

新しい本「基礎からのServlet/JSP第5版」

今月のJava読書会BOF主催のJava読書会は、「基礎からのサーブレット/JSP 第5版」を読む会(第1回)を開催しました。

最新のServlet 6.0/JSP 3.1に対応し、Tomcat 10.1とOpenJDK 21で動かしながら学んでいく内容です。

概要

前回は洋書でしたが、今月からは和書です。が、参加人数が3名と少ない状況でした。(3連休の合間だったせいもあるか、宣伝不足か、読書会による勉強はニーズが少なくなっているのか、など憶測色々ありました)

この書籍では、メインはWindows上でOpenJDK 21とTomcat 10.1、H2 Database 2.1を使ってServletJSPのプログラミングを学んでいこうという内容です。

Tomcatは、最後に触ったのが20年前か或いはもっと前か、懐かしいというかほとんど覚えていないです。

今時点で書籍サポートサイトには正誤表は記載ありませんが、読書会をすると毎回誤植を見つけるよねと話して読み進めていたら、いくつか発見しました。

読書会の進め方は、例によって、本文、表、図、ソースコードを朗読して進めました。

昼食は、近くの生ハラミ焼肉屋さんで焼肉ランチを食べました。

TomcatとJetty

本書はTomcatを使っていますが、書籍では他のJakarta EEアプリケーションサーバとして、WildFly、Jetty、GlassfishWeblogic Serverが紹介されています。 JettyとTomcatの違いは?と議題になり、Tomcatはサーバープログラムをまずインストールして動かし、そこにサーブレットJSPを使うプログラムを展開(デプロイ)する使い方、JettyはサーブレットJSPを使うプログラムにWebサーバーが含まれる使い方となるということでした。

開発環境構築

書籍は、Windows環境での動かし方を記載し、付録でMacOSLinuxの環境に触れています。書籍は、サンプルプログラムと一緒の作業ディレクトリにOpenJDK、tomcat、H2 Databaseを展開したアーカイブファイルを作り、書籍サイトからダウンロードして展開してねという形です(200MB超過のアーカイブファイル)。

MacOSでの環境(書籍とは別)

MacOSでは、Homebrewにtomcat 10.1が用意されており、brew install tomcat で 10.1.19がインストールされました。OpenJDKもH2 DatabaseもHomebrewでインストール可能です。

Homebrewでインストールしたら、catalina runでサーバーが起動しました。簡単ですね。デプロイは、/opt/homebrew/opt/tomcat/libexec/webapps/ ディレクトリの下にアプリケーションのディレクトリ(本書ではbook)を作成、その下のWEB-INF/classesにパッケージに対応してディレクトリ・クラスファイルを配置します。

Tomcat周りのメモ
  • examplesアプリケーションは、tomcatをインストールしたマシンからのみアクセス可能。META-INFのcontext.xmlに、リモートアクセスの許可が 127...*、::1、0:0:0:0:0:0:0:1に限定されているため
  • ROOT にTomcatのようこそ画面を表示するアプリケーションがある
  • Java Platform Module Systemに対応しているのだろうか?

Java関連書籍「ソフトウェア設計のトレードオフと誤り」

正月休みの時にぶらっと本屋さんに寄った時に、Java読書会BOFの次の読書会の候補本になりそうかと買ってみました。

題名 ソフトウェア設計のトレードオフと誤り
著者 Tomasz Lelek, Jon Skeet
訳者 渋川よしき ら訳
出版 オライリー・ジャパン 2023年5月刊行
ISBN 978-4-8144-0031-7
価格 4180円(税込)

プログラム、アプリケーション、システムを作成するときに遭遇する設計上の選択を、トレードオフとして紹介しています。課題は、プログラミング・コード上のもの(例:シングルトン・パターンの実装方法、継承とコンポジションの使い分け、例外)から、共通ライブラリ、バージョニング、APIの柔軟性と複雑性、最適化、日付と時間、データのローカリティ、メモリかディスクか、サードパーティライブラリの利用、分散システムの一貫性、アトミック、配信、DIフレームワーク、リアクティブプログラミング、関数プログラミングの使いどころなどが並んでいます。 コードはJavaで書かれています。

読み始めたばかりですが、最初の方に出てくるシングルトン・パターンでは、同期をgetInstanceでかける方法、ダブルチェックロッキング、スレッドローカル変数での実現(厳密にはシングルトンではなくなりますが)を列挙し、パフォーマンスを含めてトレードオフしています。

日付と時間についてかなり深い記述がありますが、著者の一人JonがNoda Time(.NET用の日時ライブラリ)の開発者なのでうなづけます。 なお、Noda Timeのライブラリ名から推測できる通り、JavaのJoda Timeライブラリの.NETポーティングです。Joda Timeは、javaのdate & time APIの元になったライブラリです。

IntelliJ IDEA 2023.3 で Java 21 Preview機能を使う

IntelliJ IDEA 2023.3 と Java 21

先日リリースされた、IntelliJ IDEA 2023.3では、Java 21対応が完全サポートと謳われています。

9月に書いたブログ(下記)では、IntelliJ IDEA 2023.2.1でOpenJDK 21のPreview機能であるJEP 445 Unnamed Classes and Instance Main Methods の構文を認識できなかったと書きました。その後も2023.2.5までは未対応でした。

今回、2023.3になってどうなったかを見てみます。

torutk.hatenablog.jp

IntelliJ IDEA 2023.3 で JEP 445

New Projectを作成

IntelliJ IDEAの[File]メニュー > [New] > [Project...] を選択し、「New Project」ダイアログを表示します(下図)。

  • ① プロジェクト名を入力、この名前がディレクトリ名、生成されるJARファイルの基底名などに使われます。
  • ② プロジェクトディレクトリを作成する親ディレクトリを指定します。
  • ③ このプロジェクトで開発対象とするプログラミング言語を指定します。
  • ④ このプロジェクトのビルド・実行・デバッグ・配布などの活動に使うビルドツールを指定します。
  • ⑤ このプロジェクトのビルド・実行に使うJDKを指定します。
  • ⑥ ビルドツールに④でGradleを指定したとき、Gradleのビルド定義ファイルの記述に使うDSLの種類を指定します。
  • ⑦ 追加設定をするために、Advanced Settingsの先頭の[>]をクリックし追加設定を表示します。
  • ⑧ このプロジェクトが使うビルドツールGradleの共有方法を指定します。通常はWrapperを使用します。
  • ⑨ このプロジェクトで生成するアーティファクト(成果物)のGroupIdを指定します。成果物のオーナーを識別する目的で使われます。Javaの場合、パッケージ名に使用する組織(オーナー)のドメイン名に基づく部分を抽出して使用することが多いです。その下のArtifactIdは、通常①で指定したプロジェクト名と同じものが入っていますのでそのまま使います。

プロジェクトが生成されました。build.gradle.ktsが表示されます。

mainメソッドを記述

JEP 445では、無名パッケージのjavaソースファイルに、クラス定義なしに直接 メソッドなどを記述できます。 そこで、ソースディレクトリ( src/main/java/)の下に新規Javaソースファイルを作成します。

  • 左側ペインの src > main > java を選択し右クリックで、New > Java Class を選択して「New Java Class」ダイアログを表示
  • 名前にHelloとつける

では、Hello.java ファイルにmainメソッドを記述します。

エラーが出ています。OpenJDK 21では、JEP 445 は Preview機能なのでビルドするにはコンパイルオプションでPreview機能を有効にする必要があります。 IntelliJ IDEAでは、[File]メニュー > [Project Structure]で「Project Structure」ダイアログを表示し、Language level欄で[21 (Preview) ...]を設定します。

実行(IntelliJ IEA上から)

mainメソッドの宣言行にある緑枠の右三角アイコンをクリックすると、mainメソッドを実行します。

しかし、エラーとなってしまいました。先程の、Lanugage levelの設定とは別に、Gradleの定義でPreviewを有効にする必要がありそうです。

  • 試行1 Settings > Build, Execution, Deployment > Compiler > Java Compiler を開き、[Additional command line parameters]に、--enable-preview を設定した。結果、エラーが出るのは変わらず。

  • 試行2 Gradle Documentに記載のタスク定義を追記

次のドキュメントの Enabling Java preview features の記載の定義を、build.gradle.ktsに追記します。

Building Java & JVM projects

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("--enable-preview")
}

tasks.withType<Test>().configureEach {
    jvmArgs("--enable-preview")
}

tasks.withType<JavaExec>().configureEach {
    jvmArgs("--enable-preview")
}

この定義を追加することにより、コンパイル・実行が可能となりました。 IntelliJ IDEA上から実行するときは、Runメニュー、またはmainメソッドの宣言行の三角アイコンから可能です。

実行(コマンドライン

コマンドラインから実行する場合、この時点の記述ではGradleのタスクで実行は用意されていないので、javaコマンドにパスを指定して実行する必要があります。

hello % ./gradlew build    

> Task :compileJava
  :
BUILD SUCCESSFUL in 927ms
2 actionable tasks: 2 executed
hello % ls build/libs 
hello-1.0-SNAPSHOT.jar
hello % java --enable-preview -cp build/libs/hello-1.0-SNAPSHOT.jar Hello
Hello, Java 21 Preview world.
hello % 

まとめ

IntelliJ IDEAでGradleをビルドツールに使うプロジェクトで OpenJDK のプレビュー機能を使ってプログラミングする場合、

  • Project Structureで、Language level に Previewを有効にするバージョンを指定する
  • Gradleのビルド定義(build.gradle.kts)に、コンパイル・実行時に javaVMオプション --enable-previewを指定する記述を追記する

を行います。