はじめに
Servlet/JSP入門の第8回は EL式(Expression Language)とJSTL(JSP Standard Tag Library) です。
これまでのJSPでは <% ... %> のスクリプトレットを多用してきましたが、実はスクリプトレットは 現在では非推奨 です。代わりに、EL式とJSTLを使うことで、より読みやすくメンテナンスしやすいJSPを書けます。
第8回で学ぶこと
- スクリプトレットの問題点
- EL式(Expression Language)の基本構文
- リクエスト属性・セッション属性へのアクセス
- JavaBeanプロパティ、Map、Listへのアクセス
- JSTLコアタグ(c:if, c:choose, c:forEach, c:set, c:out)
- JSTLフォーマットタグ(fmt:formatDate, fmt:formatNumber)
- スクリプトレットからEL+JSTLへのリファクタリング
1. スクリプトレットの問題点
Before:スクリプトレットだらけのJSP
<%@ page import="java.util.List" %>
<%@ page import="model.User" %>
<%
List<User> users = (List<User>) request.getAttribute("users");
String message = (String) request.getAttribute("message");
%>
<html>
<body>
<% if (message != null) { %>
<p><%= message %></p>
<% } %>
<table>
<% for (User user : users) { %>
<tr>
<td><%= user.getName() %></td>
<td><%= user.getAge() %></td>
</tr>
<% } %>
</table>
</body>
</html>
問題点
| 問題 | 説明 |
|---|---|
| 可読性が低い |
<% %> でJavaとHTMLが入り交じる |
| デザイナーと分業できない | HTMLを編集するデザイナーがJavaコードを壊す危険 |
| テストが困難 | JSP内のロジックを単体テストできない |
| セキュリティリスク |
<%= %> はXSS対策がない(HTMLエスケープされない) |
After:EL式+JSTLを使ったJSP
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<body>
<c:if test="${not empty message}">
<p><c:out value="${message}" /></p>
</c:if>
<table>
<c:forEach var="user" items="${users}">
<tr>
<td><c:out value="${user.name}" /></td>
<td><c:out value="${user.age}" /></td>
</tr>
</c:forEach>
</table>
</body>
</html>
Javaコードが一切なくなり、HTMLタグと同じ記法で読めるようになりました。
2. EL式(Expression Language)の基本
EL式とは?
EL式 は ${式} の形式で、JSPページ内でデータを参照するための式言語です。
<!-- EL式の基本形 -->
${expression}
Servletで属性をセット → JSPでEL式で参照
Servlet側:
@WebServlet("/el-demo")
public class ELDemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// リクエスト属性にデータをセット
request.setAttribute("userName", "田中太郎");
request.setAttribute("age", 25);
request.setAttribute("isAdmin", true);
request.getRequestDispatcher("/WEB-INF/jsp/elDemo.jsp").forward(request, response);
}
}
JSP側(elDemo.jsp):
<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<body>
<h1>EL式のデモ</h1>
<p>名前: ${userName}</p>
<p>年齢: ${age}</p>
<p>管理者: ${isAdmin}</p>
<p>年齢 + 5 = ${age + 5}</p>
</body>
</html>
出力:
名前: 田中太郎
年齢: 25
管理者: true
年齢 + 5 = 30
EL式の演算子
| 演算子 | 意味 | 例 |
|---|---|---|
+, -, *, /
|
算術演算 | ${a + b} |
div, mod
|
除算、剰余 |
${10 div 3} → 3.3333...(浮動小数点除算) |
==, eq
|
等値比較 | ${name == '田中'} |
!=, ne
|
不等比較 | ${name != '田中'} |
<, lt
|
より小さい | ${age lt 30} |
>, gt
|
より大きい | ${age gt 20} |
<=, le
|
以下 | ${age le 25} |
>=, ge
|
以上 | ${age ge 18} |
&&, and
|
論理AND | ${a && b} |
| ` |
, or` |
|
!, not
|
論理NOT | ${not isAdmin} |
empty |
null/空チェック | ${empty name} |
? : |
三項演算子 | ${age >= 20 ? '成人' : '未成年'} |
3. EL式でのデータアクセス
3.1 JavaBeanプロパティへのアクセス
Servletでセットしたオブジェクトの getterメソッド に自動でアクセスできます。
User.java(JavaBean):
package model;
public class User {
private String name;
private int age;
private String email;
// getter / setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
Servlet側:
User user = new User();
user.setName("田中太郎");
user.setAge(25);
user.setEmail("tanaka@example.com");
request.setAttribute("user", user);
JSP側:
<!-- user.getName() が呼ばれる -->
<p>名前: ${user.name}</p>
<!-- user.getAge() が呼ばれる -->
<p>年齢: ${user.age}</p>
<!-- user.getEmail() が呼ばれる -->
<p>メール: ${user.email}</p>
ポイント: ${user.name} は内部的に user.getName() を呼んでいます。EL式ではgetterの get を省略した プロパティ名 で参照します。
3.2 Mapへのアクセス
// Servlet側
Map<String, String> config = new HashMap<>();
config.put("theme", "dark");
config.put("language", "ja");
request.setAttribute("config", config);
<!-- JSP側 -->
<p>テーマ: ${config.theme}</p>
<p>テーマ: ${config["theme"]}</p> <!-- こちらの書き方もOK -->
<p>言語: ${config.language}</p>
3.3 Listへのアクセス
// Servlet側
List<String> fruits = List.of("りんご", "バナナ", "みかん");
request.setAttribute("fruits", fruits);
<!-- JSP側 -->
<p>先頭: ${fruits[0]}</p>
<p>2番目: ${fruits[1]}</p>
<p>3番目: ${fruits[2]}</p>
3.4 スコープの検索順序
EL式で ${userName} と書いた場合、以下の順序でスコープを検索します。
| 優先順位 | スコープ | EL暗黙オブジェクト | 説明 |
|---|---|---|---|
| 1 | page | pageScope |
そのJSPページ内のみ |
| 2 | request | requestScope |
1リクエスト内 |
| 3 | session | sessionScope |
セッション内 |
| 4 | application | applicationScope |
アプリ全体 |
スコープを明示する場合:
<!-- リクエスト属性を明示的に参照 -->
${requestScope.userName}
<!-- セッション属性を明示的に参照 -->
${sessionScope.loginUser}
3.5 EL式の暗黙オブジェクト
| 暗黙オブジェクト | 型 | 説明 |
|---|---|---|
param |
Map<String,String> |
リクエストパラメータ |
paramValues |
Map<String,String[]> |
リクエストパラメータ(複数値) |
header |
Map<String,String> |
リクエストヘッダー |
cookie |
Map<String,Cookie> |
クッキー |
pageContext |
PageContext |
ページコンテキスト |
<!-- リクエストパラメータを直接参照 -->
<p>検索キーワード: ${param.keyword}</p>
<!-- コンテキストパスを取得 -->
<a href="${pageContext.request.contextPath}/users">ユーザー一覧</a>
4. JSTLの導入
JSTLとは?
JSTL(JSP Standard Tag Library) は、JSPでよく使う処理(条件分岐、繰り返し、フォーマットなど)を カスタムタグ として提供するライブラリです。
JSTLの導入方法
JARファイルをプロジェクトに追加する必要があります。
Maven の場合(pom.xml):
<!-- JSTL API -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<!-- JSTL 実装 -->
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
手動の場合は、上記2つのJARをダウンロードして WEB-INF/lib/ に配置します。
JSTLのタグライブラリ
| プレフィックス | URI(Tomcat 10+) | 用途 |
|---|---|---|
c |
jakarta.tags.core |
コアタグ(条件分岐、繰り返し等) |
fmt |
jakarta.tags.fmt |
フォーマットタグ(日付、数値等) |
fn |
jakarta.tags.functions |
関数タグ(文字列操作等) |
タグライブラリの宣言
JSPファイルの先頭に taglib ディレクティブを追加します。
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
5. JSTLコアタグ
5.1 c:out - 値の出力
<!-- HTMLエスケープ付きで出力(XSS対策) -->
<c:out value="${user.name}" />
<!-- デフォルト値の指定 -->
<c:out value="${user.nickname}" default="未設定" />
なぜ ${user.name} ではなく <c:out> を使うのか?
| 書き方 | HTMLエスケープ | XSS対策 |
|---|---|---|
${user.name} |
されない | なし(危険) |
<c:out value="${user.name}" /> |
される | あり(安全) |
例:ユーザーが名前に <script>alert('XSS')</script> と入力した場合
${user.name} → スクリプトが実行される(XSS攻撃成功)
<c:out value="${user.name}" /> → <script>... と表示される(安全)
5.2 c:set - 変数の設定
<!-- 変数の設定 -->
<c:set var="greeting" value="こんにちは" />
<p>${greeting}</p>
<!-- スコープを指定 -->
<c:set var="count" value="${count + 1}" scope="session" />
<!-- オブジェクトのプロパティを設定 -->
<c:set target="${user}" property="name" value="新しい名前" />
5.3 c:if - 条件分岐
<!-- 単純な条件分岐 -->
<c:if test="${user.age >= 20}">
<p>${user.name}さんは成人です。</p>
</c:if>
<!-- 空チェック -->
<c:if test="${not empty users}">
<p>${fn:length(users)}件のユーザーが見つかりました。</p>
</c:if>
<!-- 文字列比較 -->
<c:if test="${user.role == 'admin'}">
<a href="/admin">管理画面</a>
</c:if>
注意:
c:ifにelseはありません。elseが必要な場合はc:chooseを使います。
5.4 c:choose / c:when / c:otherwise - 複数条件分岐
<c:choose>
<c:when test="${user.age < 13}">
<p>子供料金: 500円</p>
</c:when>
<c:when test="${user.age < 18}">
<p>学生料金: 800円</p>
</c:when>
<c:when test="${user.age >= 65}">
<p>シニア料金: 800円</p>
</c:when>
<c:otherwise>
<p>一般料金: 1,200円</p>
</c:otherwise>
</c:choose>
Javaの if-else if-else や switch に相当します。
5.5 c:forEach - 繰り返し
リストの繰り返し:
<table>
<tr><th>名前</th><th>年齢</th><th>メール</th></tr>
<c:forEach var="user" items="${users}">
<tr>
<td><c:out value="${user.name}" /></td>
<td>${user.age}</td>
<td><c:out value="${user.email}" /></td>
</tr>
</c:forEach>
</table>
インデックス付き:
<c:forEach var="user" items="${users}" varStatus="status">
<tr class="${status.index % 2 == 0 ? 'even' : 'odd'}">
<td>${status.count}</td> <!-- 1から始まる番号 -->
<td><c:out value="${user.name}" /></td>
</tr>
</c:forEach>
| varStatus属性 | 型 | 説明 |
|---|---|---|
index |
int | 0始まりのインデックス |
count |
int | 1始まりの番号 |
first |
boolean | 最初の要素かどうか |
last |
boolean | 最後の要素かどうか |
数値の繰り返し:
<!-- 1から10まで -->
<c:forEach var="i" begin="1" end="10">
<span>${i} </span>
</c:forEach>
<!-- 0から100まで10刻み -->
<c:forEach var="i" begin="0" end="100" step="10">
<option value="${i}">${i}</option>
</c:forEach>
5.6 c:forTokens - 文字列の分割
<c:set var="csvData" value="Java,Python,JavaScript,Go" />
<ul>
<c:forTokens var="lang" items="${csvData}" delims=",">
<li>${lang}</li>
</c:forTokens>
</ul>
5.7 c:url - URL生成
<!-- コンテキストパス付きのURLを生成 -->
<c:url var="userUrl" value="/users">
<c:param name="page" value="1" />
<c:param name="sort" value="name" />
</c:url>
<a href="${userUrl}">ユーザー一覧</a>
<!-- 結果: /WebStudy/users?page=1&sort=name -->
6. JSTLフォーマットタグ
6.1 fmt:formatDate - 日付のフォーマット
Servlet側:
request.setAttribute("now", new java.util.Date());
request.setAttribute("timestamp", java.sql.Timestamp.valueOf("2025-04-01 09:30:00"));
JSP側:
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!-- 日付フォーマット -->
<p>日付: <fmt:formatDate value="${now}" pattern="yyyy年MM月dd日" /></p>
<p>日時: <fmt:formatDate value="${now}" pattern="yyyy/MM/dd HH:mm:ss" /></p>
<p>曜日付き: <fmt:formatDate value="${now}" pattern="yyyy年MM月dd日(E)" /></p>
<!-- typeを指定するパターン -->
<p>日付のみ: <fmt:formatDate value="${now}" type="date" dateStyle="long" /></p>
<p>時刻のみ: <fmt:formatDate value="${now}" type="time" timeStyle="medium" /></p>
<p>日時両方: <fmt:formatDate value="${now}" type="both" /></p>
6.2 fmt:formatNumber - 数値のフォーマット
<c:set var="price" value="1234567" />
<c:set var="rate" value="0.085" />
<c:set var="score" value="78.5" />
<!-- カンマ区切り -->
<p>価格: <fmt:formatNumber value="${price}" pattern="#,###" />円</p>
<!-- 出力: 価格: 1,234,567円 -->
<!-- 通貨 -->
<p>通貨: <fmt:formatNumber value="${price}" type="currency" currencySymbol="¥" /></p>
<!-- 出力: 通貨: ¥1,234,567 -->
<!-- パーセント -->
<p>税率: <fmt:formatNumber value="${rate}" type="percent" /></p>
<!-- 出力: 税率: 9% -->
<!-- 小数点以下の桁数指定 -->
<p>スコア: <fmt:formatNumber value="${score}" pattern="#.00" /></p>
<!-- 出力: スコア: 78.50 -->
フォーマットタグまとめ
| タグ | 用途 | 例 |
|---|---|---|
fmt:formatDate |
日付を指定形式で表示 | yyyy年MM月dd日 |
fmt:formatNumber |
数値を指定形式で表示 | #,### |
fmt:parseDate |
文字列を日付に変換 | - |
fmt:parseNumber |
文字列を数値に変換 | - |
7. JSTL関数タグ
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<c:set var="text" value=" Hello, World! " />
<p>長さ: ${fn:length(text)}</p>
<p>大文字: ${fn:toUpperCase(text)}</p>
<p>小文字: ${fn:toLowerCase(text)}</p>
<p>トリム: [${fn:trim(text)}]</p>
<p>含む?: ${fn:contains(text, 'World')}</p>
<p>置換: ${fn:replace(text, 'World', 'Java')}</p>
<p>部分文字列: ${fn:substring(text, 2, 7)}</p>
<!-- リストの長さ -->
<p>ユーザー数: ${fn:length(users)}</p>
| 関数 | 説明 |
|---|---|
fn:length(obj) |
文字列の長さ / コレクションのサイズ |
fn:toUpperCase(str) |
大文字に変換 |
fn:toLowerCase(str) |
小文字に変換 |
fn:trim(str) |
前後の空白を除去 |
fn:contains(str, sub) |
部分文字列を含むか |
fn:startsWith(str, prefix) |
指定文字列で始まるか |
fn:endsWith(str, suffix) |
指定文字列で終わるか |
fn:replace(str, before, after) |
文字列の置換 |
fn:substring(str, begin, end) |
部分文字列の取得 |
fn:split(str, delim) |
文字列の分割 |
fn:join(array, delim) |
配列の結合 |
8. リファクタリング例
Before:スクリプトレット版(ユーザー一覧)
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.util.List" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="model.User" %>
<%
List<User> users = (List<User>) request.getAttribute("users");
String keyword = (String) request.getAttribute("keyword");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
%>
<!DOCTYPE html>
<html>
<body>
<h1>ユーザー一覧</h1>
<% if (keyword != null && !keyword.isEmpty()) { %>
<p>検索キーワード: <%= keyword %></p>
<% } %>
<% if (users != null && users.size() > 0) { %>
<table border="1">
<tr><th>#</th><th>名前</th><th>年齢</th><th>メール</th><th>登録日</th><th>区分</th></tr>
<%
int count = 0;
for (User user : users) {
count++;
%>
<tr>
<td><%= count %></td>
<td><%= user.getName() %></td>
<td><%= user.getAge() %></td>
<td><%= user.getEmail() %></td>
<td><%= sdf.format(user.getCreatedAt()) %></td>
<td>
<% if (user.getAge() < 20) { %>
未成年
<% } else if (user.getAge() < 65) { %>
一般
<% } else { %>
シニア
<% } %>
</td>
</tr>
<% } %>
</table>
<p>合計: <%= users.size() %>件</p>
<% } else { %>
<p>ユーザーが見つかりませんでした。</p>
<% } %>
</body>
</html>
After:EL式+JSTL版
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<body>
<h1>ユーザー一覧</h1>
<c:if test="${not empty keyword}">
<p>検索キーワード: <c:out value="${keyword}" /></p>
</c:if>
<c:choose>
<c:when test="${not empty users}">
<table border="1">
<tr><th>#</th><th>名前</th><th>年齢</th><th>メール</th><th>登録日</th><th>区分</th></tr>
<c:forEach var="user" items="${users}" varStatus="status">
<tr>
<td>${status.count}</td>
<td><c:out value="${user.name}" /></td>
<td>${user.age}</td>
<td><c:out value="${user.email}" /></td>
<td><fmt:formatDate value="${user.createdAt}" pattern="yyyy/MM/dd" /></td>
<td>
<c:choose>
<c:when test="${user.age < 20}">未成年</c:when>
<c:when test="${user.age < 65}">一般</c:when>
<c:otherwise>シニア</c:otherwise>
</c:choose>
</td>
</tr>
</c:forEach>
</table>
<p>合計: ${fn:length(users)}件</p>
</c:when>
<c:otherwise>
<p>ユーザーが見つかりませんでした。</p>
</c:otherwise>
</c:choose>
</body>
</html>
比較
| 項目 | スクリプトレット版 | EL+JSTL版 |
|---|---|---|
| import文 | 3つ必要 | 不要 |
| Javaコード | 大量 | なし |
| 可読性 | 低い | 高い |
| XSS対策 | なし | c:out で対策あり |
| 日付フォーマット | SimpleDateFormat が必要 | fmt:formatDate で簡単 |
練習問題
問題1:EL式で商品表示 ⭐
以下のServletからフォワードされたデータを、EL式を使ってJSPで表示してください。
Servlet側(すでに実装済みとする):
Map<String, Object> product = new HashMap<>();
product.put("name", "ノートPC");
product.put("price", 98000);
product.put("stock", 15);
product.put("onSale", true);
request.setAttribute("product", product);
表示要件:
- 商品名、価格(カンマ区切り)、在庫数を表示
- セール中の場合「セール中!」と表示
- 在庫が5個以下なら「残りわずか」と表示
模範解答
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!DOCTYPE html>
<html>
<head><title>商品詳細</title></head>
<body>
<h1>商品詳細</h1>
<table border="1">
<tr>
<th>商品名</th>
<td><c:out value="${product.name}" /></td>
</tr>
<tr>
<th>価格</th>
<td><fmt:formatNumber value="${product.price}" pattern="#,###" />円</td>
</tr>
<tr>
<th>在庫</th>
<td>
${product.stock}個
<c:if test="${product.stock <= 5}">
<span style="color: red; font-weight: bold;">残りわずか</span>
</c:if>
</td>
</tr>
<tr>
<th>ステータス</th>
<td>
<c:if test="${product.onSale}">
<span style="color: red; font-size: 1.2em;">セール中!</span>
</c:if>
<c:if test="${not product.onSale}">
通常販売
</c:if>
</td>
</tr>
</table>
</body>
</html>
ポイント: Mapのキーに対しては ${product.name} のようにドット記法でアクセスできます。fmt:formatNumber で価格をカンマ区切りにフォーマットしています。
問題2:c:forEach で成績表 ⭐⭐
Servletから以下のデータが渡されます。JSTLを使って成績表を表示し、平均点による評価(A/B/C/D)も表示してください。
Servlet側で渡すデータ:
List<Map<String, Object>> students = new ArrayList<>();
Map<String, Object> s1 = new HashMap<>();
s1.put("name", "田中");
s1.put("japanese", 85);
s1.put("math", 72);
s1.put("english", 90);
students.add(s1);
// ... 同様に複数の生徒を追加
request.setAttribute("students", students);
評価基準:
- A: 平均90点以上
- B: 平均70点以上
- C: 平均50点以上
- D: 平均50点未満
模範解答
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!DOCTYPE html>
<html>
<head>
<title>成績表</title>
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #333; padding: 8px; text-align: center; }
th { background-color: #4CAF50; color: white; }
.grade-A { color: gold; font-weight: bold; }
.grade-B { color: blue; }
.grade-C { color: green; }
.grade-D { color: red; }
</style>
</head>
<body>
<h1>成績表</h1>
<table>
<tr>
<th>#</th>
<th>名前</th>
<th>国語</th>
<th>数学</th>
<th>英語</th>
<th>合計</th>
<th>平均</th>
<th>評価</th>
</tr>
<c:forEach var="student" items="${students}" varStatus="status">
<c:set var="total" value="${student.japanese + student.math + student.english}" />
<c:set var="avg" value="${total / 3.0}" />
<tr>
<td>${status.count}</td>
<td><c:out value="${student.name}" /></td>
<td>${student.japanese}</td>
<td>${student.math}</td>
<td>${student.english}</td>
<td>${total}</td>
<td><fmt:formatNumber value="${avg}" pattern="#.0" /></td>
<td>
<c:choose>
<c:when test="${avg >= 90}">
<span class="grade-A">A</span>
</c:when>
<c:when test="${avg >= 70}">
<span class="grade-B">B</span>
</c:when>
<c:when test="${avg >= 50}">
<span class="grade-C">C</span>
</c:when>
<c:otherwise>
<span class="grade-D">D</span>
</c:otherwise>
</c:choose>
</td>
</tr>
</c:forEach>
</table>
</body>
</html>
ポイント: c:set でEL式の計算結果を変数に格納できます。c:choose/c:when/c:otherwise でif-else if-else相当の条件分岐を実現しています。fmt:formatNumber で小数点以下1桁に揃えています。
問題3:一覧 + 検索 + ページング表示 ⭐⭐
Servletから商品リストと検索条件が渡されるとします。以下の要件でJSPを作成してください。
- 検索フォーム(キーワード入力 + 検索ボタン)
- 商品をテーブルで一覧表示
- 価格が10,000円以上の商品は太字で表示
- 在庫が0の商品は「売り切れ」と赤字で表示
- リストが空の場合は「該当する商品がありません」と表示
- 検索結果の件数を表示
模範解答
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<head>
<title>商品検索</title>
<style>
body { font-family: sans-serif; margin: 20px; }
.search-form { margin-bottom: 20px; padding: 15px; background: #f0f0f0; border-radius: 5px; }
table { border-collapse: collapse; width: 100%; max-width: 900px; }
th, td { border: 1px solid #ddd; padding: 10px; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f9f9f9; }
.expensive { font-weight: bold; }
.sold-out { color: red; font-weight: bold; }
.result-count { margin: 10px 0; color: #666; }
</style>
</head>
<body>
<h1>商品検索</h1>
<!-- 検索フォーム -->
<div class="search-form">
<form action="${pageContext.request.contextPath}/products/search" method="get">
<label>キーワード:</label>
<input type="text" name="keyword" value="${fn:escapeXml(param.keyword)}" placeholder="商品名を入力">
<button type="submit">検索</button>
</form>
</div>
<!-- 検索条件の表示 -->
<c:if test="${not empty keyword}">
<p class="result-count">
「<c:out value="${keyword}" />」の検索結果: ${fn:length(products)}件
</p>
</c:if>
<!-- 商品リスト -->
<c:choose>
<c:when test="${not empty products}">
<table>
<tr>
<th>#</th>
<th>商品名</th>
<th>価格</th>
<th>在庫</th>
<th>ステータス</th>
</tr>
<c:forEach var="product" items="${products}" varStatus="status">
<tr>
<td>${status.count}</td>
<td>
<c:choose>
<c:when test="${product.price >= 10000}">
<span class="expensive"><c:out value="${product.name}" /></span>
</c:when>
<c:otherwise>
<c:out value="${product.name}" />
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="${product.price >= 10000}">
<span class="expensive">
<fmt:formatNumber value="${product.price}" pattern="#,###" />円
</span>
</c:when>
<c:otherwise>
<fmt:formatNumber value="${product.price}" pattern="#,###" />円
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="${product.stock == 0}">
<span class="sold-out">売り切れ</span>
</c:when>
<c:when test="${product.stock <= 5}">
<span style="color: orange;">${product.stock}個(残りわずか)</span>
</c:when>
<c:otherwise>
${product.stock}個
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="${product.stock == 0}">販売終了</c:when>
<c:when test="${product.onSale}">セール中</c:when>
<c:otherwise>通常販売</c:otherwise>
</c:choose>
</td>
</tr>
</c:forEach>
</table>
</c:when>
<c:otherwise>
<p>該当する商品がありません。</p>
</c:otherwise>
</c:choose>
</body>
</html>
ポイント: fn:escapeXml() で入力値をエスケープしてフォームのvalue属性に安全にセットしています。c:choose のネストで複雑な条件分岐も綺麗に書けます。スクリプトレットを一切使わずに、すべてのロジックをJSTLで表現できています。
まとめ
| 学んだこと | キーワード |
|---|---|
| スクリプトレットの問題 | 可読性低下、XSSリスク、非推奨 |
| EL式の基本 |
${expression}、演算子、暗黙オブジェクト |
| データアクセス | Bean プロパティ、Map、List、スコープ検索順 |
| JSTLコアタグ |
c:out、c:if、c:choose、c:forEach、c:set
|
| JSTLフォーマット |
fmt:formatDate、fmt:formatNumber
|
| JSTL関数 |
fn:length、fn:contains、fn:escapeXml
|
| リファクタリング | スクリプトレット → EL式+JSTL |
次回は フィルターとリスナー を学びます!
シリーズ一覧:Servlet/JSP入門
- 環境構築とはじめてのServlet
- HTTPリクエストとレスポンス
- JSPの基礎
- フォーム処理(GET/POST)
- セッション管理とCookie
- MVCパターン(Servlet + JSP)
- JDBC連携(データベース操作)
- 👉 EL式とJSTL(本記事)
- フィルターとリスナー
- 総合演習:掲示板アプリを作ろう
著者: @kotaro_ai_lab
AI駆動開発やテック情報を毎日発信しています。フォローお気軽にどうぞ!