プロジェクト

全般

プロフィール

Jakarta EE Servlet and JSP

はじめに

Java読書会BOF主催の 「基礎からのサーブレット/JSP 第5版」 を読む会の勉強メモとして記載します。
Servlet 6.0, JSP 3.1仕様を、Tomcat 10で動かします。

開発環境

書籍のサンプルは、ユーザーディレクトリ下にソースコードとTomcatを展開し、ソースコードはTomcatのwebapps/book/WEB-INF/src下に作成し、コンパイル結果のクラスファイルを同 WEB-INF/classes 下に生成する構成となっています。書籍は初心者向けをターゲットとしているので、環境構築でつまずくことのないようにとの配慮で構成されています。

この記事では、一般的な開発・実行環境を把握しておきたいので、Webアプリケーションをwarファイルとして生成し、webappsの下に配備する一般的な構成とします。アプリケーションのビルトには、Gradleを使用します。IDEはIntelliJを使いますが、Gradleに対応できる他の開発環境でも可能です。

Windows 11

  • OpenJDK 21
  • Gradle 8.7
  • Tomcat 10.1.20
  • H2 database 2.2.224
  • IntelliJ IDEA 2023.3.6

macOS 13.4

  • OpenJDK 21
  • Gradle 8.7
  • Tomcat 10.1.20
  • H2 database 2.2.224
  • IntelliJ IDEA 2024.1

OpenJDK、Gradle、Tomcat、H2は、Homebrewでインストールしています。
IntelliJ IDEAは、アプリケーションのインストーラでインストール(と記憶)しています。

Homebrewでインストールしたtomcatは、webappsが /opt/homebrew/opt/tomcat/libexec/webapps に存在します。

知識編

サーブレット、JSPおよびWebアプリケーションに関する知識の記録です。

サーブレットとJSPの違い

 Java Servlet     Java Server Page
+------------+   +-------------+
| Java code  |   | HTML code   |
| +--------+ |   | +---------+ |
| | HTML   | |   | |Java code| |
| +--------+ |   | +---------+ |
| +--------+ |   | +---------+ |
| | HTML   | |   | |Java code| |
| +--------+ |   | +---------+ |
| +--------+ |   | +---------+ |
| | HTML   | |   | |Java code| |
| +--------+ |   | +---------+ |
+------------+   +-------------+

ServletはJavaのコードの中にHTMLのコードが埋め込まれている(print文の文字列がHTMLコード)のに対して、JSPはHTMLコードの中にJavaのコードが埋め込まれているという形態です。

サーブレット

サーブレットクラスの実装

  • java.servlet.http.HttpServletを継承する
  • doGetやdoPostメソッドを実装して、HTTPのリクエスト・レスポンス処理を記述する
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class Reserve extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        : HTTP GETメソッドによるリクエストを受けてレスポンスを返す
    }
}
  • 引数 HttpServletRequest には、リクエストパラメータ、リクエスト属性、セッション、クッキーなどが格納
  • レスポンスは、HTML文書(テキスト)を返すのが基本
  • レスポンスで返すテキストは、引数 HttpServletResponse オブジェクトからgetWriter()で取得したPrintWriterを使って出力
  • HttpServletResponseにクッキーの設定が可能
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8"); // (1)
        var out = resp.getWriter();
        :
    }
  • レスポンスで返すHTMLテキストのMIMEタイプ設定をする。一般的にはHTMLがUTF-8文字符号化されるので "text/html; charset=UTF-8"を設定
    この設定は、getWriterの呼び出し前に行う
protected修飾子

HttpServletのdoGet/doPostメソッドは、protected修飾子で定義されています。書籍ではサーブレットアプリケーションのdoGet/doPostメソッドはpublicとして記述されています。
doGet/doPostメソッドは、serviceメソッドから呼び出され、外部のクラスからの呼び出しはないので、本記事ではprotecedのままとしています。

リクエストパラメータ

HTTPのGETまたはPOSTメソッドで渡されるパラメータを取得します。

次はPOSTメソッドでの取得のコード断片ですが、GETメソッドでも同様です。

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("UTF-8");                   // (1)
    String count = req.getParameter("count");            // (2)
    String[] values = req.getParameterValues("option");  // (3)

  • (1) リクエストパラメータのテキストのエンコーディングを指定
  • (2) リクエストパラメータの名前を指定して値を取得(同じ名前でリクエストが1つの場合)
  • (3) リクエストパラメータの名前を指定して複数の値を取得(同じ名前でリクエストが複数の場合、チェックボックス等)

リクエストパラメータの名前一覧を取得するAPIもあります。

  • Enumeration<String> getParameterNames()

HTMLファイル

HTMLの規格と書き方

HTMLの規格は現在 WHATWGが策定する HTML Living Standardとして標準化されています。

HTML記述例 説明
<!DOCTYPE html> DOCTYPE宣言で、htmlと指定します
<html> トップ要素は html です。lang属性でHTMLファイルを記述する言語(日本語、英語、など)を指定するとブラウザの自動判別に頼らず意図する表示になります。
<head> html要素の子要素で、HTML文書のメタデータを記述します。題名、スクリプト、スタイルシート、文字コードなど
<meta charset="UTF-8"> head要素の子要素で、HTMLファイルの文字コードを指定します。
<title> head要素の子要素でHTMLの題名を指定します。ブラウザで表示するときにはタブの名称、ブックマーク名などに使用されます。
<body> html要素の子要素で、HTMLコンテンツを記述する要素です。

HTMLのフォームとサーブレット呼び出し

サーブレットの典型的な使い方の一つに、HTMLのフォームで入力した情報をサーブレットが処理するというものがあります。

<form action="reserve" method="post">
    <p>レストランをご予約ください</p>
    <p>人数
        <select name="count">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
        </select>
    </p>

    <p>座席
        <input type="radio" name="seat" value="禁煙" checked>禁煙
        <input type="radio" name="seat" value="喫煙">喫煙
    </p>

    <p>オプション
        <input type="checkbox" name="option" value="ケーキ">ケーキ
        <input type="checkbox" name="option" value="花束">花束
    </p>

    <p><input type="submit" value="予約"></p>
</form>
  • form要素のaction属性で、フォームに入力した情報をリクエストするURL(相対URL)とHTTPメソッド(GETまたはPUT)を指定
    • /chapter5/greeting.html のフォームでaction属性にgreetingを指定すると、URLパターン /chapter5/greeting にマップされたサーブレットが受ける
  • input要素は、type属性で指定した形式のユーザー入力をHTML画面上に設定
    • radioはラジオボタンで、name属性の値が同じラジオボタンが一つのグループとなりグループで1つだけがチェック可能、またname属性の値がリクエストパラメータの名前となり、value属性の値がリクエストパラメータの値となる
    • checkboxはチェックボックスで、name属性の値がリクエストパラメータの名前、valueがリクエストパラメータの値となる。複数のチェックボックスが選択されたとき、それぞれチェックボックスが同じname属性の値を持つと、同じリクエストパラメータの名前で複数のリクエストが送信される
    • submitは、入力したデータをまとめてサーバーに送信するボタンを生成する。
label要素の記述とinput要素との紐付け

HTMLの記述では、コントロール部品(input要素)に対してキャプション(名称など)をlabel要素で定義して紐付けすることが推奨されています。
これにより、コントロール部分だけでなく、label要素で記述した文字列もコントロール部分と同じくマウスクリック等に反応します。また、アクセシビリティの向上(音声リーダー等がコントロール部品と関係づけ)が可能となります。

例1)label要素の属性forとinput要素の属性idを記述して紐付け

<label for="user">お名前を入力してください。</label>
<form action="greeting" method="get">
    <input type="text" name="user" id="user">
    <input type="submit" value="確定">
</form>

例2)label要素の子要素にinput要素を記述、属性の記述による紐付けは不要

    <label>お名前を入力してください。<br>
        <input type="text" name="user">
    </label>

コンテキストパス、URLパターン

WebアプリケーションへアクセスするURLは、
http://localhost:8080/book/hello
                     ^^^^^
  • /book がコンテキストパスでサーバー内でWebアプリケーションを識別します。
  • /hello はサーブレットの指定で、アノテーションまたはweb.xmlでURLパターンとして指定し特定のサーブレットと紐づけます。
http://localhost:8080 /book /hello
サーバーURL コンテキストパス リソースURL

アノテーションか設定ファイルか

楽なのはアノテーションですね。1行の記述で済みます。一方、web.xmlで記述する場合、FQCNクラスメイトの対応付け、URLパターンとの対応付けをそれぞれXMLのタグ付き記述しなくてはならないです。

一方、誰が作ったかソースコードをすぐに参照できないようなWebアプリケーションがあるとアノテーションで定義したURLパターンを確認するのが大変ということがあるかもしれません。

どちらが良いかは、トレードオフになります。

URLパターン

URLパターンの指定では、次の指定方法があります。

  • フルパスを指定する
  • ワイルドカードで拡張子パターンを指定する
  • ワイルドカードでプレフィックスを指定する

ワイルドカードの指定はあまり柔軟ではなく、パターン中において1つしか指定できず、指定場所も中間には置けず、前、または後のどちらかです。

拡張子パターンは、*.jsp のように、ワイルドカードで拡張子を指定する方法です。この場合、アプリケーションのすべてに対して拡張子がマッチします。ワイルドカードの前に文字は指定できません。

プレフィックス指定は、/myapp/* のように、/から始まるコンテキストパスを記述し、途中の/以降をワイルドカードで指定します。
パスの要素の文字列の一部をワイルドカード指定はできません。(/my* とは指定できない)

JSP

HTML文書の中にJavaコードを埋め込んだファイルです。実行時にJSPファイルからサーブレットのコードを生成しコンパイルされ、サーブレットとして実行されます。
JSPは、プログラマーではないユーザーインタフェースデザイナーがHTMLをベースに画面デザインを実施できることをねらいとしています。

シンプルなJSP

最低限のJSP
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>Title</title>
</head>
<body>

<%-- メッセージの出力 --%>
<p>Hello!</p>
<p>こんにちは</p>

</body>
</html>
  • <% ~ %> がJSPのディレクティブで、JSP特有の機能を記述
  • <%-- ~ --%> は、JSPのコメントでHTMLには出力されません。HTMLに出力したいコメントは、HTMLのコメント を使います
  • @pageディレクティブは、JSPページ全体の設定(MIMEタイプなど)
共通部分を別ファイルに記述

@includeディレクティブで共通のHTML記述を別ファイルから取り込みます。
Javaのコード(ステートメント)を、スクリプトレット <% ~ %> に記載しています。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="../header.html" %>

<p>Hello!</p>
<p>こんにちは!</p>

<p><% out.println(java.time.ZonedDateTime.now()); %></p>

<%@ include file="../footer.html" %>
  • header.html
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Servlet/JSP Samples</title>
    </head>
    <body>
    
  • footer.html
    </body>
    </html>
    

次のスクリプトレットにはJavaのコードを直接記述できます。

<% out.println(java.time.ZonedDateTime.now()); %>

outは、暗黙に定義されたオブジェクトで、レスポンスのテキスト出力に使います。

スクリプトレットとは別に、Javaの式を記述すると評価結果が展開される式も記述できます。

<%= java.time.ZonedDateTime.now() %>

上述のスクリプトレットおよび式では、ZonedDateTimeクラスを利用するためにFQCN(パッケージ名付きのクラス名)を記述しましたが、Javaプログラムと同様にimportで使用するクラスを宣言することもできます。importは、ページディレクティブ <%@ ~ %>で記述します。

<%@ page import="java.time.ZonedDateTime" %>
  :
<p><%= ZonedDateTime.now() %></p>

EL式

タグライブラリ

JSTL標準タグライブラリを使用するときは、実行時にライブラリのJARファイルを配備する必要があります。
アプリケーションと一緒にwarファイルに含める場合、次のようにGradleビルド定義に記述します。

    runtimeOnly("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0")
    runtimeOnly("org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1")
}

JSPは、サーバーに配備後に最初にアクセスされるときにサーバー上でサーブレットに変換されるので、サーバーにJSTLのAPIと実装のJARファイルを配備します。

エラーページ

JSPで例外が発生した場合に表示するページで、JSPの処理中に例外が発生すると、errorPageで指定したリンクに飛びます。
<%@page errorPage="total-error.jsp" %>

フィルタ

それぞれのサーブレットやJSPに同じコードを繰り返し記述する代わりに、フィルターを定義して複数のサーブレットやJSPに適用することができます。
例えば、サーブレットのリクエスト(doGetやdoPost)の際に決まった処理を実行するなどです。

jakarta.servlet.Filterクラスを継承してフィルタを定義します。フィルタの適用対象は、フィルタクラスのアノテーション @WebFilter で指定するか、web.xmlに指定します。複数のフィルタを指定する場合にフィルタの実行順序を指定したい場合は、web.xmlの記述が必要です。

データベース

Webアプリケーションからデータベースを利用します。
SQLでのアクセスは、JDBC APIを使用します。JDBCでは、データソースに定義されたデータベースへ接続し、SQLを介してデータベースとやり取りをします。

  • データソースの設定
  • JDBCドライバの配置
  • JDBC接続
  • JDBCを使ったデータベース操作

データソースの設定

Tomcatの場合、アプリケーションのMETA-INF/context.xmlにデータソースの定義を記述します。

<?xml version="1.0" encoding="UTF-8" ?>
<Context>
    <Resource
        name="jdbc/book" 
        auth="Container" 
        type="javax.sql.DataSource" 
        driverClassName="org.h2.Driver" 
        url="jdbc:h2:tcp://localhost/~/work/h2/book" 
        username="sa" 
        password="password" 
    />
</Context>
  • nameはサーブレット/JSPからデータソースを取得する際に使用
  • authは認証を誰が行うかを指定、ContainerはWebコンテナ(Tomcat)が実施、Applicationはアプリケーションが実施
  • typeは、JDBCの場合、javax.sql.DataSourceを指定
  • driverClassNameは接続に使用するJDBCドライバのクラス名を指定
  • urlは接続文字列
  • usernameとpasswordは認証に使用するユーザー名とパスワード

JDBCドライバの配置

JDBCドライバは、アプリケーションサーバー(Webコンテナ)のライブラリ配置場所にあらかじめ配置しておくか、Webアプリケーションと一緒に配布するかの方法があります。

アプリケーションサーバーに配置する場合は、Tomcatの場合は、$CATALINA_HOME/libの下に置くことになります。

TomcatのJDBC Connection poolを使用する場合、connection poolのライブラリ(jar)と同じクラスローダーでJDBCドライバーをロードする必要があります。その場合、JDBCドライバは$CATALINA_HOME/libに配置します。

SQL

リレーショナルデータベースに対する問い合わせ言語で、ISOで標準化されています。

  • ISO/IEC 9075

ISOの規格は要購入、ISOの規格をJISが別途規格化(日本語)しています。こちらは閲覧だけならインターネット上で可能です。

  • JIS X 3005
既定義型のデータ型

SQLの規格(JIS)を流し読みしたメモ

  • データ列型(string type)
    • 文字列型(character string type)
      CHARACTER4, CHARACTER VARYING5, CHARACTER LARGE OBJECT16
    • 2進オクテット列型(binary string type)
      BINARY, BINARY VARYING7, BINARY LARGE OBJECT18
  • 数型(numeric type)
    • 真数型(exact numeric type)
      NUMERIC, DECIMAL3, SMALLINT, INTEGER2, BIGINT
    • 概数型(approximate numeric type)
      FLOAT, REAL, DOUBLE PRECISION
  • 時刻印型(timestamp type)
    TIMESTAMP WITHOUT TIME ZONE, TIMESTAMP WITH TIMEZONE
  • 日時型(datetime type)
    DATE, TIME, TIMESTAMP
  • 時間間隔型(interval type)
    INTERVAL

1 長大オブジェクトデータ列型(large object string type)としても括られる

2 SQL言語上で、INTとして指定可能(エイリアス的な?、以下同様)

3 SQL言語上で、DECとして指定可能

4 SQL言語上で、CHARとして指定可能

5 SQL言語上で、CHAR VARYING または VARCHAR として指定可能

6 SQL言語上で、CHAR LARGE OBJECT または CLOB として指定可能

7 SQL言語上で、VARBINARYとして指定可能

8 SQL言語上で、BLOBとして指定可能

INTEGERのサイズ

SQLと各プログラミング言語とのマッピングの規定を見ると、およそ32bit整数型に対応付けられているので、4バイト整数として扱われます。

検索結果の件数を取得

検索結果の件数を取得しようとした場合、書籍のサンプルコードでは、ResultSetをnext()で回し切ってカウンターをインクリメントして件数を取得していました。他に方法がないかを調べてみたところ、次となりました。

  • SQLのクエリーで select count(*) を使う
  • スクロール可能なResultSetであれば、rs.last() を実行してカーソルを最終行に移動し、rs.getRow() で行番号を取得する。
    ただし、lastをサポートしていないドライバ、Statementの生成時にResultSet.TYPE_FORWARD_ONLYが指定されている場合などは利用できない

実践編

実際にサーブレット、JSPを動かしてみます。書籍の内容をもとに、HTMLファイルやサーブレットのJavaクラスをwarファイルに生成して tomcat に配備してブラウザからアクセスします。

tomcat の実行

Windows

パッケージ管理 scoop で、tomcatとoraclejdkをインストールします。scoopでインストールすると、コマンドへのPATHが設定されます。

コマンドプロンプトを開き、文字コードをUTF-8に設定します。

D:\work\book> chcp 65001

tomcatを実行します。

D:\work\book> catalina run
  :

macOS

パッケージ管理 Homebrew で、tomcatとopenjdkをインストールします。

tomcatのコマンドライン環境での起動は

% /opt/homebrew/opt/tomcat/bin/catalina run

HTMLとサーブレットのプロジェクト

Gradleプロジェクトの作成

D:\work> mkdir book
D:\work> cd book
D:\work\book>

D:\work\book\build.gradle.kts ファイルを作成します。
warプラグインを指定します。
サーブレットAPIのライブラリと、アノテーションAPIのライブラリを依存関係に定義し、リポジトリをmaven centralに指定します。

plugins {
    war
}

repositories {
    mavenCentral()
}

dependencies {
    providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
    providedCompile("jakarta.annotation:jakarta.annotation-api:2.1.1")
}

warプラグインが使用するデフォルトのソースファイル等を収容するディレクトリを作成します。

D:\work\book> mkdir src\main\java
D:\work\book> mkdir src\main\resources
D:\work\book> mkdir src\main\webapp
  • src\main\javaディレクトリ以下のJavaソースファイルはコンパイルされてclassファイルがwarファイルのWEB-INF/classes 下にパッケージ階層に応じて配置されます。
  • src\main\resourcesディレクトリ以下のファイルは、そのまま warファイルのWEB-INF/classes下にディレクトリ階層に応じて配置されます。
  • src\main\webappディレクトリに置いたファイルは、そのまま warファイルのルートディレクトリ以下に配置されます。HTMLファイルやJSPファイルはこの配下に作成します。
  • 注)WEB-INF/web.xml として配置したい場合は、src\main\webapp\WEB-INF\web.xml に作成する。src\maini\resources\web.xmlに作成すると、warファイルのWEB-INF/classes/web.xml に配置されてしまう。

HTMLファイルの生成

src\main\webapp\index.html を作成します。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Servlet/JSP book</title>
</head>
<body>
Welcome!
</body>
</html>

warの生成と配備

HTMLファイルを1つ作成した段階でwarを生成し、tomcatに配備します。

  • Gradleでwarファイルを生成
    D:\work\book> gradlew war
      :
    D:\work\book> dir build\libs
    2024/04/04  20:35               492 book.war
    
  • warファイルをtomcatに配備
    D:\work\book> copy build\libs\book.war %USERPROFILE%\scoop\persist\tomcat\webapp
    

ブラウザでbookアプリケーションのHTMLを表示

http://localhost:8080/book/

にWebブラウザでアクセスすると、Welcome! と表示されます。

サーブレットプログラムの作成(annotation使用)

annotation で URLパターンを指定するサーブレット。
src\main\java\com\torutk\hello\HelloAlfa.java

package com.torutk.hello;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.ZonedDateTime;

@WebServlet(urlPatterns = {"/hello/alfa"})
public class HelloAlfa extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println(""" 
                <!DOCTYPE html>
                <html>
                <head>
                <meta charset="UTF-8">
                <title>Servlet/JSP Sample</title>
                </head>
                <body>
                <p>Hello!</p>
                <p>こんにちは!</p>
                <p>%s</p>
                </body>
                </html>
                """.formatted(ZonedDateTime.now()));
    }
}

サーブレットの実装は、HttpServletを継承します。

public class HelloAlfa extends HttpServlet {

このサーブレットは、URLパターン /hello/alfa でアクセスします。

@WebServlet(urlPatterns = {"/hello/alfa"})

HTTPのGETメソッドでアクセスするときは、HttpServletのdoGetメソッドをオーバーライドして処理を記述します。

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

引数には、リクエストに関する情報を保持するHttpServletRequestと、レスポンス(応答)を保持する(書き込む)HttpServletResponseを取ります。

レスポンス(response)に際して使用するMIMEタイプと文字コードを最初に(getWriter呼び出しより前に)指定します。

response.setContentType("text/html; charset=UTF-8");

応答メッセージを出力するwriterをresponse引数から取得します。

PrintWriter out = response.getWriter();

writerに、動的にHTMLを生成して文字列として渡します(println)。

        out.println(""" 
                <!DOCTYPE html>
                <html>
                <head>
                <meta charset="UTF-8">
                <title>Servlet/JSP Sample</title>
                </head>
                <body>
                <p>Hello!</p>
                <p>こんにちは!</p>
                <p>%s</p>
                </body>
                </html>
                """.formatted(ZonedDateTime.now()));

  • JDK 15で正式搭載されたテキストブロックで記述すると、Javaコード中にHTML文書を複数行まとめて書くことができます。
  • HTML文書の中で動的に決定する文字列(上述例では実行時の日時)は、%sプレースホルダーを使用して、formattedメソッドで指定します。
  • 実行時の日時は、Java Date Time APIのZonedDateTime(タイムゾーンを持つ日時クラス)で取得します。

warの再生成とtomcatへの配備

先ほどと同じくgradleでwarタスクを実行し、tomcat にwarファイルをコピーします。自動で展開・再ロードされますので、tomcatの再起動は不要です。

ブラウザでbookアプリケーションのサーブレットを表示

http://localhost:8080/book/hello/alfa

にWebブラウザでアクセスすると、サーブレットの実行結果が表示されます。

サーブレットプログラムの作成(web.xml使用)

web.xml で URLパターンを指定するサーブレット。
src\main\java\com\torutk\hello\HelloXray.java

HelloAlfa.javaとHelloXray.javaの差分を次に示します。

--- src/main/java/com/torutk/book/HelloAlfa.java    
+++ src/main/java/com/torutk/book/HelloXray.java    
@@ -1,7 +1,6 @@
 package com.torutk.book;

 import jakarta.servlet.ServletException;
-import jakarta.servlet.annotation.WebServlet;
 import jakarta.servlet.http.HttpServlet;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -9,8 +8,7 @@
 import java.io.IOException;
 import java.time.ZonedDateTime;

-@WebServlet(urlPatterns = {"/hello/alfa"})
-public class HelloAlfa extends HttpServlet {
+public class HelloXray extends HttpServlet {
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         resp.setContentType("text/html; charset=UTF-8");

差分は次の2つです。

  • アノテーションでURLパターンを指定しないので削除
  • クラス名を変更

web.xmlにURLパターンを定義

  • src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
         https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" 
         version="6.0">
<servlet>
    <servlet-name>helloXray</servlet-name>
    <servlet-class>com.torutk.book.HelloXray</servlet-class>
</servlet>
 <servlet-mapping>
    <servlet-name>helloXray</servlet-name>
    <url-pattern>/hello/xray</url-pattern>
</servlet-mapping>
</web-app>
  • web-appタグは、お決まりの記述となります。
  • servletタグおよびservlet-mappingタグで、サーブレットのクラス名、URLパターンを定義します。
  • servlet-nameは、web.xmlファイル中で一意の名前であれば任意に記述可能です。通常はサーブレットのクラス名(パッケージ名なしの)にすると混乱ないでしょう。
  • web.xmlファイル中で、helloXrayの名前で指定するサーブレットは、FQCN(完全修飾クラス名)が com.torutk.book.HelloXrayである
    <servlet>
        <servlet-name>helloXray</servlet-name>
        <servlet-class>com.torutk.book.HelloXray</servlet-class>
    </servlet>
    
  • web.xmlファイル中でhelloXrayの名前で指定するサーブレットは、URLパターンが /hello/xray である
     <servlet-mapping>
        <servlet-name>helloXray</servlet-name>
        <url-pattern>/hello/xray</url-pattern>
    </servlet-mapping>
    

warファイルの生成

先ほどと同じくgradleでwarタスクを実行し、tomcat にwarファイルをコピーします。自動で展開・再ロードされますので、tomcatの再起動は不要です。
ここまでのプロジェクトで生成したbook.warの内容は次となります。

+-- META-INF/
|     +-- MANIFEST.MF
+-- WEB-INF/
|     +-- classes/
|     |     +-- com/
|     |           +-- torutk/
|     |                 +-- book/
|     |                       +-- HelloAlfa.class
|     |                       +-- HelloXray.class
|     +-- web.xml
+-- index.html

ブラウザでbookアプリケーションのサーブレットを表示

http://localhost:8080/book/hello/xray

にWebブラウザでアクセスすると、サーブレットの実行結果が表示されます。

JDBCドライバの配置

アプリケーションのwarファイルに、使用するJDBCドライバーを含めます。
WEB-INF/lib/h2-2.2.224.jar

Gradleの場合、warプラグインを使用し、依存性の定義に runtimeOnly でH2を記述すると WEB-INF/lib/の下にjarファイルが配置されます。

  • build.gradle.kts
    plugins {
        war
    }
        :
    dependencies {
        :
        runtimeOnly("com.h2database:h2:2.2.224")
    }
    

課題

フィルター指定で意図しないCSSなどを含んでしまう

ServletResponseのsetContentTypeをフィルタに定義

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

書籍の例のように、各サーブレットでresponseのsetContentTypeをtext/htmlに指定するコードをフィルタに定義し、適用するURLパターンを /* と指定した場合、サーブレットだけでなく、cssファイルを使用している箇所において、cssが適用されない問題が生じました。

ブラウザのデベロッパーツールを使って調べると、次のエラーが表示されていました。

MIME タイプが “text/css” ではなく “text/html” となっているため、スタイルシート http://localhost:8080/book/css/book.css は読み込まれていません。

これは、サーブレットフィルタのURLパターンが /* となっていると、コンテキストルート以下のすべてのファイルのMIMEタイプがフィルタのsetContentTypeで指定した text/html が適用されてしまったためです。

対策としては、例えば次があります。

  1. サーブレットやJSPのURL指定を、/servlet/chapter5/hello のように共通のパス要素を先頭に付与し、フィルタの適用URLパターンを /servlet/* のように先頭のパス要素を指定する
    • urlPatterns={"/chapter5/*", "/chapter6/*", ... "/chapter20/*"} のように複数列挙することも可能
  2. サーブレットやJSPのURL指定を、/chapter5/HelloServlet、/chapter5/hello.jps のように特定の接尾辞や拡張子を付けるようルール化し、フィルタの適用パターンを "/*Servlet", "/*.jsp" のように指定する

JSPのWebアプリケーションにおけるコーディング規約の1つ、Code Convention for the JavaServer Pages Technology Version 1.x Language より
https://www.oracle.com/technical-resources/articles/javase/code-convention.html

  • JSPファイル
    コンテキストルート/サブシステムパス/
  • CSSファイル
    コンテキストルート/css/
  • 画像ファイル
    コンテキストルート/images
  • HTMLファイル
    コンテキストルート/サブシステムパス/

このようなディレクトリ構成をとって、サーブレットフィルタをサブシステムパス以下に適用すれば、CSSや画像ファイルに誤って MIMEタイプが指定されることが避けられます。

JNDI/JDBCのリソースをきちんとクローズする

書籍では、JDBCを使ったデータベースアクセスのコードにおいて、リソースのクローズのコーディングは次のようになっています。

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()) {
        out.println(rs.getInt("id"));
        :
    }

    st.close();
    con.close();
} catch (Exception e) {
    e.printStackTrace(out);
}

いくつか問題点を含んでいるコードとなっています。

  • closeメソッドを呼んでいる対象は、PreparedStatementとConnectionの2つ。InitialContextとResultSetもcloseメソッドを持っているが、この2つについてはcloseメソッドを呼んでいない。
  • tryブロックのコードの途中で例外が発生した場合、st.close()とcon.close()が呼ばれることなくブロックを抜けてしまうため、リソースリークの要因となる

そこで、次の2つの観点で整理していきます。

  1. クローズしなければいけないリソースはどれか
  2. リソースリークが発生しないようにクローズする方法

明示的にクローズするリソースの確認

ResultSetのcloseを明示的にせずとも、Statementのcloseを呼べばResutlSetもクローズされるとの記載もあります。
ですが、JDBCの実装においては コネクションプーリングなどでキャッシュされ、closeを呼んだとしても破棄ではなくプールに戻すようになっている場合や、ガベージコレクションに委ねており、finalizeが遅延した場合にメモリに抱え続けるといった可能性もあるので、ResultSetも明示的にクローズするのがよいのではないでしょうか。

そこで、次のリソースについては、プログラムの中で明示的にクローズが必要と考えます。

  • データソースから取得したConnection
  • PreparedStatement
  • ResultSet
  • データベースアクセスの際に、JNDIのデータソース取得に使用する InitialContext
資料メモ(1) The Java Tutorial

Javaチュートリアルドキュメントから、JDBCの接続をクローズする記述では、Connection, Statement, ResultSetのオブジェクトを使用し終わったら直ちにcloseメソッドを呼んでリソースを解放するとの記載があります。

When you are finished using a Connection, Statement, or ResultSet object, call its close method to immediately release the resources it's using.

同じくJavaチュートリアルドキュメントから、JNDIのLDAP APIでは、Contextインスタンスが使用されなくなったらガベージコレクタがそのインスタンスを除去するときにリソースを解放するので明示的なclose呼び出しは必須ではないとの記載があります。

Normal garbage collection takes care of removing Context instances when they are no longer in use. Connections used by Context instances being garbage collected will be closed automatically. Therefore, you do not need to explicitly close connections.

資料メモ(2) ブログなどから

Connection、Statement、ResultSetのクローズ方法について解説。(try-with-resourceの導入前の記事)

アプリケーションサーバーによっては、PreparedStatementキャッシュを搭載し、サーブレット/JSPが1回の実行でPreparedStatementを生成・クローズするが裏でそのPreparedStatementをキャッシュしておき、再度同じSQLを使用する時に再利用することで高速化を図っているというお話し。

この場合、PreparedStatementが実際にはcloseされないので、StatementのcloseでResultSetがcloseされることを期待していてもResutlSetが即座にはcloseされないというケースが発生します。

リソースリークを発生させないクローズ方法

複数のリソース(Connection、Statement、ResultSet)を、使用中のいろいろな状況で確実にクローズするようなコーディングを検討します。

closeの呼び出しはfinallyブロックで

tryブロックの最後の方にclose呼び出しを記載しても、tryブロックの途中で例外が発生するとcatch節に飛んでしまい、closeが呼び出されません。そこで、close呼び出しはfinallyブロックに記載します。

最初に、finallyブロックにcloseを記載するものの、まだ問題が含まれているコードを示します。

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

このコードでは、次の問題が含まれています。

  • close()呼び出しが4つ続いているうち、途中のcloseで例外が発生すると、残りのcloseが呼ばれずに抜けてしまう。
  • また、そのときこのtry-catch-finallyブロックからスローされる例外は、try文での例外ではなくfinallyブロックの例外に上書きされてしまう。

そこで、closeが確実に実行されるようにすること、およびfinallyブロックから例外を外に漏らすことのないようにコーディングをします。

} finally {
    try {
        rs.close();
    } catch (Exception ignored) {
    }
    try {
        st.close();
    } catch (Exception ignored) {
    }
    try {
        con.close();
    } catch (Exception ignored) {
    }
}

close文で発生した例外はエラー処理をする意義が薄いので、catch文では何もせず例外を握りつぶしています。
それでも面倒なコーディングになってしまっています。

ネストしたtry-finallyで
try {
    InitialContext ic = null;
    DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/book");
    Connection con = ds.getConnection();
    try {
        PreparedStatement st = con.prepareStatement("select * from product");
        try {
            ResultSet rs = st.executeQuery();
            try {
                while (rs.next()) {
                    out.println(rs.getInt("id"));
                    :
                }
            } finally {
                rs.close();
            }
        } finally {
            st.close();
        }
    } finally {
        con.close();
    }
} catch (Exception e) {
    e.printStackTrace(out);
}
  • closeの対象変数をtry文の外側で初期値nullで宣言しなくて良いので、finallyブロックでnullチェックが不要となる
  • closeで例外が発生した場合、そのtryブロックから発生する例外は、closeの例外に上書きされる
try-with-resource構文を使う

まずは、ネストが1段のtry-with-resource構文

DataSource ds = null;
try {
    InitialContext ic = new InitialContext();
    ds = (DataSource) ic.lookup("java:comp/env/jdbc/book");
} catch (NamingException e) {
    // エラーページに飛ぶ等の対処
}
try (
    Connection con = ds.getConnection();
    PreparedStatement st = con.prepareStatement("select * from product");
    ResultSet rs = st.executeQuery();
) {
    while (rs.next()) {
        out.println(rs.getInt("id"));
        :
    }
} catch (Exception e) {
    e.printStackTrace(out);
}
  • try-with-resourceを用いると、tryブロックおよびtryの()内に記述したAutoCloseableなクラスのインスタンスをcloseした時に例外が発生しても、closeで発生した例外は抑制(suppress)される。抑制された例外は、tryブロックで発生した例外インスタンスにgetSuppressedメソッドを呼び出すと取得できる。
  • PreparedStatementにパラメータをセットする必要がある場合、1つのtry-with-resourceで3つをまとめることができない
try-with-resource構文を使う(多段)
DataSource ds = null;
try {
    InitialContext ic = new InitialContext();
    ds = (DataSource) ic.lookup("java:comp/env/jdbc/book");
} catch (NamingException e) {
    // エラーページに飛ぶ等の対処
}
try (
    Connection con = ds.getConnection();
    PreparedStatement st = con.prepareStatement("insert into product(name, price) values(?, ?)")
) {
    st.setString(1, name);
    st.setInt(2, price);
    try (ResultSet rs = st.executeUpdate()) {
        while (rs.next()) {
            out.println(rs.getInt("id"));
            :
        }
    }
} catch (Exception e) {
    e.printStackTrace(out);
}

CSSを使うには

HTMLからの使用

HTMLからcssファイルを適用するには、link要素でstylesheetを参照します。

  <head>
    <link rel="stylesheet" href="css/book.css">
  </head>

その際、Webアプリケーションとして構成するHTMLファイルは、コンテキストルートの下にコンテキストパスが入ります。そのため、href="/css/book.css" と記述するとCSSファイルが参照できません。
そこで、相対パスでCSSファイルを参照します。

開発プロジェクトでは次のようにcssファイルを配置します。

src
  +- main
      +- webapp
          +- css
          |   +- book.css
          +- hello.html

JSPからの使用

HTMLと同様に、JSPファイルの head 要素に link 要素で参照を記載しますが、JSPの場合、相対パスでcssのURLを指定するのはJSPファイルの階層変更時につど変更が発生するので厄介です。

サーバー上の配置例

webapps
  +-- book
       +-- css
       |     +-- book.css
       +-- chapter14
             +-- insert.jsp

上記のような配置の時、insert.jspの中からbook.cssを参照するには、相対URLを使って次のように記載することが可能です。ただし、JSPファイルの階層を変更すると、相対パスが変わるので都度修正が必要になります。
<link rel="stylesheet" href="../css/book.css">

絶対URLを使うと、コンテキストパスを含めて記述する必要があります。コンテキストパスを変更すると、絶対パスが変わるので都度修正が必要になります。
<link rel="stylesheet" href="/book/css/book.css">

そこで、コンテキストパスを動的に取得する記述をします。
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/book.css">

サーブレットからの使用

T.B.D.

フィルターの指定に注意

サーブレットのフィルター機能で、HttpServletResponseにsetContentType

response.setContentType("text/html;charset=UTF-8"); を設定し、フィルタの適用範囲を

@WebFilter(urlPatterns = {"/*"}) のように指定すると、HTMLやJSPだけでなく、CSSファイルもMIMEタイプがtext/htmlとして送信されてしまいます。その結果、CSSファイルが正しく適用されません。

Safariの開発者ツールでは、次のようなエラーが発生しているのを確認できました。

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

Chromeの開発者ツールでは、次のような警告メッセージが確認できました。

MIME タイプが “text/css” ではなく “text/html” となっているため、スタイルシート http://localhost:8080/book/css/book.css は読み込まれていません。

フィルターの指定を、サーブレット(JSP)に限定できるようにコンテキストパス以下のURLを設計する必要があります。

CSSの書き方(簡易版)

cssの記述 簡単な説明
body {
    background: antiquewhite;
}
ページの背景色を指定した色に
table {
    border-collapse: collapse;
}

th, td {
    border: solid 1px;
    padding: 8px;
}
セル同士の枠線を重ねる(border-collapse)
各セルに、枠線(実線で幅1px)を描画(border)
各セルのパディング(内側の余白)を8px分確保

時間のかかる処理

サーブレットの中で、時間のかかる処理を実施していると、サーブレットへのリクエストからコンテンツが表示されるまでの間、次の状況となってしまい好ましくありません。例えば、サーブレットの中からデータベースへの問い合わせを実施しており、データベースの問い合わせの応答に数十秒、数分、もしくは数十分かかった場合などです。

  • 直接サーブレットのURLをリクエストした場合、処理が終わるまでブラウザの画面がブランクのまま
  • フォームからサーブレットのURLをsubmitした場合、サーブレットの中で処理が終わるまで、フォームの画面が表示されたまま

画面の問題だけでなく、サーブレットを実行するスレッドが処理時間の間1ユーザーのリクエストに張り付いてしまうので、数百〜数万アクセスがあった場合にリソースがパンクしてしまう等の問題になります。

対策案

  • Servlet 3.0仕様で導入された Asynchronous Processing(非同期処理) を使う
  • Servlet 3.1仕様で導入された Non-blocking IO を使う

後者は、処理が遅い場合ではなく、I/Oが遅い場合の対処になるかと思います。(ユースケースは何?)

非同期処理

Servlet 3.0仕様で非同期処理(Asynchronous Processing)が導入されました。
サーブレットの処理(serviceメソッド -> doGet/doPostメソッド)では、時間のかかる処理をAsyncContextのコンテクスト上で別スレッドで実行させ、serviceメソッドから速やかに復帰します。処理結果は、AsyncContextから得られるresponseのPrintWriterを使って結果を返すか、または別なサーブレット(JSP)にディスパッチします。

  • WebServletアノテーションに、属性 @asyncSupported=true を追加
  • AsyncContext インスタンスを生成 (AsyncContext ctx = req.startAsync(req, res); または AsyncContext ctx = req.startAsync();@)
  • AsyncContext のstartメソッドで、別スレッドで処理するタスク(Runnable)を渡す
  • 別スレッドで処理するタスクは、処理が終わったら AsyncContextのcompleteメソッドを呼ぶ
  • responseから取得するPrintWriterに、逐次出力するときは、flush()を呼ぶとその時点でブラウザに送られます。flush()を呼ばない場合、completeメソッドを呼んだ段階でWebブラウザに出力されます。
@WebServlet(urlPatterns = {"/async/helloasync"}, asyncSupported = true)
public class HelloAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println(""" 
                <!DOCTYPE html>
                <html>
                <head>
                <meta charset="utf-8">
                </head>
                <body>
                <h1>非同期処理Servlet</h1>
                <h2>doGet</h2>
                <p>この後、非同期処理を実行し、その中でHTML出力を継続します。</p>""");

        AsyncContext asyncContext = req.startAsync();
        asyncContext.start(() -> asyncProcess(asyncContext));
    }

    private void asyncProcess(AsyncContext asyncContext) {
        try {
            PrintWriter out = asyncContext.getResponse().getWriter();
            out.println("<h2>asyncProcess</h2>");
            out.println("<p>これから時間のかかる処理を開始します。</p>");
            out.flush();
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException e) {}
            out.println("<p>時間のかかる処理が終わりました。</p>");
            out.println("</body></html>");
            asyncContext.complete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

AsyncContextのタイムアウトは、Tomcatの場合デフォルトで30秒なので、それ以上の場合はAsyncContext#setTimeoutで明示的にタイムアウト値を設定します。


約1ヶ月前に更新