torutkのブログ

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

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. 不要になったリソースは解放する