Spring Bootユーザーのためのサーブレット入門およびチュートリアル

に公開

近年の新人研修では、Javaの基本文法などを学んだ後にいきなりSpring Bootを学ぶため、サーブレットを知らない方も多いと思います。

普段の開発ではサーブレットを意識することはほぼ無いのですが、Spring Bootのカスタマイズなど込み入ったことをするには、サーブレットの知識が必要になります。

この記事では、必要最低限のサーブレットの知識および簡単なアプリケーションの作成方法を解説します。

環境

  • JDK 21
  • Tomcat 10.1.40
  • Jakarta Servlet 6.0
  • macOS 15

用語などの解説

Webにおける静的コンテンツと動的コンテンツ

Webにおける静的コンテンツとは、いつ誰がアクセスしても常に同じ内容がレスポンスされるもののことです。

対して動的コンテンツとは、アクセスする時間・ユーザー・その他の条件によって、異なる内容がレスポンスされるもののことです。

サーブレットとは

サーブレットとは、Javaで動的コンテンツを生成するための技術の仕様です。正式名称はJakarta Servletです。仕様書はWeb上で公開されています(下記)。

https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0

Jakarta EEとは

Jakarta EEは、Java SEを拡張して分散コンピューティングやWebアプリケーションを開発するための仕様の集合です。

Jakarta EEに含まれる仕様は数多くあります。Spring Bootでよく利用されている仕様は次の通りです。

  • Jakarta Servlet
    • サーブレットの仕様
  • Jakarta Validation
    • @NotNull@NotBlank などのアノテーションが仕様で決まっている
  • Jakarta Annotations
    • @PostConstruct@PreDestroy などのアノテーションが仕様で決まっている

他にも数多くの仕様が定められています。興味がある方はJakarta EE公式Webサイトで確認してみてください。

Jakarta EEの歴史

Jakarta EEはもともとJava EEという名前でOracle社が管理していました。しかし2017年に管理をEclipse Foundationに移すことがアナウンスされました。その後、名称がJakarta EEに変更されました。それと併せて商標権の問題から、Java EEに含まれていた javax.* というパッケージ名が jakarta.* に変更されました。

Jakarta EEはのバージョンは下記のとおりです。数年に1回バージョンアップします。

バージョン 説明
2017 Java EE 8 Oracle管理による最後のリリース
2019 Jakarta EE 8 Eclipse Foundation管理による最初のリリース(Java EE 8と内容は同じ)
2020 Jakarta EE 9 パッケージ名の変更( javax.* -> jakarta.*
2021 Jakarta EE 9.1 JKD 11対応
2022 Jakarta EE 10 非推奨機能の削除、新機能の追加
2025 (予定) Jakarta EE 11 新機能の追加

アプリケーションサーバーとは

Jakarta EEを利用しているアプリケーションを動かすためには、一般的にアプリケーションサーバーと呼ばれるサーバーが必要になります。

世の中には有償・無償のアプリケーションサーバー製品が数多くあります。製品によって対応しているJakarta EE仕様が異なります。すなわち、Jakarta EE仕様に全て対応しているアプリケーションサーバー製品もあれば、Jakarta EE仕様の一部にのみ対応しているものもあります。

各アプリケーションサーバー製品自体もJavaで作られています。

Spring Bootでよく利用されるのは、サーブレットなど一部の仕様にのみ対応したアプリケーションサーバーです。これらはオープンソースかつ無償で利用可能です。

  • Tomcat
    • Apache Software Foundationが開発しています。
    • Spring Bootがデフォルトで利用しています。
  • Jetty
    • Eclipse Foundationが開発しています。
  • Undertow
    • JBoss Communityが開発しています。

今回利用するTomcat 10は、Jakarta EE 10で定義されたJakarta Servlet 6.0に対応しています。

上記以外のアプリケーションサーバーでSpring Bootアプリケーションを動かすことも可能です。しかし、あまり多くないと思います。

組み込みサーバーとは

アプリケーションサーバーは基本的にはOSにインストールして使います。そしてアプリケーションはアプリケーションサーバーの内部にデプロイします。

組み込みサーバーは、アプリケーションサーバーと同じ機能をライブラリ(=JARファイル)として実現しています。これによりアプリケーションサーバーのインストールを省略できるので、JDKさえインストール済みであればアプリケーションが動きます。

Spring Bootはデフォルトで組み込みサーバーを使っています。組み込みでないアプリケーションサーバーでもSpring Bootアプリケーションを動かすことは可能ですが、あまり多くはないと思います。

簡単なアプリケーションの作成

Tomcatのインストール

今回は組み込みでないアプリケーションサーバーを使ってみます。

Tomcat公式Webサイトのダウンロード画面から、ZIPファイルをダウンロードします。そしてこのZIPファイルを適当なフォルダに展開してください。今回はMacのユーザーホームフォルダ直下に展開します。

apache-tomcat-(バージョン番号)フォルダの直下には、下記のようなフォルダ・ファイルがあります。その中のwebappsフォルダに、アプリケーションをビルドしたWARファイルを配置します。

binフォルダには、起動やシャットダウンのためのシェルスクリプトが含まれています。展開したままの状態では実行権限が無いので、以下のコマンドで実行権限を追加します。

chmod u+x ~/apache-tomcat-10.1.40/bin/*.sh

サーブレットの主なクラス・インタフェース

  • jakarta.servlet.http.HttpServlet クラス
    • このクラスを継承してサーブレットクラスを作ります。
  • jakarta.servlet.http.HttpServletRequest インタフェース
    • HTTPリクエストを表します。
  • jakarta.servlet.http.HttpServletResponse インタフェース
    • HTTPレスポンスを表します。
  • jakarta.servlet.http.HttpSession インタフェース
    • セッションを表します。
  • jakarta.servlet.Filter インタフェース
    • このインタフェースを実装してフィルターを作ります。

サーブレットの仕様で決まっているものは、上記のようにインタフェースが多いです。これらのインタフェースの実装クラスは、アプリケーションサーバー内にあります。例えばTomcatの場合、org.apache.catalina.connector.RequestFacade クラスHttpServletRequest インタフェースの実装クラスです。

各クラス・インタフェースにどのようなメソッドがあるか興味がある方は、Javadocを確認してみてください。

プロジェクトの作成

Mavenプロジェクトを作成し、pom.xmlを下記のように記述します。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>servlet-sample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>servlet-sample</finalName>
    </build>
</project>

ポイントはいくつかあります。

  • packagingwar にします。これにより、アプリケーションをビルドするとJARファイルではなくWARファイルという形式になります。
  • 依存性には jakarta.servlet-api を追加します。このライブラリに、サーブレット関連のクラス・インタフェースが含まれています。サーブレット関連のクラス・インタフェースはTomcat内部にもあるため、このライブラリをWARファイルには含めないようにするために、スコープは provided にします。
  • finalNameservlet-sample にすることで、ビルドしてできるWARファイルの名前が servlet-sample.war になります。

サーブレットクラスの作成

サーブレットクラスは jakarta.servlet.http.HttpServlet クラスを継承して作成します。

HttpServlet クラスには、HTTPリクエストメソッドのGET・POST・PUT・DELETEメソッドに対応する doGet()doPost()doPut()doDelete() メソッドが定義されています。これらをオーバーライドして処理を記述します。

HelloServlet.java
package com.example;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {
    /**
     * GETメソッドを処理するメソッド
     *
     * @param req  リクエスト
     * @param resp レスポンス
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // リクエストパラメーターnameの値を取得
        String name = req.getParameter("name");
        // HTTPステータスコードを設定
        resp.setStatus(200);
        // Content-Typeレスポンスヘッダーを設定
        resp.setContentType("text/html; charset=UTF-8");
        // レスポンスするHTMLを出力
        PrintWriter writer = resp.getWriter();
        writer.println("""
            <!DOCTYPE html>
            <html lang="ja">
            <head>
                <meta charset="UTF-8">
                <title>結果画面</title>
            </head>
            <body>
                <h1>こんにちは、%sさん!</h1>
            </body>
            </html>
            """.formatted(name));
    }
}

しかし、このままではサーブレットは動きません。アプリケーションサーバーに対して、どのURLでリクエストされたらこのサーブレットクラスを動かすべきなのかを、設定ファイルで伝える必要があるのです。

その設定ファイルがweb.xmlです。web.xmlはsrc/main/webapp/WEB-INFフォルダに配置します。そして下記のように記述します。

web.xml
<?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">
    <servlet>
        <!-- サーブレットに任意の名前を設定 -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- サーブレットクラスの完全修飾名 -->
        <servlet-class>com.example.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <!-- この名前のサーブレットが -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- このURLにリクエストが来たら動く -->
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

サーブレットクラスは複数作成できます。その場合 <servlet> 要素および <servlet-mapping> もサーブレットクラスの数だけ記述することになります。

フィルタークラスの作成

フィルターは、サーブレットの前処理・後処理を実行するものです。複数のサーブレットで共通する処理をフィルターに記述することが多いです。例は以下のとおりです。

  • 文字コードの設定
  • アクセスログの出力
  • 認証・認可などのセキュリティ処理

フィルターは jakarta.servlet.Filter インタフェースを実装し、 doFilter() メソッドをオーバーライドすることで作成します。

今回は文字コードを設定するフィルターを作成します。

CharacterEncodingFilter.java
package com.example;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import java.io.IOException;

/**
 * リクエストパラメーターに対する文字コードを設定するフィルター
 */
public class CharacterEncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
        throws IOException, ServletException {
        // リクエストパラメーターに対する文字コードをUTF-8に設定
        servletRequest.setCharacterEncoding("UTF-8");
        // 次のフィルターまたはサーブレットを実行
        chain.doFilter(servletRequest, servletResponse);
    }
}

ServletRequestServletResponse はぞれぞれ HttpServletRequestHttpServletResponse のスーパーインタフェースです。

chain.doFilter() は、次のフィルターまたはサーブレットを実行するための処理です。

逆に言うと、例えば認証処理を行うフィルターで認証に失敗した場合は、 chain.doFilter() を実行しなければいいのです。

認証フィルターの例
public class AuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
        throws IOException, ServletException {
        // 認証処理を実行
        ...
        if (認証が失敗したら) {
            // chain.doFilter()せずに例外をスロー
            throw new AuthenticationException();
        }
        // 認証が成功したら、次のフィルターまたはサーブレットを実行
        chain.doFilter(servletRequest, servletResponse);
    }
}

サーブレットと同様、このままではフィルターは動きません。アプリケーションサーバーに対して、どのURLでリクエストされたらこのフィルタークラスを動かすべきなのかを、web.xmlに記述する必要があります(下記)。

web.xml
<?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">
    <servlet>
        <!-- サーブレットに任意の名前を設定 -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- サーブレットクラスの完全修飾名 -->
        <servlet-class>com.example.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <!-- この名前のサーブレットが -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- このURLにリクエストが来たら動く -->
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

    <!-- コレを追加 -->
    <filter>
        <!-- フィルタークラスに任意の名前を設定 -->
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- フィルタークラスの完全修飾名 -->
        <filter-class>com.example.CharacterEncodingFilter</filter-class>
    </filter>
    <filter-mapping>
        <!-- この名前のフィルターが -->
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- このURLにリクエストが来たら動く(/*の場合は全URL) -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

フィルタークラスは複数作成できます。その場合 <filter> 要素および <filter-mapping> もサーブレットクラスの数だけ記述することになります。その場合、 <filter-mapping> 要素が書かれている順番に実行されます。

入力画面の作成

HTML画面はsrc/main/webapp直下に作成します。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>入力画面</title>
</head>
<body>
<h1>入力画面</h1>
<form action="/servlet-sample/hello" method="get"> <!-- 最初に/servlet-sampleが付くことに注意(後ほど説明) -->
    <label for="name">名前:</label>
    <input type="text" id="name" name="name">
    <input type="submit" value="送信">
</form>
</body>
</html>

アプリケーションのビルド

以下のコマンドでアプリケーションをビルドします。

mvn clean package

すると、targetフォルダ直下にservlet-sample.warが作成されます。このファイルの正体は、下図のような構成になっているZIPファイルです。

アプリケーションサーバーへのデプロイ

servlet-sample.warをTomcatのwebappsフォルダ直下にコピーすれば、Tomcatへのデプロイは完了です。

cp target/servlet-sample.war ~/apache-tomcat-10.1.40/webapps/

Tomcatの起動

~/apache-tomcat-10.1.40/bin/startup.sh

起動するとWARファイルが展開されます。そして、アプリケーションがHTTPリクエストを受け付けられる状態になります。

ブラウザでのアクセス

ブラウザで http://localhost:8080/servlet-sample/index.html にアクセスしてください。名前に適当な名前を入力後、送信ボタンをクリックしてください。

そうすると http://localhost:8080/servlet-sample/hello に遷移し「こんにちは、◯◯さん!」と表示されます。これは HelloServlet が動的に生成したHTMLを表示しています。

URL内の /servlet-sampleコンテキストパスと呼ばれる、アプリケーションを識別するためのパスです(WARファイルを展開されたアプリケーションのフォルダ名で決まります)。実は1つのアプリケーションサーバーで複数のアプリケーションを動かすことができるため、識別のためにこのコンテキストパスが必要なのです。

Spring Bootのように組み込みサーバーを使っている場合、1つのアプリケーションサーバーで複数のアプリケーションを動かすことは無いため、コンテキストパスは不要です。

Tomcatの停止

~/apache-tomcat-10.1.40/bin/shutdown.sh

アプリケーションを削除したい場合は、Tomcatを停止後にwebappsフォルダからWARファイルおよび展開されたフォルダを削除してください。

rm -rf ~/apache-tomcat-10.1.40/webapps/servlet-sample
rm -rf ~/apache-tomcat-10.1.40/webapps/servlet-sample.war

最後に

ここまで、サーブレットの概論および基本的なアプリケーションの作り方を解説してきました。もう少しサーブレットを学びたい方は基礎からのサーブレット/JSPを読んでみてください。

加えて、今回は解説しませんでしたが大切な内容として HttpSession があります。これについては👇️のスライドを参照してください。

https://www.docswell.com/s/MasatoshiTada/ZYWP29-spring-boot-security#p34

Discussion