Skip to content
Playground

2.JSP

学習目標

  • JSP が HTML を生成してブラウザーに返す流れを説明できる
  • JSP テンプレートに値を埋め込み、リストを繰り返して一覧を表示できる
  • サーブレットが用意した値を JSP に渡せる
  • データベースから取得した一覧を 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 です。

hello.jsp
<%@ 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 です。表示のたびに変わる値を埋め込む記法は、値の埋め込み で扱います。

JSP を動かすには、プロジェクトの準備 で作成したプロジェクトに 2 つの変更を加えます。

1 つ目は、パッケージング形式の変更です。Spring Boot の Jar では、組み込みの Tomcat が JSP を読み込めません。pom.xml<packaging>war に変えると、JSP を扱える形式になります。

pom.xml
<packaging>war</packaging>

2 つ目は、JSP の実行に必要な依存関係の追加です。<dependencies> に次の 3 つを加えます。

pom.xml
<!-- 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 をここに置くと、必ずサーブレットを経由して表示されます。

固定の HTML だけの hello.jsp に、表示のたびに変わる値を埋め込みます。hello.jspようこそ に、${name} で名前を加えます。

hello.jsp
<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 に渡すサーブレットです。

HelloViewServlet.java
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);
}
}

@WebServletHttpServletdoGet の形は HelloServlet と同じです。異なるのは、response に HTML を書く代わりに、setAttribute で値を格納し forwardhello.jsp に渡している点です。forward は転送先で例外が発生すると ServletException をスローするため、doGet の宣言に throws ServletException, IOException を加えています。

ブラウザーで /hello-view を開くと、HelloViewServlethello.jspname を渡し、${name}田中 に置き換わった HTML が表示されます。

<p>ようこそ、田中 さん</p>

setAttribute で格納した値は、その 1 回のリクエストの処理が終わるまで有効です。この、1 リクエスト内だけで有効な範囲を リクエストスコープ と呼びます。forward はサーバー内部で同一のリクエストをそのまま JSP に渡すため、サーブレットで格納した値を JSP から参照できます。リクエストが終わると値は破棄され、次のリクエストには引き継がれません。

${name} で表示したのは 1 つの文字列でした。本一覧で表示するのは、タイトルと価格をあわせ持つ本のデータです。複数のフィールドを持つデータは、クラス のインスタンスとして表します。タイトルと価格を持つ Book クラスを定義します。

Book.java
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 です。

book.jsp
<%@ 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.jspBook を渡すサーブレットも、HelloViewServlet と同じく setAttributeforward を使います。渡す値が、文字列から Book のインスタンスに変わるだけです。

BookServlet.java
@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} だけです。

book.jsp は本 1 冊を表示しました。一覧では、同じ形の行を本の数だけ繰り返します。繰り返しには、JSTL の <c:forEach> を使います。JSTL は EL と違い、使う前に JSP の先頭で宣言が必要です。

<c:forEach var="要素の名前" items="${リスト}">
... 要素ごとに出力する HTML ...
</c:forEach>

items に繰り返すリスト、var に 1 要素を受け取る変数名を指定します。リストの要素数だけ、内側の HTML が繰り返されます。本の一覧を表示する books.jsp です。

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 の渡し方を、リストの格納に変えます。

BookListServlet.java
@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.Listimport が加わります。ブラウザーで /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 が用意するリストの中身が、そのまま表に並びます。

ここまでの本のリストは、サーブレットに直接書いた固定のデータでした。書店アプリでは、本のデータは JDBC で扱った books テーブルにあります。固定リストを用意していた箇所を、JDBC での取得に差し替えます。

JDBC で MySQL に接続するには、ドライバーを <dependencies> に加えます。

pom.xml
<!-- MySQL に接続する JDBC ドライバー -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
BookListServlet.java
@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);
}
}

JDBCmain メソッドに記述した接続・SELECT・結果の読み取りを、そのまま doGet に移しています。ConnectionPreparedStatementResultSetSQLExceptionjava.sql)と ArrayListjava.util)の import が新たに加わります。

固定リストでは件数が決まっていたため List.of で作れましたが、DB から取得する件数は実行するまで分かりません。1 行読むたびに books.add で要素を足せるよう、空の new ArrayList<>() から始めます。SQLExceptionチェック例外なので、catch で受けて ServletException として再スローします。

変わったのは本のリストを用意する部分だけで、request への格納と books.jsp への受け渡しは固定リストのときと同じです。books.jsp にも手を加えていません。

out.println で HTML を組み立てていたときは、処理(Java)と画面(HTML)が同じメソッドに混在していました。BookListServletbooks.jsp に分けたことで、この 2 つが別々のファイルに分かれます。

担当役割
BookListServlet(サーブレット)リクエストを受け取り、データを用意して JSP に渡す処理
books.jsp(JSP)受け取ったデータを HTML に埋め込む表示

サーブレットがデータを用意して JSP に渡し、JSP はそれを表示します。これは、サービスクラスと単一責任 の、1 つのクラスに役割を 1 つだけ持たせる考え方を、処理と表示の分担に当てはめたものです。

役割が分かれていると、変更の影響も分かれます。画面の見た目を変えるときに触れるのは books.jsp だけで、サーブレットには影響しません。逆に、データの取得元を固定リストから DB に差し替えたときは、BookListServlet だけが変わり、books.jsp はそのままでした。