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

処理結果や入力エラーを、そのリクエストのままJSPに表示したいならフォワード。登録処理を終え、ブラウザに表示用のURLへアクセスし直してもらいたいならリダイレクト。この違いが分かると、「画面は開いたのに値がない」「更新したらもう一度送信された」という疑問を整理できます。
どちらも別の画面を表示するために使えますが、フォワードはサーバー内で処理を引き継ぐ仕組み、リダイレクトはブラウザへ別URLへのアクセスを指示する仕組みです。見た目ではなく、誰が次の処理を始めるのかに注目しましょう。
リダイレクトの移動先は、同じサイト内に限りません。別のサーバーで動く外部サイトへ移動させることもできます。まず同じアプリ内の例でリクエストの違いを確認し、外部URLを指定する書き方も紹介します。
Schoo「Java入門 中級」第5回「サーバーサイドアプリケーションの実装基礎」のServlet・JSP連携を補足する記事です。今回は、フォワードと比較しながらリダイレクトの使いどころまで確認します。
最初に結論:同じリクエストか、新しいリクエストか
| 比較する点 | フォワード | リダイレクト |
|---|---|---|
| 処理を進めるのは | サーバー内のServletコンテナ | 指示を受け取ったブラウザ |
| リクエスト | 同じリクエストの処理を引き継ぐ | ブラウザから新しいリクエストが来る |
| requestの属性 | 引き継ぎ先で利用できる | 自動では引き継がれない |
| ブラウザのURL | forwardによっては変わらない | 指定した移動先になる |
| 今回使うメソッド | RequestDispatcher.forward() | response.sendRedirect() |
requestの属性とは、setAttribute() で名前を付けて保存するサーバー側のデータです。リクエストスコープの値は、そのリクエストの処理中に共有します。別のリクエストになるリダイレクト先には、自動でコピーされません。
| 移動先の範囲 | フォワード | リダイレクト |
|---|---|---|
| 同じアプリのServlet・JSP | サーバー内で処理を引き継げる | ブラウザにアクセスし直してもらえる |
| 外部サイトのURL | 外部URLへブラウザを移動させる仕組みではない | https://から始まる完全なURLを指定できる |
イラストで比較する:ブラウザはいつアクセスするのか

まず上段と中段を比較してください。どちらも自分のサーバーの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;
- 自分のアプリが、移動先URLをLocationヘッダーに入れてブラウザへ返す。
- ブラウザが、そのURLの外部サーバーへ新しいリクエストを送る。
- 外部サーバーが応答し、ブラウザにそのサイトが表示される。
自分の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へ登録するアプリの構成例です。
- ブラウザが
POST /products/createで登録内容を送る。 - 登録用ServletがModelやDAOを呼び、DBへの登録を完了する。たとえば「ノート」が商品ID17として保存される。
- 登録用Servletが、ブラウザを
/products/detail?id=17へリダイレクトする。 - ブラウザがそのURLへGETでアクセスする。表示用Servletが、閲覧権限を確認したうえでID17の商品をDBから取得する。
- 表示用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 とします。
- 12000を入力して「フォワードで表示」を押しました。画面の割引額、ブラウザのURL、フォーム送信から結果HTMLを得るまでのHTTPリクエスト数を答えてください。
- 同じ入力で「リダイレクトで表示」を押しました。画面の表示とブラウザのURLを答えてください。Modelは500を返しているのに、その値が表示されない理由も説明してください。
- 入力不正時のcatch内を、
response.sendRedirect(request.getContextPath() + "/navigation-form.jsp");に置き換えました。requestに保存したerrorを入力JSPで表示できるでしょうか。メッセージをそのまま表示する修正方法も答えてください。 - DBへ商品を登録したあと、詳細画面を表示したいと考えています。登録Servletから表示Servlet、表示Servletから詳細JSPには、それぞれどちらを使いますか。requestに保存した商品をそのまま受け取れない場合、表示側はどう用意しますか。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- フォワードは、同じリクエストのままサーバー内で処理を引き継ぐ。
- リダイレクトは、ブラウザに新しいリクエストを送ってもらう。元のrequest属性は自動で引き継がれない。
- リダイレクトは同じサイト内だけでなく、完全なURLを指定して外部サイトへ移動させることもできる。
- 入力エラーや処理結果をJSPへ渡す場面ではフォワードが使える。
- 登録後は表示用GETへリダイレクトし、その表示処理からJSPへforwardする構成がある。
- PRGは再読み込みによるPOST再送信を避けるための構成であり、すべての重複登録を防ぐ仕組みではない。