Javaのフォワードとリダイレクトの違いとは?画面遷移とデータの受け渡しを理解する

最終更新日

Schoo Java入門 中級 第5回 サーバーサイドアプリケーションの実装基礎

処理結果や入力エラーを、そのリクエストのままJSPに表示したいならフォワード。登録処理を終え、ブラウザに表示用のURLへアクセスし直してもらいたいならリダイレクト。この違いが分かると、「画面は開いたのに値がない」「更新したらもう一度送信された」という疑問を整理できます。

どちらも別の画面を表示するために使えますが、フォワードはサーバー内で処理を引き継ぐ仕組み、リダイレクトはブラウザへ別URLへのアクセスを指示する仕組みです。見た目ではなく、誰が次の処理を始めるのかに注目しましょう。

リダイレクトの移動先は、同じサイト内に限りません。別のサーバーで動く外部サイトへ移動させることもできます。まず同じアプリ内の例でリクエストの違いを確認し、外部URLを指定する書き方も紹介します。

Schoo「Java入門 中級」第5回「サーバーサイドアプリケーションの実装基礎」のServlet・JSP連携を補足する記事です。今回は、フォワードと比較しながらリダイレクトの使いどころまで確認します。

最初に結論:同じリクエストか、新しいリクエストか

比較する点フォワードリダイレクト
処理を進めるのはサーバー内のServletコンテナ指示を受け取ったブラウザ
リクエスト同じリクエストの処理を引き継ぐブラウザから新しいリクエストが来る
requestの属性引き継ぎ先で利用できる自動では引き継がれない
ブラウザのURLforwardによっては変わらない指定した移動先になる
今回使うメソッドRequestDispatcher.forward()response.sendRedirect()

requestの属性とは、setAttribute() で名前を付けて保存するサーバー側のデータです。リクエストスコープの値は、そのリクエストの処理中に共有します。別のリクエストになるリダイレクト先には、自動でコピーされません。

移動先の範囲フォワードリダイレクト
同じアプリのServlet・JSPサーバー内で処理を引き継げるブラウザにアクセスし直してもらえる
外部サイトのURL外部URLへブラウザを移動させる仕組みではないhttps://から始まる完全なURLを指定できる

イラストで比較する:ブラウザはいつアクセスするのか

上段はフォワード。同じサーバー内でServletからJSPへ同じrequestを引き継ぐ。中段は同じサイトへのリダイレクト。ブラウザが移動先URLを受け取り、自分のサーバーのJSPへ新しくGETする。下段は外部サイトへのリダイレクト。ブラウザが外部サーバーへ新しくGETする。どちらのリダイレクトも元のrequestの属性は引き継がれない。
上段はフォワード、中段は同じサイトへのリダイレクト、下段は外部サイトへのリダイレクトです。この例のリクエスト数は、上段が1回、中段・下段がそれぞれ2回です。画像やCSSの取得は数えていません。画像を選択すると拡大できます。

まず上段と中段を比較してください。どちらも自分のサーバーのJSPを表示しますが、フォワードはサーバー内で処理を引き継ぎ、リダイレクトはいったんブラウザへ移動先を返します。同じサーバーへの移動でも、リダイレクトでは新しいリクエストになることが違いです。

次に中段と下段を見ると、どちらも「ブラウザへ移動先を返す」「ブラウザがアクセスし直す」という矢印になっています。移動先は自分のサーバーでも外部サーバーでも構いません。フォワードとリダイレクトの違いは、移動先が内部か外部かではなく、ブラウザが新しいリクエストを送るかどうかです。

後の比較コードで購入金額12000を入力すると、どちらのボタンでも割引額500をrequestへ保存してから、同じ結果JSPへ進みます。リダイレクト側は「割引額がありません。」と表示されます。これは計算の失敗ではなく、値を保存したrequestとは別のrequestでJSPが動くためです。

フォワード:値を持ったままJSPへ引き継ぐ

request.setAttribute("discount", discount);
request.getRequestDispatcher("/navigation-result.jsp")
        .forward(request, response);

JSPは request.getAttribute("discount") で割引額を取り出せます。Servletが値を用意し、JSPがHTMLにする役割分担です。JSPもサーバー側で動き、ブラウザへは生成したHTMLを返します。

この例でフォームの送信先が /lesson05/discount-navigation なら、結果表示後のアドレス欄もそのURLのままです。JSPの内容が表示されても、URLが /lesson05/navigation-result.jsp に変わるわけではありません。

フォワードは、POSTをGETへ変える処理ではありません。元がPOSTなら、JSPもそのPOSTの処理として実行されます。また、forwardはJavaのreturnとは異なるメソッド呼び出しなので、戻ったあとに不要な処理が続かないよう、分岐の途中ではreturnを使います。

リダイレクト:移動先をブラウザに伝える

response.sendRedirect(request.getContextPath()
        + "/navigation-result.jsp");
return;

Java 17・Tomcat 10.1で使うこのsendRedirectは、移動先を示す Location ヘッダーとHTTPステータス302を返します。通常のブラウザでフォームをPOST送信した今回の例では、その指示を受けたブラウザが移動先へGETでアクセスします。

移動先が /lesson05/navigation-result.jsp なら、アドレス欄もそのURLになります。元のPOSTで保存した属性や、フォームのPOSTデータが、そのまま新しいGETへ渡るわけではありません。

ただし、すべてのリダイレクトが必ずGETになるわけではありません。HTTPにはメソッドを維持する307・308や、POST後にGETで参照させる意図を明示できる303もあります。本記事の比較は、上記のsendRedirectによる302と通常のブラウザのフォーム送信を対象にしています。

外部サイトへリダイレクトすることもできる

たとえば、自分のアプリで処理を終えたあとに、別サイトの案内ページへ移動してもらいたい場合です。移動先に https:// とドメインを含む完全なURLを指定します。次は説明用の外部URLです。

response.sendRedirect("https://www.example.com/");
return;
  1. 自分のアプリが、移動先URLをLocationヘッダーに入れてブラウザへ返す。
  2. ブラウザが、そのURLの外部サーバーへ新しいリクエストを送る。
  3. 外部サーバーが応答し、ブラウザにそのサイトが表示される。

自分のServletが外部サイトのHTMLを取得して表示するのではありません。アクセスし直すのはブラウザです。先ほどのイラストの下段が、この外部サイトへの移動を表しています。同じサイト内でも外部サイトでも、「いったんブラウザへ移動先を返す」という仕組みは同じです。

外部URLに自分のアプリの getContextPath() は付けません。また、元のrequestの属性や、自分のアプリのsessionに保存した値が、移動先の別サイトへ自動で渡るわけではありません。

移動先は、アプリ側で決めた信頼できるURLを使います。利用者が入力したURLをそのままsendRedirectへ渡すと、自分のサイトを経由して不正なサイトへ誘導される「オープンリダイレクト」の原因になります。外部サイトへの移動自体が問題なのではなく、移動先を無制限に指定できることが問題です。

先頭の「/」の意味を混同しない

コードアプリが /lesson05 の場合
request.getRequestDispatcher("/navigation-result.jsp")アプリ内の /lesson05/navigation-result.jsp を指す。
response.sendRedirect("/navigation-result.jsp")サイトのルート直下の /navigation-result.jsp を指す。
response.sendRedirect(request.getContextPath() + "/navigation-result.jsp")/lesson05/navigation-result.jsp を移動先にできる。

request.getContextPath() は、このアプリのURL上の配置場所を返します。アプリ名が変わっても動くよう、今回のリダイレクトではそれを付けています。サーバーのファイルシステム上のパスではありません。

同じ入力で、フォワードとリダイレクトを試す

ここからは、requestの属性が引き継がれるかを確かめる比較用プログラムです。購入金額が10,000円以上なら500円引き、それ未満なら0円とします。リダイレクト先へ値を渡す処理は意図的に入れていないため、リダイレクト側は割引額を表示しません。完成した購入・登録アプリではなく、違いを確認するための例です。

Java 17・Tomcat 10.1の jakarta.servlet を使います。各ファイルはUTF-8で保存します。JSPはWebアプリの直下に配置します。

1.入力画面:src/main/webapp/navigation-form.jsp

<%@ page pageEncoding="UTF-8"
         contentType="text/html; charset=UTF-8" %>
<%
    String error = (String) request.getAttribute("error");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>画面遷移の比較</title>
</head>
<body>
    <h1>画面遷移の比較</h1>
    <% if (error != null) { %>
        <p><%= error %></p>
    <% } %>
    <form action="discount-navigation" method="post"
          accept-charset="UTF-8">
        <label for="amount">購入金額(円)</label>
        <input id="amount" name="amount" type="number"
               min="0" max="2147483647" step="1" required>
        <button type="submit" name="navigation" value="forward">
            フォワードで表示
        </button>
        <button type="submit" name="navigation" value="redirect">
            リダイレクトで表示
        </button>
    </form>
</body>
</html>

押したボタンの name="navigation" とvalueが送信されます。Servletでは getParameter("navigation") で、forwardかredirectかを受け取れます。

2.計算:src/main/java/example/DiscountModel.java

package example;

public class DiscountModel {
    public int calculateDiscount(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException(
                    "購入金額は0円以上にしてください。");
        }
        if (amount >= 10000) {
            return 500;
        }
        return 0;
    }
}

割引額の判定はModelが担当します。Servletはこの戻り値を受け取り、requestへ保存します。

3.画面遷移:src/main/java/example/DiscountNavigationServlet.java

package 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("/discount-navigation")
public class DiscountNavigationServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String text = request.getParameter("amount");
        String navigation = request.getParameter("navigation");

        int discount;
        try {
            if (text == null || text.isBlank()) {
                throw new IllegalArgumentException();
            }
            int amount = Integer.parseInt(text.trim());
            DiscountModel model = new DiscountModel();
            discount = model.calculateDiscount(amount);
        } catch (IllegalArgumentException e) {
            request.setAttribute("error",
                    "購入金額は0〜2147483647の整数で入力してください。");
            request.getRequestDispatcher("/navigation-form.jsp")
                    .forward(request, response);
            return;
        }

        request.setAttribute("discount", discount);

        if ("redirect".equals(navigation)) {
            response.sendRedirect(request.getContextPath()
                    + "/navigation-result.jsp");
            return;
        }

        request.getRequestDispatcher("/navigation-result.jsp")
                .forward(request, response);
    }
}

どちらのボタンでも、同じModelを呼び、同じ名前で割引額を保存します。変わるのは、その後に forward するか sendRedirect するかだけです。redirect以外の値やnavigationの省略時は、比較コードではフォワード側へ進みます。

未入力・整数でない値・intの範囲外・負の金額は、固定のエラーメッセージを保存して入力JSPへforwardします。Integer.parseIntのNumberFormatExceptionもIllegalArgumentExceptionの一種なので、ここでは同じcatchで扱っています。エラー後のreturnにより、結果画面への処理を続けません。

4.結果画面:src/main/webapp/navigation-result.jsp

<%@ page pageEncoding="UTF-8"
         contentType="text/html; charset=UTF-8" %>
<%
    Integer discount =
            (Integer) request.getAttribute("discount");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>画面遷移の比較結果</title>
</head>
<body>
    <h1>画面遷移の比較結果</h1>
    <% if (discount != null) { %>
        <p>割引額: <%= discount %>円</p>
    <% } else { %>
        <p>割引額がありません。</p>
    <% } %>
    <a href="navigation-form.jsp">入力画面に戻る</a>
</body>
</html>

Integerで受け取り、nullかどうかを分けています。0円は正常な計算結果なので、「割引額がありません。」にはなりません。表示するのは整数と固定文言だけで、入力文字列を直接HTMLに埋め込まない形です。

表示結果・URL・通信を確認する

アプリが /lesson05 なら、http://localhost:8080/lesson05/navigation-form.jsp を開き、12000を入力して2つのボタンを順に試します。毎回「入力画面に戻る」からフォームへ戻ってください。

試すこと表示結果画面のURL
12000をフォワード割引額: 500円/lesson05/discount-navigation
12000をリダイレクト割引額がありません。/lesson05/navigation-result.jsp
9999をフォワード割引額: 0円/lesson05/discount-navigation
入力不正なリクエストが届く入力画面に固定のエラーメッセージ/lesson05/discount-navigation

ブラウザの入力制約で未入力や負数の送信が止まる場合でも、サーバーへ届いた値はServlet側で確認します。ServletのURLをアドレスバーから直接開くとGETになるため、doPostのみのこのServletではフォームから送信してください。

開発者ツールのNetworkで「ログを保持」を有効にすると、リダイレクトでは最初のPOSTの302応答と、その後のJSPへのGETを確認できます。フォワードでは、ServletへのPOSTに対してHTMLが返ります。JSPへの別のHTTPリクエストは出ません。

使い分け1:入力エラーは、その場でメッセージを表示する

たとえば購入金額が整数でない場合、入力画面に「整数で入力してください」と表示したいだけです。エラーメッセージをrequestへ入れて入力JSPへforwardすれば、エラーを持ったまま入力画面を表示できます。今回のcatch内がその例です。

ここでsendRedirectだけに置き換えると、error属性は次のリクエストへ引き継がれず、入力画面のメッセージが表示されません。入力値も再表示したい場合は、その値を用意してHTML用に安全に出力する処理を加えます。今回の比較例はメッセージだけを表示します。

使い分け2:登録が終わったら、表示用のGETへ進む

商品を登録する画面を考えます。「ノート」を登録するPOSTの最後で、そのまま完了JSPへforwardした場合、ブラウザに表示されているのは登録POSTへの応答です。再読み込みでPOSTが再送信されると、登録処理が再実行される可能性があります。

そこで、登録処理と登録後の表示を、次のように分けます。この節は、比較コードとは別に、DBへ登録するアプリの構成例です。

  1. ブラウザが POST /products/create で登録内容を送る。
  2. 登録用ServletがModelやDAOを呼び、DBへの登録を完了する。たとえば「ノート」が商品ID17として保存される。
  3. 登録用Servletが、ブラウザを /products/detail?id=17 へリダイレクトする。
  4. ブラウザがそのURLへGETでアクセスする。表示用Servletが、閲覧権限を確認したうえでID17の商品をDBから取得する。
  5. 表示用Servletが商品データを新しいrequestへ保存し、詳細JSPへforwardしてHTMLを返す。

POST → Redirect → GETという流れを、PRGパターンと呼びます。表示が完了したあとに再読み込みしても、通常は表示用GETをやり直すだけで、直前の登録POSTを再送信しません。GET側には登録・更新処理を書かず、表示に必要な読み取りを担当させます。

この構成では、登録Servletから表示Servletへはリダイレクト、表示ServletからJSPへはフォワードを使います。どちらか一方に統一するのではなく、役割に合わせて組み合わせます。

なお、PRGだけで二重登録を完全に防げるわけではありません。送信ボタンの連打、通信の再試行などへの対策は別に必要です。重要な登録処理では、一意な操作IDによる重複確認やDBの制約なども組み合わせます。

リダイレクト先でデータを使うには?

ここでは、同じアプリ内の表示処理へリダイレクトする場合を考えます。特にsessionを使う方法は、同じセッションを利用できることが前提です。外部サイトへ自分のアプリのsessionがそのまま渡るわけではありません。

リダイレクトで変わるのは、リクエストです。保存済みのDBデータまで消えるわけではありません。先ほどの商品ID17の例では、URLからIDを受け取り、DBから表示用データを取得し直しています。前のrequestの属性を使い回しているのではありません。

渡したいもの考え方
登録済みの商品など必要なIDを移動先へ伝え、表示側でデータを取得し直す。IDも外部入力として検証し、閲覧権限を確認する。
「登録しました」という一度だけの通知sessionなどに一時的に保持し、次の表示で取り出したら削除する方法がある。requestの属性とは有効範囲が異なる。
検索条件など、URLに載せてよい情報クエリパラメータで渡す。文字列のURLエンコードと、受信側の入力確認が必要。

何でもsessionへ入れると、古い値が残ったり複数タブの操作が干渉したりします。パスワードや個人情報をURLへ付けることも避けます。今回の割引比較では、属性が引き継がれないことを見るため、これらの引き継ぎ処理は追加していません。

書くときに注意したいこと

  • どちらも応答を確定させる前に使う。HTMLを出力・送信したあとでforwardやsendRedirectを行うと、IllegalStateExceptionなどの原因になります。
  • 分岐の途中ではreturnで後続処理を止める。sendRedirectを呼んでも、Javaのメソッドが自動で終了するわけではありません。
  • 移動先を外部入力のまま使わない。フォームやURLで受け取った任意のURLへリダイレクトできると、不正なサイトへ誘導される原因になります。今回の移動先はコード内の固定パスです。

確認問題

本文の比較プログラムを前提に、次の4問を考えてください。アプリのコンテキストパスは /lesson05 とします。

  1. 12000を入力して「フォワードで表示」を押しました。画面の割引額、ブラウザのURL、フォーム送信から結果HTMLを得るまでのHTTPリクエスト数を答えてください。
  2. 同じ入力で「リダイレクトで表示」を押しました。画面の表示とブラウザのURLを答えてください。Modelは500を返しているのに、その値が表示されない理由も説明してください。
  3. 入力不正時のcatch内を、response.sendRedirect(request.getContextPath() + "/navigation-form.jsp"); に置き換えました。requestに保存したerrorを入力JSPで表示できるでしょうか。メッセージをそのまま表示する修正方法も答えてください。
  4. DBへ商品を登録したあと、詳細画面を表示したいと考えています。登録Servletから表示Servlet、表示Servletから詳細JSPには、それぞれどちらを使いますか。requestに保存した商品をそのまま受け取れない場合、表示側はどう用意しますか。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • フォワードは、同じリクエストのままサーバー内で処理を引き継ぐ。
  • リダイレクトは、ブラウザに新しいリクエストを送ってもらう。元のrequest属性は自動で引き継がれない。
  • リダイレクトは同じサイト内だけでなく、完全なURLを指定して外部サイトへ移動させることもできる。
  • 入力エラーや処理結果をJSPへ渡す場面ではフォワードが使える。
  • 登録後は表示用GETへリダイレクトし、その表示処理からJSPへforwardする構成がある。
  • PRGは再読み込みによるPOST再送信を避けるための構成であり、すべての重複登録を防ぐ仕組みではない。

関連記事

シェアする