サーブレット(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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?