2.JSP
学習目標
- JSP が HTML を生成してブラウザーに返す流れを説明できる
- JSP テンプレートに値を埋め込み、リストを繰り返して一覧を表示できる
- サーブレットが用意した値を JSP に渡せる
- データベースから取得した一覧を JSP で表示できる
1. JSP テンプレート
Section titled “1. JSP テンプレート”JSP(JavaServer Pages)は、HTML を書くファイルです。拡張子は .jsp、中身の大部分は通常の HTML で、本のタイトルや価格のように表示のたびに変わる部分だけを、値を埋め込む専用の記法で書きます。
リクエストが届くと、サーバーが JSP から HTML を組み立てます。固定の HTML はそのまま使い、値を埋め込む部分には用意された値を入れて、1 つの HTML にします。ブラウザーに返るのは、この組み立て後の HTML です。
HTTP とサーブレット では、doGet の中で out.println を使い、HTML を 1 行ずつ文字列として出力しました。この方法も、サーバーが HTML を組み立ててブラウザーに返す点は JSP と同じです。違いは、HTML を Java コードで組み立てるか、HTML ファイルとして書くかにあります。
あいさつを表示する hello.jsp です。先頭の 1 行を除けば、中身は通常の HTML です。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><!DOCTYPE html><html><head> <title>あいさつ</title></head><body> <p>ようこそ</p></body></html>先頭の <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> は JSP の宣言です。contentType は生成する HTML の文字コード、pageEncoding は JSP ファイル自身の文字コードを、どちらも UTF-8 に指定します。2 行目以降は通常の HTML で、そのままブラウザーに表示されます。
out.println のように HTML を文字列リテラルに分解しないため、画面の構造が .jsp の中にそのまま残ります。ここまでは値を埋め込む部分のない、固定の HTML だけの JSP です。表示のたびに変わる値を埋め込む記法は、値の埋め込み で扱います。
1-1. JSP の実行準備
Section titled “1-1. JSP の実行準備”JSP を動かすには、プロジェクトの準備 で作成したプロジェクトに 2 つの変更を加えます。
1 つ目は、パッケージング形式の変更です。Spring Boot の Jar では、組み込みの Tomcat が JSP を読み込めません。pom.xml の <packaging> を war に変えると、JSP を扱える形式になります。
<packaging>war</packaging>2 つ目は、JSP の実行に必要な依存関係の追加です。<dependencies> に次の 3 つを加えます。
<!-- JSP をコンパイルして実行する --><dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope></dependency>
<!-- JSP で繰り返しや条件分岐を記述する JSTL --><dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId></dependency><dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId></dependency>tomcat-embed-jasper は JSP をコンパイルして実行するエンジン、残りの 2 つは JSP 内で一覧の繰り返しなどを記述する JSTL のライブラリです。pom.xml を保存すると依存関係が解決されます。
JSP ファイルを置く src/main/webapp/WEB-INF/jsp フォルダーは、生成直後のプロジェクトには存在しないため、手で作成します。WEB-INF の中のファイルは、ブラウザーから URL で直接開けません。JSP をここに置くと、必ずサーブレットを経由して表示されます。
1-2. 値の埋め込み
Section titled “1-2. 値の埋め込み”固定の HTML だけの hello.jsp に、表示のたびに変わる値を埋め込みます。hello.jsp の ようこそ に、${name} で名前を加えます。
<body> <p>ようこそ、${name} さん</p></body>${...} の記法を EL(Expression Language)と呼びます。${name} は、name という属性名で用意された値に置き換わって出力されます。name に 田中 が用意されていれば、${name} と書いた位置が 田中 になります。
<body> の中の ${name} だけが可変部分で、ほかは固定の HTML のままです。hello.jsp 自体には ${name} に入れる値がありません。値と、それに付ける name という属性名は、サーブレットが用意して JSP に渡します。
1-3. サーブレットからの受け渡し
Section titled “1-3. サーブレットからの受け渡し”HTTP とサーブレット の HelloServlet は、doGet の中で response に HTML を直接書き込みました。JSP を使う場合、サーブレットは response に HTML を書きません。表示する値だけを用意して JSP に渡し、HTML の生成は JSP に任せます。この受け渡しには request を使います。
request.setAttribute("属性名", 値);request.getRequestDispatcher("JSP のパス").forward(request, response);setAttribute は値に 属性名 を付けて request に格納し、forward はその request を指定した JSP に渡して表示を移します。属性名は、JSP 側で値を取り出すための目印です。name という属性名で 田中 を格納し、hello.jsp に渡すサーブレットです。
package com.example.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;
@WebServlet("/hello-view")public class HelloViewServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // name という属性名で値を格納し、hello.jsp に渡す request.setAttribute("name", "田中"); request.getRequestDispatcher("/WEB-INF/jsp/hello.jsp").forward(request, response); }}@WebServlet・HttpServlet・doGet の形は HelloServlet と同じです。異なるのは、response に HTML を書く代わりに、setAttribute で値を格納し forward で hello.jsp に渡している点です。forward は転送先で例外が発生すると ServletException をスローするため、doGet の宣言に throws ServletException, IOException を加えています。
ブラウザーで /hello-view を開くと、HelloViewServlet が hello.jsp に name を渡し、${name} が 田中 に置き換わった HTML が表示されます。
<p>ようこそ、田中 さん</p>setAttribute で格納した値は、その 1 回のリクエストの処理が終わるまで有効です。この、1 リクエスト内だけで有効な範囲を リクエストスコープ と呼びます。forward はサーバー内部で同一のリクエストをそのまま JSP に渡すため、サーブレットで格納した値を JSP から参照できます。リクエストが終わると値は破棄され、次のリクエストには引き継がれません。
1-4. データの表示
Section titled “1-4. データの表示”${name} で表示したのは 1 つの文字列でした。本一覧で表示するのは、タイトルと価格をあわせ持つ本のデータです。複数のフィールドを持つデータは、クラス のインスタンスとして表します。タイトルと価格を持つ Book クラスを定義します。
package com.example.hello;
public class Book { private final String title; private final int price;
public Book(String title, int price) { this.title = title; this.price = price; }
public String getTitle() { return title; }
public int getPrice() { return price; }}Book のフィールドは、EL で ${book.title} のように、属性名のあとにドットでフィールド名を続けて取り出します。book という属性名で 1 冊の Book が用意されているとき、${book.title} はその本のタイトル、${book.price} は価格に置き換わります。1 冊の本を表示する book.jsp です。${name} のときと同じく、可変部分だけを EL で書き、ほかは通常の HTML です。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><!DOCTYPE html><html><head> <title>書籍</title></head><body> <p>${book.title}(${book.price}円)</p></body></html>book.jsp に Book を渡すサーブレットも、HelloViewServlet と同じく setAttribute と forward を使います。渡す値が、文字列から Book のインスタンスに変わるだけです。
@WebServlet("/book")public class BookServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // book という属性名で 1 冊の Book を格納し、book.jsp に渡す Book book = new Book("こころ", 473); request.setAttribute("book", book); request.getRequestDispatcher("/WEB-INF/jsp/book.jsp").forward(request, response); }}ブラウザーで /book を開くと、book.jsp の ${book.title} と ${book.price} がそれぞれ置き換わった HTML が表示されます。
<p>こころ(473円)</p>${book.title} は book.getTitle()、${book.price} は book.getPrice() の戻り値に置き換わります。getter の呼び出しは EL が行うため、JSP に書くのは ${book.title} だけです。
1-5. 一覧の繰り返し
Section titled “1-5. 一覧の繰り返し”book.jsp は本 1 冊を表示しました。一覧では、同じ形の行を本の数だけ繰り返します。繰り返しには、JSTL の <c:forEach> を使います。JSTL は EL と違い、使う前に JSP の先頭で宣言が必要です。
<c:forEach var="要素の名前" items="${リスト}"> ... 要素ごとに出力する HTML ...</c:forEach>items に繰り返すリスト、var に 1 要素を受け取る変数名を指定します。リストの要素数だけ、内側の HTML が繰り返されます。本の一覧を表示する books.jsp です。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ taglib prefix="c" uri="jakarta.tags.core" %><!DOCTYPE html><html><head> <title>書籍一覧</title></head><body> <h1>書籍一覧</h1> <table> <tr><th>タイトル</th><th>価格</th></tr> <c:forEach var="book" items="${books}"> <tr> <td>${book.title}</td> <td>${book.price}円</td> </tr> </c:forEach> </table></body></html>2 行目の <%@ taglib prefix="c" uri="jakarta.tags.core" %> は、<c:forEach> を使うための JSTL の宣言です。items="${books}" は books という名前のリストを参照し、var="book" で 1 要素を book として受け取ります。<c:forEach> の内側の <tr> が要素数だけ繰り返され、各回の ${book.title} と ${book.price} は、その回の book の値に置き換わります。book.jsp で 1 冊を表示した ${book.title} が、リストの要素ごとに繰り返されます。
books.jsp に渡すのは、Book 1 冊ではなく List<Book> です。BookServlet の渡し方を、リストの格納に変えます。
@WebServlet("/books")public class BookListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 表示する本のリストを用意する(この段階では固定のデータ) List<Book> books = List.of( new Book("こころ", 473), new Book("吾輩は猫である", 693) );
// books という属性名で request に格納し、books.jsp に渡す request.setAttribute("books", books); request.getRequestDispatcher("/WEB-INF/jsp/books.jsp").forward(request, response); }}@WebServlet から下は HelloViewServlet と同じ形で、List<Book> を扱う java.util.List の import が加わります。ブラウザーで /books を開くと、books.jsp の <c:forEach> がリストの 2 冊を 2 行に展開した HTML が表示されます。
<h1>書籍一覧</h1><table> <tr><th>タイトル</th><th>価格</th></tr> <tr><td>こころ</td><td>473円</td></tr> <tr><td>吾輩は猫である</td><td>693円</td></tr></table>本が何件でも books の要素数に応じて行数が変わるため、books.jsp を書き換える必要はありません。BookListServlet が用意するリストの中身が、そのまま表に並びます。
2. データベースからの取得
Section titled “2. データベースからの取得”ここまでの本のリストは、サーブレットに直接書いた固定のデータでした。書店アプリでは、本のデータは JDBC で扱った books テーブルにあります。固定リストを用意していた箇所を、JDBC での取得に差し替えます。
JDBC で MySQL に接続するには、ドライバーを <dependencies> に加えます。
<!-- MySQL に接続する JDBC ドライバー --><dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope></dependency>@WebServlet("/books")public class BookListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // books テーブルから本の一覧を取得する List<Book> books = new ArrayList<>(); String url = "jdbc:mysql://127.0.0.1:3307/bookstore"; String sql = "SELECT title, price FROM books";
try (Connection conn = DriverManager.getConnection(url, "root", "root"); PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) {
// 1 行ずつ読み取り、Book に変換してリストに追加する while (rs.next()) { String title = rs.getString("title"); int price = rs.getInt("price"); books.add(new Book(title, price)); } } catch (SQLException e) { throw new ServletException(e); }
// request への格納と JSP への受け渡しは固定リストのときと同じ request.setAttribute("books", books); request.getRequestDispatcher("/WEB-INF/jsp/books.jsp").forward(request, response); }}JDBC で main メソッドに記述した接続・SELECT・結果の読み取りを、そのまま doGet に移しています。Connection・PreparedStatement・ResultSet・SQLException(java.sql)と ArrayList(java.util)の import が新たに加わります。
固定リストでは件数が決まっていたため List.of で作れましたが、DB から取得する件数は実行するまで分かりません。1 行読むたびに books.add で要素を足せるよう、空の new ArrayList<>() から始めます。SQLException はチェック例外なので、catch で受けて ServletException として再スローします。
変わったのは本のリストを用意する部分だけで、request への格納と books.jsp への受け渡しは固定リストのときと同じです。books.jsp にも手を加えていません。
3. 処理と表示の分離
Section titled “3. 処理と表示の分離”out.println で HTML を組み立てていたときは、処理(Java)と画面(HTML)が同じメソッドに混在していました。BookListServlet と books.jsp に分けたことで、この 2 つが別々のファイルに分かれます。
| 担当 | 役割 |
|---|---|
BookListServlet(サーブレット) | リクエストを受け取り、データを用意して JSP に渡す処理 |
books.jsp(JSP) | 受け取ったデータを HTML に埋め込む表示 |
サーブレットがデータを用意して JSP に渡し、JSP はそれを表示します。これは、サービスクラスと単一責任 の、1 つのクラスに役割を 1 つだけ持たせる考え方を、処理と表示の分担に当てはめたものです。
役割が分かれていると、変更の影響も分かれます。画面の見た目を変えるときに触れるのは books.jsp だけで、サーブレットには影響しません。逆に、データの取得元を固定リストから DB に差し替えたときは、BookListServlet だけが変わり、books.jsp はそのままでした。