简介:Servlet和JSP是Java Web开发的核心技术,分别承担业务逻辑处理与动态页面展示的职责。通过理解Servlet的生命周期、请求转发与重定向机制,以及JSP的转换过程和脚本元素使用,开发者可构建结构清晰的MVC应用。本文深入讲解两者之间的协同工作方式,涵盖JSP指令、EL表达式、JSTL标签库以及在Eclipse与MyEclipse中的开发差异,并结合“Hello”示例演示基本流程,帮助开发者掌握Java Web中关键的跳转控制与页面交互技术。
Servlet与JSP跳转机制的深度解析与工程实践
在Java Web开发的世界里,页面跳转看似简单——点个按钮、提交个表单,然后“唰”一下就到了新页面。但你有没有想过:这背后到底是怎么实现的?为什么有时候地址栏变了,有时候又不变?为什么刷新页面会导致表单重复提交?这些问题的答案,都藏在一个叫 跳转机制 的核心技术中。
今天咱们不讲空话,直接上硬核内容。从一个用户登录的小功能开始,一路挖到Servlet容器底层,彻底搞清楚请求转发和重定向的本质区别、适用场景以及那些只有老手才知道的坑。
🧱 Servlet生命周期:一切跳转行为的起点
要理解跳转,得先知道Servlet是怎么工作的。别被“生命周期”这个词吓住,它其实就是一句话:
一次初始化,多次服务,一次销毁。
听起来像不像打工人的一生?😄 不过咱们的重点是:这个过程如何影响跳转?
初始化阶段(init)—— 配置先行
当Tomcat启动时,它会为每个Servlet创建唯一实例,并调用 init() 方法。这一阶段只执行一次,非常适合加载配置参数,比如跳转路径。
@Override
public void init() throws ServletException {
ServletConfig config = getServletConfig();
successPage = config.getInitParameter("success-page");
errorPage = config.getInitParameter("error-page");
}
这些参数来自 web.xml :
<init-param>
<param-name>success-page</param-name>
<param-value>/dashboard.jsp</param-value>
</init-param>
🎯 关键洞察 :把跳转目标写死在代码里是非常糟糕的做法!一旦要改路径就得重新编译打包。而通过 init-param 集中管理,部署灵活性大大提升。
你可以把它想象成飞机起飞前的检查清单——油加好了吗?航路设好了吗?一切都准备就绪,才能进入飞行模式(处理请求)。
flowchart TD
A[Web容器启动] --> B{首次请求LoginServlet?}
B -->|是| C[实例化LoginServlet]
C --> D[调用init()方法]
D --> E[读取web.xml中的init-param]
E --> F[初始化successPage/errorPage]
F --> G[进入就绪状态等待请求]
B -->|否| H[直接进入service处理]
看到没? init() 不是可有可无的装饰品,它是整个跳转控制的数据基础。没有它,后面的决策就成了“盲人骑瞎马”。
请求处理阶段(service/doPost)—— 跳转战场
这是最热闹的地方。每当浏览器发来一个请求,容器就会调用 service() 方法,再根据HTTP动词分发到 doGet() 或 doPost() 。
举个真实例子:用户提交登录表单后,后台这样处理:
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean isValid = authenticate(username, password);
if (isValid) {
request.setAttribute("user", username);
RequestDispatcher rd = request.getRequestDispatcher(successPage);
rd.forward(request, response); // 服务器内部跳转
} else {
response.sendRedirect(errorPage); // 客户端重新发起请求
}
}
注意这里有两个完全不同的跳转方式!
| 方法 | 是否共享request域 | 地址栏变化 | 典型用途 |
|---|---|---|---|
forward() | ✅ 是 | ❌ 否 | 数据传递展示 |
sendRedirect() | ❌ 否 | ✅ 是 | 表单防重复提交 |
💡 灵魂拷问时间 :你真的清楚什么时候该用哪个吗?
我们后面会展开讲,但现在先记住一点:
👉 forward 是“悄悄换人干活”,redirect 是“让客户另找一家店”。
销毁阶段(destroy)—— 善后比开始更重要
应用关闭时,容器会调用 destroy() 方法清理资源。虽然这时候已经没人访问了,但你仍需考虑优雅停机问题。
想象这样一个场景:
用户正在登录,系统突然重启。如果此时强行关闭数据库连接池,那个还没完成的登录请求就会失败。
解决方案很简单:加个“关门预告”标志位。
private volatile boolean shuttingDown = false;
@Override
public void destroy() {
shuttingDown = true;
try {
Thread.sleep(2000); // 等待最多2秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cleanupResources();
}
配合过滤器监控活跃请求数量,就能实现真正的“优雅下线”。
flowchart LR
A[收到shutdown指令] --> B[设置shuttingDown=true]
B --> C{仍有活跃请求?}
C -->|是| D[等待超时或完成]
C -->|否| E[执行destroy清理]
D --> F[超时或全部结束]
F --> E
这就像餐厅打烊前挂出“最后点单”牌子,而不是直接赶人走。用户体验好不好,往往就在这种细节里。
🔁 请求转发 vs 重定向:不只是API调用那么简单
现在我们深入两种跳转机制的技术本质。你以为只是换个方法名?错!它们的工作原理、性能表现、安全性都有天壤之别。
请求转发(Request Forward)—— 服务器内部的秘密交接
核心机制:一次请求,多方协作
请求转发最大的特点是: 整个过程只涉及一次HTTP请求 。客户端根本不知道发生了什么。
流程如下:
1. 浏览器请求 /login
2. Servlet处理完逻辑
3. 调用 request.getRequestDispatcher("/welcome.jsp").forward(...)
4. 容器将请求“内部移交”给JSP
5. JSP生成HTML返回给浏览器
6. 地址栏依然是 /login
🤯 惊奇吗?表面上你在访问 /login ,实际上显示的是 welcome.jsp 的内容!
这就是MVC架构的精髓所在:
- Controller(Servlet) 处理业务逻辑
- View(JSP) 负责渲染展示
两者分工明确,又能无缝协作。
数据共享的秘密武器:request域
由于是在同一个请求周期内完成的跳转,所有通过 request.setAttribute() 设置的数据都能保留。
request.setAttribute("currentUser", "张三");
dispatcher.forward(request, response);
到了JSP那边,可以直接用EL表达式拿到:
<p>欢迎你,${currentUser}!</p>
✅ 这种方式特别适合传输复杂对象,比如一个完整的User实体。
⚠️ 但是!有个致命限制: 不能有任何输出提前提交响应 。
out.println("Hello"); // 已经写了内容
dispatcher.forward(...); // 抛异常!
因为HTTP协议规定响应头只能发送一次,而 forward() 需要修改Content-Type等头部信息。所以最佳实践是:
if (!response.isCommitted()) {
dispatcher.forward(request, response);
} else {
throw new IllegalStateException("响应已提交,无法执行转发");
}
路径获取方式大揭秘
RequestDispatcher 有两种获取方式:
| 获取方式 | 路径类型 | 推荐度 |
|---|---|---|
request.getRequestDispatcher(path) | 支持相对路径 | ⭐⭐⭐⭐⭐ |
context.getRequestDispatcher(path) | 必须以 / 开头 | ⭐⭐ |
为啥推荐前者?因为它更灵活!
// 当前URL: /app/user/edit
request.getRequestDispatcher("../list");
// 相当于 /app/user/list —— 多方便!
request.getRequestDispatcher("/user/list");
// 必须写全路径 —— 容易出错
下面是路径解析规则的可视化说明:
graph TD
A[开始获取RequestDispatcher] --> B{调用者是谁?}
B -->|HttpServletRequest| C[使用request.getRequestDispatcher(path)]
B -->|ServletContext| D[使用context.getRequestDispatcher(path)]
C --> E[路径是否以'/'开头?]
E -->|是| F[解释为Web应用根路径下的绝对路径]
E -->|否| G[解释为相对于当前请求URI的相对路径]
D --> H[路径必须以'\/'开头]
H --> I[解释为Web应用根路径下的绝对路径]
F --> J[创建RequestDispatcher实例]
G --> J
I --> J
J --> K[结束]
掌握这套规则,你就不会再遇到“404找不到页面”的低级错误了。
底层真相:JSP其实是个Servlet
很多人以为JSP是“静态模板”,错了!每一个 .jsp 文件最终都会被编译成一个 .class 文件,本质上就是一个标准的Servlet。
以 login.jsp 为例,Tomcat会生成类似这样的类:
public final class login_jsp extends org.apache.jasper.runtime.HttpJspBase {
public void _jspService(final HttpServletRequest request,
final HttpServletResponse response)
throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
PageContext pageContext = _jspxFactory.getPageContext(this, request, response, ...);
try {
out.write("<html><body>");
out.write(" <form action=\"LoginServlet\" method=\"post\">");
// 更多HTML输出...
} catch (Throwable t) {
if (!(t instanceof SkipPageException)) {
_jspError(pageContext, t);
}
} finally {
pageContext.release();
}
}
}
看到了吗?所谓的“动态网页”,其实就是一堆 out.write() 拼出来的字符串。
而且这个 _jspService() 方法会在请求转发时被直接调用,传入的就是原来的 request 和 response 对象。
sequenceDiagram
participant Client
participant Servlet
participant JSP
Client->>Servlet: POST /login
Servlet->>Servlet: setAttribute("username", "张三")
Servlet->>JSP: forward(request, response) → /result.jsp
JSP->>JSP: _jspService() 执行
JSP->>Client: 返回HTML响应
Note right of JSP: URL仍为 /login
所以别小看JSP,它是有“灵魂”的——它的灵魂就是Servlet容器赋予的生命力。
重定向(Redirect)—— 让浏览器自己跑一趟
如果说请求转发是“暗箱操作”,那重定向就是“公开通知”。
response.sendRedirect("/home");
这行代码的背后,是一整套HTTP协议级别的交互。
协议层面的真相:302 + Location
当你调用 sendRedirect() 时,Servlet容器会自动做三件事:
- 设置状态码为
302 Found - 添加响应头
Location: /home - 发送空响应体
完整的响应报文长这样:
HTTP/1.1 302 Found
Location: http://localhost:8080/myapp/home
Content-Length: 0
Date: Mon, 06 Apr 2025 10:30:00 GMT
浏览器收到后,立刻解析 Location 字段,并自动发起新的GET请求。
整个流程清晰可见:
graph LR
A[浏览器: GET /login] --> B[LoginServlet处理]
B --> C{验证成功?}
C -->|是| D[response.sendRedirect("/home")]
C -->|否| E[response.sendRedirect("/login?error=1")]
D --> F[服务器返回302 + Location:/home]
E --> G[服务器返回302 + Location:/login?error=1]
F --> H[浏览器发起新请求: GET /home]
G --> I[浏览器发起新请求: GET /login]
H --> J[HomeServlet或JSP返回响应]
I --> K[LoginJSP显示错误提示]
你会发现,总共发生了 两次独立的HTTP请求 。
这意味着:
- 性能损耗更大(多了网络往返)
- 原来的 request 域数据全部丢失
- 地址栏更新为新路径
🎯 适用场景总结 :
- 表单提交后跳转(防止刷新重复提交)
- 登录成功后跳首页
- OAuth授权跳转第三方平台
特别是第一点,“Post-Redirect-Get”模式几乎是现代Web开发的标配。
数据丢了怎么办?三大补救方案
由于重定向会开启全新请求,原来存放在 request 里的数据自然就没了。那怎么把消息带到下一页?
方案一:Session暂存(推荐)
利用 session 存储临时消息,读取后立即清除。
// 在源Servlet中
HttpSession session = request.getSession();
session.setAttribute("flashMsg", "操作成功");
response.sendRedirect("/result");
// 在目标页面读取并清除
String msg = (String) session.getAttribute("flashMsg");
request.setAttribute("msg", msg);
session.removeAttribute("flashMsg"); // 清除,防止重复显示
这种模式叫做 Flash Attribute ,Spring MVC内部也是这么干的。
优点:支持复杂对象,安全可靠
缺点:占用服务器内存,需手动清理
方案二:URL参数传递(轻量级)
适用于简单字符串:
response.sendRedirect("/result?status=success&msg=" + URLEncoder.encode("删除成功", "UTF-8"));
前端通过 request.getParameter("msg") 读取。
优点:无需服务端存储,速度快
缺点:长度受限,暴露敏感信息风险高
方案三:Cookie存储(持久化需求)
Cookie cookie = new Cookie("lastAction", "delete");
cookie.setMaxAge(60); // 有效1分钟
response.addCookie(cookie);
适合记录用户偏好或短期行为追踪。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Session暂存 | 可传递复杂对象 | 占用内存,需清理 | 短期提示消息 |
| URL参数 | 简单高效 | 安全性低 | 状态标识、分页参数 |
| Cookie存储 | 持久化能力强 | 客户端可禁用 | 用户偏好设置 |
📌 建议优先顺序 :Session > URL参数 > Cookie
⚖️ 终极对比:选型决策树
说了这么多,到底该怎么选?别急,我给你画了个 跳转方式选择决策树 :
graph TD
Start[开始] --> Q1{是否需要改变地址栏?}
Q1 -->|是| UseRedirect[使用 sendRedirect]
Q1 -->|否| Q2{是否在同一应用内跳转?}
Q2 -->|否| UseRedirect
Q2 -->|是| Q3{是否需要传递复杂数据?}
Q3 -->|是| UseForward[使用 forward]
Q3 -->|否| Q4{是否为表单提交后跳转?}
Q4 -->|是| UseRedirect
Q4 -->|否| UseForward
结合实战场景来看:
场景一:表单提交防重复(PRG模式)
protected void doPost(...) {
processForm(); // 处理业务
response.sendRedirect("/success"); // PRG核心
}
用户刷新 /success 页面只会重复GET,不会再次提交表单。这是唯一正确解法!
场景二:登录验证跳转
if (valid) {
session.setAttribute("user", user);
response.sendRedirect("/index"); // 成功则重定向
} else {
request.setAttribute("error", "账号或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response); // 失败则转发
}
聪明吧?失败时用 forward 可以保留用户输入的内容,体验更好;成功后用 redirect 避免刷新导致重新登录。
场景三:权限拦截统一处理
if (!isAuthenticated(request)) {
response.sendRedirect("/login");
return;
}
chain.doFilter(request, response);
必须用重定向!否则地址栏还是停留在受保护资源,用户还以为自己有权限呢。
🛠️ 工程最佳实践:让你的代码更健壮
光懂理论不够,还得会落地。以下是我在多个大型项目中验证过的实用技巧。
封装通用跳转工具类
别到处写 request.getRequestDispatcher(...).forward() 了,封装起来!
public class NavigationUtil {
/**
* 安全转发,避免响应已提交异常
*/
public static void forwardTo(HttpServletRequest req,
HttpServletResponse resp,
String path)
throws ServletException, IOException {
if (!resp.isCommitted()) {
req.getRequestDispatcher(path).forward(req, resp);
} else {
throw new IllegalStateException("Response already committed: cannot forward to " + path);
}
}
/**
* 自动编码URL的重定向
*/
public static void redirectTo(HttpServletResponse resp, String path) throws IOException {
resp.sendRedirect(resp.encodeRedirectURL(path));
}
/**
* 根据Referer智能跳转
*/
public static void safeRedirect(HttpServletRequest req,
HttpServletResponse resp,
String defaultTarget) throws IOException {
String referer = req.getHeader("Referer");
String target = (referer != null && !referer.isEmpty()) ? referer : defaultTarget;
resp.sendRedirect(target);
}
}
用了这个类,你的控制器代码会清爽很多:
NavigationUtil.forwardTo(request, response, "/success.jsp");
日志记录 + 监控审计
集成SLF4J记录关键跳转事件:
private static final Logger logger = LoggerFactory.getLogger(LoginServlet.class);
logger.info("User {} login success, redirecting to /dashboard", username);
logger.warn("Authentication failed for {}, forwarding to login page", username);
还可以结合过滤器做全局审计:
graph TD
A[客户端请求] --> B{是否登录?}
B -- 否 --> C[记录未授权访问]
C --> D[跳转至登录页]
B -- 是 --> E{是否为AJAX?}
E -- 是 --> F[返回440状态码]
E -- 否 --> G[执行业务逻辑]
G --> H[记录跳转动作]
H --> I[完成跳转]
线上出了问题,翻日志一看就知道是谁、什么时候、从哪跳到哪了。
防止跳转死循环
最常见的死循环原因:
- 过滤器误拦截静态资源(CSS/JS/Images)
- 登录判断逻辑错误导致反复跳转
解决办法:加个计数器防护墙。
Integer redirectCount = (Integer) request.getSession().getAttribute("redirectCount");
if (redirectCount == null) redirectCount = 0;
if (redirectCount > 5) {
logger.error("Potential redirect loop detected for session: {}", request.getSession().getId());
throw new IllegalStateException("Too many redirects. Possible configuration error.");
}
request.getSession().setAttribute("redirectCount", redirectCount + 1);
// 正常跳转完成后记得清零
request.getSession().removeAttribute("redirectCount");
上线前跑一遍自动化测试,这类问题基本都能提前发现。
💡 特殊场景解决方案集锦
AJAX请求中的跳转失效问题
传统重定向对AJAX无效!因为XMLHttpRequest不会自动跟随302跳转。
解决方案:约定特殊状态码。
// 后端检测未登录
if (!authenticated) {
response.setStatus(440); // 自定义未认证状态
response.setHeader("Location", "login.jsp");
return;
}
前端捕获并处理:
$.ajax({
url: 'api/data',
method: 'GET',
success: function(data) { /* 正常处理 */ },
error: function(xhr) {
if (xhr.status === 440) {
window.location.href = xhr.getResponseHeader('Location');
}
}
});
这样既保持了RESTful风格,又能实现安全跳转。
文件下载后返回原页面
利用HTTP Referer头实现“来时的路”:
String referer = request.getHeader("Referer");
if (referer == null) referer = "dashboard.jsp";
// 下载逻辑...
OutputStream out = response.getOutputStream();
// 写入文件流...
// 通过JS延迟跳转
PrintWriter writer = response.getWriter();
writer.println("<script>window.onload=function(){setTimeout(function(){window.location.href='"
+ referer + "'}, 1000);}</script>");
用户下载完Excel报表,1秒后自动回到仪表盘,体验丝滑。
权限校验后的智能回跳
用户想访问 /admin/users ,但没登录。登录成功后应该自动跳回去。
// 在过滤器中记录原始请求
String originalURL = request.getServletPath();
if (request.getQueryString() != null) {
originalURL += "?" + request.getQueryString();
}
session.setAttribute("redirectAfterLogin", originalURL);
// 登录成功后读取并跳转
String redirectAfterLogin = (String) session.getAttribute("redirectAfterLogin");
if (redirectAfterLogin != null) {
response.sendRedirect(redirectAfterLogin);
session.removeAttribute("redirectAfterLogin");
} else {
response.sendRedirect("/default.jsp");
}
这才是真正以用户为中心的设计思维。
✅ 总结:构建现代化Web导航体系
今天我们从零开始,完整拆解了Servlet/JSP跳转机制的方方面面。最后划重点:
-
forward 和 redirect 不是简单的语法差异,而是两种截然不同的通信模型
- 前者是服务器内部调度,后者是客户端重新请求
- 选择依据应基于业务需求而非习惯 -
合理利用request/session/url参数三种数据传递方式
- 对象用session,状态用参数,敏感信息绝不放URL -
封装+日志+监控=可维护系统的三大支柱
- 工具类减少重复代码
- 日志帮助快速定位问题
- 监控预防潜在风险 -
永远不要低估用户体验细节
- 保留表单输入、防止重复提交、智能回跳……这些才是专业性的体现
最后送大家一句话:
“优秀的程序员不仅写出能运行的代码,更写出能进化的系统。”
希望这篇文章能帮你建立起对跳转机制的系统认知。下次写 sendRedirect 的时候,你会想起今天的对话 😄
简介:Servlet和JSP是Java Web开发的核心技术,分别承担业务逻辑处理与动态页面展示的职责。通过理解Servlet的生命周期、请求转发与重定向机制,以及JSP的转换过程和脚本元素使用,开发者可构建结构清晰的MVC应用。本文深入讲解两者之间的协同工作方式,涵盖JSP指令、EL表达式、JSTL标签库以及在Eclipse与MyEclipse中的开发差异,并结合“Hello”示例演示基本流程,帮助开发者掌握Java Web中关键的跳转控制与页面交互技术。
1542

被折叠的 条评论
为什么被折叠?



