- JAVA
Javaサーブレット入門|JSPとの違いとTomcat 10での動かし方
サーブレット(Servlet)とは、HTTPリクエストを受け取り、レスポンスを返すJavaのクラスです。 ブラウザから届いたURLやフォームの値をJavaのコードで受け取り、処理した結果をHTMLなどで返します。JSPは「HTMLの中にJavaの値を埋め込むテンプレート」で、サーブレットが処理を、JSPが表示を受け持つのが基本の分担です。
この記事では、jakarta.servlet に対応した Tomcat 10.1 で、HelloServlet → フォーム処理(doGet/doPost)→ JSPへのforward を最小構成で動かします。コードはすべて Java 21.0.10 と Tomcat 10.1.55 で実行し、HTTPレスポンスをそのまま掲載しました。参考書やサンプルで多い javax.servlet のコードを Tomcat 10 に載せたときの 404・ClassNotFoundException・NoClassDefFoundError も実際に再現し、直し方を順に示します。
サーブレットとJSPの早見表
| 知りたいこと | 答え |
|---|---|
| サーブレットとは | HttpServlet を継承し、doGet / doPost でリクエストを処理するJavaクラス |
| JSPとは | HTMLに ${値} などを書けるテンプレート。初回アクセス時にサーブレットへ変換されて動く |
| 役割の分担 | サーブレット=入力の受け取りと処理、JSP=画面の表示 |
| 動かす場所 | Tomcat などのサーブレットコンテナ(Webアプリケーションサーバー) |
| パッケージ名 | Tomcat 10以降は jakarta.servlet、Tomcat 9までは javax.servlet |
| URLとクラスの対応づけ | @WebServlet("/hello") か web.xml の <servlet-mapping>(どちらか片方) |
| クラスの置き場所 | WEB-INF/classes/ 以下(パッケージ階層つき) |
| JSPの置き場所 | 直接開かせたくないものは WEB-INF/ 以下に置き、サーブレットから forward する |
| URLの形 | http://localhost:8080/{コンテキストパス}/{URLパターン} |
| フォームの値を読む | request.getParameter("name") |
| JSPへ値を渡す | request.setAttribute("key", 値) → forward → JSPで ${key} |
| 日本語の文字化け対策 | リクエストは setCharacterEncoding("UTF-8")、レスポンスは setContentType("text/html; charset=UTF-8") |
| 画面を切り替える | 同じリクエスト内で渡す=forward、別のURLへ移動させる=sendRedirect |
Tomcat のバージョンと仕様の対応は次のとおりです(2026-10-02 に Apache Tomcat 公式の「Which Version」ページで確認)。
| Tomcat | Servlet仕様 | JSP仕様 | パッケージ | 必要なJava | 確認時点の最新版 |
|---|---|---|---|---|---|
| 9.0.x | 4.0 | 2.3 | javax.servlet |
8 以上 | 9.0.122 |
| 10.1.x | 6.0 | 3.1 | jakarta.servlet |
11 以上 | 10.1.60 |
| 11.0.x | 6.1 | 4.0 | jakarta.servlet |
17 以上 | 11.0.26 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaサーブレット入門|JSPとの違いとTomcat 10での動かし方」
サーブレットが動く仕組み
サーブレットは単体では起動しません。Tomcat などの「サーブレットコンテナ」がHTTP通信を受け持ち、URLに対応するサーブレットのメソッドを呼び出します。 main メソッドを書かないのはこのためです。
sequenceDiagram
participant B as ブラウザ
participant T as Tomcat(コンテナ)
participant S as TaskServlet
participant J as result.jsp
B->>T: POST /hello-app/task(title=議事録を書く)
T->>S: doPost(request, response)
S->>S: getParameter で値を取り出す
S->>J: setAttribute して forward
J-->>T: HTML を生成
T-->>B: 200 OK + HTML
サーブレットの一生(ライフサイクル)は次の3段階です。
| 段階 | メソッド | 呼ばれるタイミング | 回数 |
|---|---|---|---|
| 初期化 | init() |
最初のリクエスト時(または起動時) | 1回 |
| 処理 | service() → doGet() / doPost() など |
リクエストのたび | 毎回 |
| 終了 | destroy() |
アプリの停止時 | 1回 |
サーブレットのインスタンスは1つだけ作られ、複数のリクエストで共有されます。 そのため、利用者ごとに変わる値をフィールド(インスタンス変数)に持たせると、別のリクエストと値が混ざります。リクエストごとの値は、メソッド内のローカル変数か request の属性に置きます。
ディレクトリ構成を確認する
Webアプリケーションは、決まったフォルダ構成で配置します。今回作る hello-app の全体像です。
hello-app/
├── src/
│ └── com/example/
│ ├── HelloServlet.java … ソース(配備はしない)
│ └── TaskServlet.java
└── webapp/ … ここから下が配備する中身
├── index.html … ブラウザから直接開ける
└── WEB-INF/ … ブラウザから直接は開けない領域
├── web.xml … 配備記述子(設定ファイル)
├── classes/ … コンパイル済みクラス
│ └── com/example/
│ ├── HelloServlet.class
│ └── TaskServlet.class
├── lib/ … 追加ライブラリの jar(今回は空)
└── views/
├── form.jsp
└── result.jsp
| 場所 | 置くもの | ブラウザから直接開けるか |
|---|---|---|
webapp/ 直下 |
HTML、CSS、画像、公開してよいJSP | 開ける |
WEB-INF/web.xml |
アプリ全体の設定 | 開けない |
WEB-INF/classes/ |
.class ファイル(パッケージ階層つき) |
開けない |
WEB-INF/lib/ |
アプリが使う .jar |
開けない |
WEB-INF/views/ など |
サーブレット経由でだけ表示するJSP | 開けない |
WEB-INF は大文字で、つづりも固定です。 Web-inf などにするとクラスが見つからず 404 になります。
HelloServletを作って動かす
1. サーブレットを書く
src/com/example/HelloServlet.java です。import が jakarta.servlet で始まっている点が Tomcat 10 以降の書き方です。
package com.example;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDate;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><head><title>Hello</title></head><body>");
out.println("<h1>こんにちは、サーブレット</h1>");
out.println("<p>今日は " + LocalDate.of(2026, 10, 2) + " です。</p>");
out.println("</body></html>");
}
}
| 行 | 意味 |
|---|---|
extends HttpServlet |
HTTP用のサーブレットの土台を継承する |
@WebServlet("/hello") |
/hello というURLパターンにこのクラスを対応づける |
doGet(...) |
GETリクエスト(URLを開く、リンクを押す)で呼ばれる |
request |
届いたリクエスト(パラメータ、ヘッダー、セッションなど) |
response |
返すレスポンス(ステータス、ヘッダー、本文) |
setContentType(...) |
本文の種類と文字コードを伝える。getWriter() より前に呼ぶ |
2. web.xmlを書く
webapp/WEB-INF/web.xml です。@WebServlet を使う場合、サーブレットの登録を書く必要はありません。ここでは文字コードとトップページだけを設定します。
<?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">
<display-name>hello-app</display-name>
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
version="6.0" は Servlet 6.0(Tomcat 10.1)用です。Tomcat 11 では web-app_6_1.xsd と version="6.1" が用意されています(公式ドキュメントで確認。6.0 のままでも読み込まれます)。
3. コンパイルする
サーブレットAPIは JDK に含まれていないため、Tomcat に入っている servlet-api.jar をクラスパスに指定します。
# Tomcat を展開したフォルダを CATALINA_HOME とする
javac -cp "$CATALINA_HOME/lib/servlet-api.jar" \
-d webapp/WEB-INF/classes \
src/com/example/*.java
今回の検証では、Ubuntu 24.04 のパッケージ libtomcat10-java(Tomcat 10.1.55)に入っている /usr/share/java/tomcat10-servlet-api.jar を指定しました。コンパイル後のファイルは次のとおりです。
webapp/WEB-INF/classes/com/example/HelloServlet.class
webapp/WEB-INF/classes/com/example/TaskServlet.class
webapp/WEB-INF/views/form.jsp
webapp/WEB-INF/views/result.jsp
webapp/WEB-INF/web.xml
webapp/index.html
Maven を使う場合は、次の依存関係を provided(コンパイル時だけ使い、war には含めない)で追加します。Tomcat 11 では 6.1.0 を指定します(Maven でのビルドは今回は未実行で、公式の座標を確認して記載しています)。
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
4. Tomcatに配備して起動する
配布版の Tomcat を使う場合は、webapp フォルダの中身を hello-app という名前で webapps に置きます。フォルダ名がそのままコンテキストパス(URLの最初の部分)になります。
cp -r webapp "$CATALINA_HOME/webapps/hello-app"
"$CATALINA_HOME/bin/startup.sh" # Windows は bin\startup.bat
# ブラウザで http://localhost:8080/hello-app/hello を開く
"$CATALINA_HOME/bin/shutdown.sh" # 停止
この記事の検証環境では配布版のダウンロードができなかったため、同じ Tomcat 10.1.55 の jar を使った「組み込み起動」で確認しました(配布版の startup.sh での起動は未実行で、手順は公式ドキュメントに沿って記載しています)。組み込み起動に使ったコードは次のとおりで、webapp フォルダを /hello-app として配備します。
import java.io.File;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public class Launcher {
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(args[0]);
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir(new File("tomcat-work").getAbsolutePath());
tomcat.setPort(port);
tomcat.getConnector();
// webapp フォルダを /hello-app として配備(web.xml と @WebServlet を読む)
Context ctx = tomcat.addWebapp("/hello-app", new File(args[1]).getAbsolutePath());
tomcat.start();
System.out.println("started: http://localhost:" + port + ctx.getPath() + "/");
tomcat.getServer().await();
}
}
# クラスパスに Tomcat の jar 一式と JSP 用コンパイラ(eclipse-jdt-core)を指定
java -cp "$CP" Launcher.java 7410 webapp
起動ログ(抜粋)です。
INFO: Initializing ProtocolHandler ["http-nio-7410"]
INFO: Starting Servlet engine: [Apache Tomcat/10.1.55 (Ubuntu)]
INFO: Starting ProtocolHandler ["http-nio-7410"]
started: http://localhost:7410/hello-app/
5. アクセスして確認する
curl -i http://localhost:7410/hello-app/hello
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 161
<!DOCTYPE html>
<html><head><title>Hello</title></head><body>
<h1>こんにちは、サーブレット</h1>
<p>今日は 2026-10-02 です。</p>
</body></html>
URLは「ポート/コンテキストパス/URLパターン」の3つで決まります。
| 部分 | 今回の値 | 決まる場所 |
|---|---|---|
| ポート | 7410(配布版の既定は 8080) |
Tomcat の設定 |
| コンテキストパス | /hello-app |
webapps に置いたフォルダ名(war名) |
| URLパターン | /hello |
@WebServlet または web.xml |
フォームの値を受け取ってJSPに表示する
次に、入力フォーム → 受け取り → 結果画面という流れを作ります。同じURL /task で、GETならフォーム表示、POSTなら登録処理という分け方です。
サーブレット(doGetとdoPost)
src/com/example/TaskServlet.java
package com.example;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/task")
public class TaskServlet extends HttpServlet {
// GET: 入力フォームを表示する
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/views/form.jsp").forward(request, response);
}
// POST: 送られた値を受け取り、結果画面(JSP)に渡す
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8"); // getParameter より前に呼ぶ
String title = request.getParameter("title");
String owner = request.getParameter("owner");
if (title == null || title.isBlank()) {
request.setAttribute("error", "タスク名を入力してください。");
request.getRequestDispatcher("/WEB-INF/views/form.jsp").forward(request, response);
return;
}
request.setAttribute("title", title);
request.setAttribute("owner", owner);
request.setAttribute("length", title.length());
request.getRequestDispatcher("/WEB-INF/views/result.jsp").forward(request, response);
}
}
入力フォームのJSP
webapp/WEB-INF/views/form.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head><title>タスク登録</title></head>
<body>
<h1>タスク登録</h1>
<p style="color:#b00">${error}</p>
<form action="${pageContext.request.contextPath}/task" method="post">
<label>タスク名 <input type="text" name="title"></label>
<label>担当 <input type="text" name="owner"></label>
<button type="submit">登録</button>
</form>
</body>
</html>
結果画面のJSP
webapp/WEB-INF/views/result.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head><title>登録結果</title></head>
<body>
<h1>登録しました</h1>
<ul>
<li>タスク名: ${title}</li>
<li>担当: ${owner}</li>
<li>文字数: ${length}</li>
</ul>
<p><a href="${pageContext.request.contextPath}/task">続けて登録する</a></p>
</body>
</html>
| 書き方 | 意味 |
|---|---|
<%@ page ... %> |
ページ全体の設定(ディレクティブ)。文字コードをここで指定する |
${title} |
EL式。request.setAttribute("title", ...) で入れた値を表示する |
${pageContext.request.contextPath} |
コンテキストパス(今回は /hello-app)。URLを固定で書かずに済む |
<form method="post"> |
送信すると doPost が呼ばれる |
name="title" |
request.getParameter("title") で読む名前 |
実行結果
フォームの表示(GET)です。
curl -i http://localhost:7410/hello-app/task
HTTP/1.1 200
Set-Cookie: JSESSIONID=E0DEF2E66A5E36D5443584A10E84C039; Path=/hello-app; HttpOnly
Content-Type: text/html;charset=UTF-8
Content-Length: 357
<!DOCTYPE html>
<html>
<head><title>タスク登録</title></head>
<body>
<h1>タスク登録</h1>
<p style="color:#b00"></p>
<form action="/hello-app/task" method="post">
<label>タスク名 <input type="text" name="title"></label>
<label>担当 <input type="text" name="owner"></label>
<button type="submit">登録</button>
</form>
</body>
</html>
${pageContext.request.contextPath} が /hello-app に置き換わっています。${error} は値がないので空文字になりました。
フォームの送信(POST)です。
curl -i -X POST --data-urlencode 'title=議事録を書く' --data-urlencode 'owner=佐藤' \
http://localhost:7410/hello-app/task
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 280
<!DOCTYPE html>
<html>
<head><title>登録結果</title></head>
<body>
<h1>登録しました</h1>
<ul>
<li>タスク名: 議事録を書く</li>
<li>担当: 佐藤</li>
<li>文字数: 6</li>
</ul>
<p><a href="/hello-app/task">続けて登録する</a></p>
</body>
</html>
タスク名を空で送ると、フォームに戻ってメッセージが出ます。
<p style="color:#b00">タスク名を入力してください。</p>
初回アクセスのあと、Tomcat の作業フォルダに JSP から生成されたJavaソースとクラスができていました。 JSPが「サーブレットに変換されて動く」ことを確認できます。
tomcat-work/work/Tomcat/localhost/hello-app/org/apache/jsp/WEB_002dINF/views/
form_jsp.class
form_jsp.java
result_jsp.class
result_jsp.java
WEB-INF 以下のJSPを直接開こうとすると、404 が返りました。サーブレットを通らずに画面だけ表示されることを防げます。
$ curl -i http://localhost:7410/hello-app/WEB-INF/views/form.jsp
HTTP/1.1 404
表示する値はエスケープする
EL式の ${title} は、値をそのままHTMLに書き出します。利用者が入力した値を表示する画面では、JSTLの <c:out value="${title}" /> か ${fn:escapeXml(title)} を使い、< や > を文字として表示します。Tomcat 10 以降で JSTL を使うには、jakarta.servlet.jsp.jstl-api と実装の jar(3.0 系)を WEB-INF/lib に置き、<%@ taglib prefix="c" uri="jakarta.tags.core" %> と書きます(JSTL は今回未実行で、公式ドキュメントで確認した内容です)。
doGetとdoPostの使い分け
| 項目 | doGet | doPost |
|---|---|---|
| 呼ばれる場面 | URLを開く、リンク、method="get" のフォーム |
method="post" のフォーム |
| 値の送られ方 | URLの ?title=...(クエリ文字列) |
リクエスト本文 |
| 向いている処理 | 表示、検索、絞り込み | 登録、更新、削除 |
| ブックマーク・再読み込み | そのまま再表示できる | 再送信の確認が出る |
| 文字コードの指定先 | URLのデコードはコンテナ側の設定(Tomcat は既定でUTF-8) | setCharacterEncoding("UTF-8") または web.xml |
対応するメソッドを実装していないHTTPメソッドで呼ぶと、405 が返ります。 doGet だけの HelloServlet に POST した結果です。
$ curl -i -X POST http://localhost:7410/hello-app/hello
HTTP/1.1 405
...
HTTP Status 405 – Method Not Allowed
Message: HTTP method POST is not supported by this URL
フォームの method と、サーブレット側で実装したメソッドがそろっているかを確認します。
forwardとリダイレクトの違い
画面を切り替える方法は2つあります。
| 項目 | forward | リダイレクト(sendRedirect) |
|---|---|---|
| 書き方 | request.getRequestDispatcher("/WEB-INF/views/x.jsp").forward(request, response) |
response.sendRedirect(request.getContextPath() + "/task") |
| 動き | サーバー内で処理を引き継ぐ | ブラウザに「次はこのURLへ」と返し、もう一度アクセスさせる |
| ブラウザのURL | 変わらない | 変わる |
request の属性 |
引き継がれる | 引き継がれない(別のリクエストになる) |
| 移動先 | 同じアプリの中(WEB-INF 以下も可) |
どのURLでも可 |
| 向いている場面 | サーブレットの結果をJSPで表示する | 登録後に一覧画面へ移す |
登録処理のあとに forward で結果を表示すると、ブラウザの再読み込みで同じ POST がもう一度送られます。登録・更新のあとはリダイレクトで別のURLへ移す(PRGパターン:Post → Redirect → Get)と、再読み込みしても登録が重なりません。
データを置く4つのスコープ
サーブレットとJSPの間で値を受け渡す入れ物を「スコープ」と呼びます。
| スコープ | 置き方 | 有効な範囲 | 用途の例 |
|---|---|---|---|
| ページ | JSP内の pageContext |
そのJSPの中だけ | 一時的な計算結果 |
| リクエスト | request.setAttribute() |
1回のリクエスト(forward先まで) | 画面に表示する値 |
| セッション | request.getSession().setAttribute() |
同じ利用者の複数リクエスト | ログイン中の会員情報 |
| アプリケーション | getServletContext().setAttribute() |
アプリ全体・全利用者 | 共通の設定値 |
まずはリクエストスコープを使い、複数の画面で持ち回る必要があるものだけセッションに置くと、値の混ざりを避けやすくなります。
web.xmlと@WebServletの違い
URLとサーブレットの対応づけは2通りあります。
web.xml で書く場合:
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
| 項目 | @WebServlet |
web.xml |
|---|---|---|
| 書く場所 | クラスの上 | WEB-INF/web.xml |
| 手軽さ | 1行で済む | 2つの要素を対で書く |
| 一覧性 | クラスごとに分散 | 1ファイルで見渡せる |
| ソースを変えずにURLを変更 | 再コンパイルが必要 | XMLの編集だけで済む |
| 向いている場面 | 学習、小〜中規模 | 設定を1か所で管理したい場合 |
同じURLパターンを両方に書くと、アプリが起動しません。 実際に、web.xml と @WebServlet の両方で /old を指定したときのログです。
Caused by: java.lang.IllegalArgumentException: The servlets named [old] and
[com.example.OldServlet] are both mapped to the url-pattern [/old] which is not permitted
2つのクラスに同じ @WebServlet("/moji") を付けた場合も、同じ種類のメッセージになりました。このとき、そのアプリのURLはすべて 404 になります。 1つのURLを登録する場所は1か所にそろえます。
URLパターンの書き方は次の4種類です。
| 書き方 | 例 | 一致するURL |
|---|---|---|
| 完全一致 | /hello |
/hello だけ |
| パス一致 | /tasks/* |
/tasks/1、/tasks/a/b など |
| 拡張子一致 | *.do |
/x.do、/a/b.do など |
| 既定 | / |
ほかに一致しなかったすべて |
javaxからjakartaへの移行で起きるエラーと直し方
サーブレットのパッケージ名は、Tomcat 9 までが javax.servlet、Tomcat 10 以降が jakarta.servlet です。仕様の管理が Eclipse Foundation に移り、Jakarta EE 9 でパッケージ名が変わりました。クラス名やメソッド名は同じなので、多くの場合は import の書き換えで動きます。
ここでは、次の javax 版サーブレットを Tomcat 10.1.55 で動かそうとしたときの様子を、段階ごとに再現します。
package com.example;
import java.io.IOException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/old")
public class OldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
response.setContentType("text/plain; charset=UTF-8");
response.getWriter().println("old servlet");
}
}
症状1:コンパイルで「package javax.servlet does not exist」
Tomcat 10 の servlet-api.jar を指定してコンパイルした結果です。
src/com/example/OldServlet.java:5: error: package javax.servlet.annotation does not exist
import javax.servlet.annotation.WebServlet;
^
src/com/example/OldServlet.java:6: error: package javax.servlet.http does not exist
import javax.servlet.http.HttpServlet;
^
src/com/example/OldServlet.java:11: error: cannot find symbol
public class OldServlet extends HttpServlet {
^
symbol: class HttpServlet
Tomcat 10 の jar には jakarta.servlet しか入っていないため、javax.servlet が見つかりません。
症状2:コンパイルは通るのに404になる
javax 版の API(servlet-api 4.0.1)でコンパイルしたクラスを、そのまま Tomcat 10.1 に配備した結果です。
$ curl -i http://localhost:7411/hello-app/old
HTTP/1.1 404
起動ログにエラーは出ませんでした。 Tomcat 10 は jakarta.servlet.annotation.WebServlet だけを探すため、javax 版の @WebServlet は読み飛ばされ、URLが登録されないまま起動します。エラーが出ない分、原因に気づきにくい症状です。
症状3:web.xmlで登録するとNoClassDefFoundErrorとClassNotFoundException
同じクラスを web.xml の <servlet> で登録すると、アクセスした時点で 500 になりました。
HTTP/1.1 500
Message: Error instantiating servlet class [com.example.OldServlet]
Root Cause
java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet
Root Cause
java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet
サーバーのログです。
SEVERE: Allocate exception for servlet [old]
java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1377)
OldServlet を読み込もうとした際に、親クラスの javax.servlet.http.HttpServlet が Tomcat 10 の中に存在しないため、この2つの例外が組になって出ます。
遠回りになる対処:javax版のjarをWEB-INF/libに入れる
「クラスが見つからないなら jar を足せばよい」と考えて servlet-api-4.0.1.jar を WEB-INF/lib に入れると、今度は別のエラーになりました。
HTTP/1.1 500
Message: Class [com.example.OldServlet] is not a Servlet
SEVERE: Allocate exception for servlet [old]
java.lang.ClassCastException: class com.example.OldServlet cannot be cast to class
jakarta.servlet.Servlet
OldServlet は javax.servlet.Servlet の一種であり、Tomcat 10 が求める jakarta.servlet.Servlet とは別の型だからです。サーブレットAPIの jar は WEB-INF/lib に入れず、コンテナが持っているものを使います。
直し方:importをjakartaに書き換えて再コンパイルする
sed -i 's/javax\.servlet/jakarta.servlet/g' src/com/example/OldServlet.java
javac -cp "$CATALINA_HOME/lib/servlet-api.jar" -d webapp/WEB-INF/classes src/com/example/OldServlet.java
書き換え後の import です。
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
web.xml の重複登録を外して起動し直すと、200 が返りました。
$ curl -i http://localhost:7411/hello-app/old
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
old servlet
症状と対処の対応表
| 症状 | 出る場面 | 原因 | 対処 |
|---|---|---|---|
package javax.servlet does not exist |
コンパイル時 | Tomcat 10 以降の jar に javax.servlet がない |
import を jakarta.servlet に変える |
| 404(ログにエラーなし) | アクセス時 | javax 版の @WebServlet が読み飛ばされる |
jakarta で再コンパイルする |
NoClassDefFoundError: javax/servlet/http/HttpServlet |
アクセス時 | javax 版でコンパイルしたクラスを配備している |
jakarta で再コンパイルする |
ClassNotFoundException: javax.servlet.http.HttpServlet |
同上(ログ側) | 同上 | 同上 |
ClassCastException: ... cannot be cast to class jakarta.servlet.Servlet |
アクセス時 | javax 版APIの jar を WEB-INF/lib に入れた |
jar を外し、jakarta で再コンパイルする |
are both mapped to the url-pattern |
起動時 | 同じURLを2か所で登録している | 登録を1か所にする |
書き換える対象は Java ソースだけではありません。
| 対象 | 変更前 | 変更後 |
|---|---|---|
| import | javax.servlet.* |
jakarta.servlet.* |
web.xml の名前空間 |
http://xmlns.jcp.org/xml/ns/javaee |
https://jakarta.ee/xml/ns/jakartaee |
| JSTL の taglib | http://java.sun.com/jsp/jstl/core |
jakarta.tags.core |
| 依存ライブラリ | javax.servlet-api |
jakarta.servlet-api |
| 利用しているライブラリ | javax.servlet に依存する版 |
jakarta 対応の版へ更新 |
javax.sql や javax.crypto など、JDK 標準に含まれる javax パッケージは変更の対象外です。 一括置換するときは javax.servlet のように範囲を絞ります。
ソースが手元にない war を動かしたい場合は、Apache Tomcat が提供する「Tomcat Migration Tool for Jakarta EE」で war を変換できます。また Tomcat 10 以降では、webapps-javaee フォルダに置いた war を起動時に自動変換して配備する仕組みがあります(どちらも今回は未実行で、公式ドキュメントで確認した内容です)。
文字化けの原因と対策
文字化けは、送る側と受け取る側で文字コードの解釈が異なるときに起きます。サーブレットでは「レスポンス」と「リクエスト」の2か所を確認します。
レスポンスの文字化け
setContentType を呼ばずに日本語を書いたサーブレットです。
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// setContentType を呼ばずに日本語を書く
response.getWriter().println("こんにちは");
}
$ curl -i http://localhost:7412/hello-app/moji
HTTP/1.1 200
Content-Length: 6
?????
文字コードの指定がないと、getWriter() は ISO-8859-1 で出力します。日本語を表せないため、5文字が ? に置き換わりました。getWriter() より前に response.setContentType("text/html; charset=UTF-8") を呼ぶと、最初の HelloServlet のように正しく表示されます。
リクエスト(フォームの値)の文字化け
web.xml の設定も setCharacterEncoding もない状態で、POST の値を取り出した結果です。
// setCharacterEncoding を呼ばずに取り出す
String title = request.getParameter("title");
response.setContentType("text/plain; charset=UTF-8");
response.getWriter().println("title=" + title + " (" + title.length() + "文字)");
$ curl -i -X POST --data-urlencode 'title=議事録を書く' http://localhost:7412/hello-app/moji
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
title=è°äºé²ãæ¸ã (18文字)
UTF-8 の18バイトを1バイトずつ文字として解釈したため、6文字のはずが18文字になっています。対策は次のどちらかです。
| 対策 | 書く場所 | 特徴 |
|---|---|---|
request.setCharacterEncoding("UTF-8") |
各サーブレットの getParameter より前 |
そのサーブレットだけに効く |
<request-character-encoding>UTF-8</request-character-encoding> |
WEB-INF/web.xml |
アプリ全体に効く(Servlet 4.0 以降) |
先ほどの TaskServlet は両方を入れているため、「議事録を書く」「6文字」と正しく表示されました。
なお、この再現は組み込み起動で、Tomcat 全体の既定設定(conf/web.xml)を読み込まない状態で行っています。配布版の Tomcat 10 以降は conf/web.xml に UTF-8 の指定が入っているため、何も書かなくても化けない場合があります。動かす環境に左右されないよう、アプリ側の web.xml に明示しておくと確実です。
文字コードのチェックリスト
| 確認する場所 | 設定 |
|---|---|
| Java ソースの保存形式 | UTF-8(javac -encoding UTF-8。Java 18 以降は既定が UTF-8) |
| JSP の先頭 | <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> |
| サーブレットのレスポンス | response.setContentType("text/html; charset=UTF-8") を getWriter() より前に |
| POST の受け取り | request.setCharacterEncoding("UTF-8") を getParameter() より前に |
| アプリ全体 | web.xml の <request-character-encoding> と <response-character-encoding> |
| 静的な HTML | <meta charset="UTF-8"> |
404になったときの確認順
サーブレットの学習で最も多く出会うのが 404 です。上から順に確認すると原因を絞り込めます。
| 順番 | 確認すること | 今回の実例 |
|---|---|---|
| 1 | コンテキストパスが入っているか | /hello(パスなし)は 404、/hello-app/hello は 200 |
| 2 | 大文字・小文字が合っているか | /hello-app/Hello は 404 |
| 3 | URLパターンが / で始まっているか |
@WebServlet("hello") は起動時にエラーになる |
| 4 | .class が WEB-INF/classes/ のパッケージ階層にあるか |
WEB-INF/classes/com/example/HelloServlet.class |
| 5 | import が jakarta.servlet か |
javax 版は Tomcat 10 でログなしの 404 |
| 6 | 起動ログに SEVERE がないか |
URLパターンの重複でアプリ全体が 404 |
| 7 | WEB-INF 以下を直接開いていないか |
/WEB-INF/views/form.jsp は 404(仕様どおり) |
| 8 | 再コンパイル後に再配備・再起動したか | クラスを差し替えたら再読み込みする |
1〜2と5〜7は今回の環境で実際に 404 になることを確認しました。3は仕様(URLパターンは / か *. で始める)に基づく記載です。
ServletとJSPとSpring Bootの役割比較
| 項目 | Servlet | JSP | Spring Boot(Spring MVC) |
|---|---|---|---|
| 正体 | Javaクラス | HTML中心のテンプレート(内部でサーブレットに変換) | サーブレットの上に作られたフレームワーク |
| 主な役割 | リクエストの受け取りと処理 | 画面の表示 | ルーティング、入力チェック、DI、設定の自動化 |
| 書くもの | doGet / doPost |
HTML + EL + タグ | @Controller / @RestController のメソッド |
| URLの対応づけ | @WebServlet / web.xml |
ファイルの場所、または forward | @GetMapping / @PostMapping |
| パラメータの取得 | request.getParameter() で文字列を取り出す |
${param.name} |
引数やオブジェクトに自動で詰める |
| 起動方法 | Tomcat に配備する | Tomcat に配備する | main から起動(Tomcat を内蔵) |
| 学ぶと分かること | HTTP とコンテナの仕組み | サーバー側での画面生成 | 効率のよいアプリ開発の進め方 |
Spring Boot の内部では DispatcherServlet という1つのサーブレットがすべてのリクエストを受け取り、各コントローラーに振り分けています。 Spring Boot 3 以降は jakarta.servlet を使うため、この記事の javax → jakarta の話はそのまま Spring Boot 2 → 3 の移行にも当てはまります。サーブレットを先に学んでおくと、HttpServletRequest、フィルター、セッション、文字コードといった Spring の土台が理解しやすくなります。
| やりたいこと | 選ぶもの |
|---|---|
| Web アプリの仕組みを基礎から学ぶ | Servlet + JSP |
| 既存の Servlet / JSP アプリを保守する | Servlet + JSP(Tomcat の版に合わせたパッケージ) |
| 新しく Web API や業務アプリを作る | Spring Boot |
| 画面をサーバー側で生成する(新規) | Spring Boot + Thymeleaf など |
Java 21とJava 25での違い
この記事のコードは Java 21.0.10 で実行しました。Java 25(2025年9月リリースの LTS)での確認事項は次のとおりです(Java 25 は今回の環境では未実行で、公式情報で確認した内容です)。
| 項目 | 内容 |
|---|---|
| Tomcat 10.1 の動作条件 | Java 11 以上。Java 21・25 のどちらでも条件を満たす |
| Tomcat 11 の動作条件 | Java 17 以上 |
| この記事のコード | バージョン固有の構文は使っていないため、書き換えは不要 |
| ソースの文字コード | Java 18 以降は既定が UTF-8 |
| サーブレットAPI | JDK ではなく Tomcat 側の jar で決まる(JDK を上げても javax / jakarta は変わらない) |
javax か jakarta かを決めるのは JDK のバージョンではなく、Tomcat(サーブレットコンテナ)のバージョンです。
よくある質問
Q. サーブレットとJSPはどちらから学ぶとよいですか?
A. サーブレットから始めると理解が進みます。JSP は内部でサーブレットに変換されて動くため、request や response の意味が分かっていると、JSP の動きも説明できるようになります。この記事の順番(HelloServlet → フォーム → JSP へ forward)がそのまま学習の順番になります。
Q. Tomcat 9 と Tomcat 10 のどちらを使えばよいですか?
A. これから学ぶなら Tomcat 10.1 か 11 が合っています。Spring Boot 3 以降や新しいライブラリが jakarta.servlet を前提にしているためです。javax.servlet で書かれた既存アプリをそのまま動かす場合は Tomcat 9 を使い、移行するときに import を書き換えます。
Q. 参考書のサンプルが javax.servlet で書かれています。どうすればよいですか?
A. Tomcat 10 以降で動かすなら、javax.servlet を jakarta.servlet に置き換えてからコンパイルします。クラス名とメソッド名は同じなので、この記事の sed の例のように import の変更だけで動くことがほとんどです。JSTL を使っている場合は taglib の URI と jar も合わせて更新します。
Q. JSP の中に Java のコード(スクリプトレット)を書いてもよいですか?
A. <% ... %> で書くこと自体はできます。ただ、処理と表示が混ざると読みにくくなるため、処理はサーブレットに書き、JSP では EL 式(${...})と JSTL で表示だけを行う分け方が広く使われています。
まとめ
- サーブレットは HTTP リクエストを処理する Java クラス、JSP は表示用のテンプレート。処理はサーブレット、表示は JSP に分ける
- Tomcat 10 以降は
jakarta.servlet、Tomcat 9 まではjavax.servlet。決めるのは JDK ではなくコンテナの版 - URL は「ポート/コンテキストパス/URLパターン」で決まる。クラスは
WEB-INF/classes、非公開の JSP はWEB-INF以下に置く - 値の受け渡しは
getParameter→setAttribute→forward→${key}の流れ javax版を Tomcat 10 に載せると、ログなしの 404、NoClassDefFoundError、ClassNotFoundException になる。import を書き換えて再コンパイルする- 文字化け対策は、
setContentTypeとsetCharacterEncodingを先に呼ぶこと、そしてweb.xmlに UTF-8 を明示すること - Spring Boot もサーブレットの上で動く。サーブレットの理解は、そのままフレームワークの理解につながる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaサーブレット入門|JSPとの違いとTomcat 10での動かし方」
株式会社RJCでは、サーブレットやJSPといったWebアプリケーションの基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。