1. 请求转发与请求重定向的本质区别,先把这两件事分开
我在面试别人时经常问一个问题:用户在浏览器里点了一个按钮,结果页面上展示的还是同一个URL,但内容已经换成另一个接口返回的数据了,这时候到底是forward还是redirect?十个人里有八个会犹豫。这其实是一个特别基础但又特别容易混淆的问题,因为从用户最终看到的结果来看,两者都能让请求从A走到B,但本质上的路径完全不同。
先说结论:forward是服务器内部的动作,客户端完全不知情。你请求/login,Servlet里forward到/home,服务器直接把/home的内容打包响应给浏览器,浏览器的地址栏永远停留在/login。而redirect是服务器告诉客户端"你去别的地方",返回一个302状态码,附带一个Location头,浏览器看到之后自己再发起一次全新的请求,所以地址栏会变成新地址。
这个区别直接导致了一个最容易考的面试点:请求次数不同。forward只有一次请求,redirect至少两次。forward能在同一个请求里共享request对象里的属性,redirect因为浏览器重新发起请求,原来的request对象早就销毁了,你塞进去的属性自然也就丢了。
单纯背这些结论没意义,关键是理解为什么。我打个比方。你在前台问工作人员A:"财务部在哪?"A说"我带你去",然后他领着你穿过走廊到财务部,你见到财务部的人。整个过程只有你去前台这一次沟通,后面的带路、开门、引荐都是A内部帮你完成的,外人看你还是在前台。这就是forward。
另一种情况,A说:"财务部在3楼302,你自己过去吧。"你听完之后自己走楼梯到302,敲门口才见到财务专员。你跟A说了第一句话,又跟财务专员说了第二句话,这是两次沟通,而且A不知道你后面干了什么。这就是redirect。
理解了这两个场景,再去看代码和技术细节,整个脉络就清晰了。这篇文章我不打算只罗列区别表格,而是从实际开发中的每一个关键决策点出发,把原理、代码、排查手段讲透。不管你是准备面试还是已经在写Servlet/Spring MVC项目,都应该能从里面找到自己需要的东西。
2. 请求转发(forward)的深入拆解:一次请求内部的接力
2.1 Forward的底层机制与生命周期
forward在Java Web里最常见的实现方式是通过RequestDispatcher:
RequestDispatcher dispatcher = request.getRequestDispatcher("/targetServlet"); dispatcher.forward(request, response);当这行代码执行时,发生了什么?实际上容器(Tomcat、Jetty这些)拿到请求后创建了HttpServletRequest和HttpServletResponse两个对象。forward就是把这两个对象直接交到下一个Servlet或JSP手里,由它继续填充response。这中间没有网络往返,没有浏览器参与,所有流程都在服务器进程内部完成。
有一个细节特别值得注意:forward之后的代码仍然会继续执行,除非你return了。这跟很多人直觉里"跳转之后后面的代码就不跑了"不一样。看下面这段代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/result.jsp").forward(request, response); System.out.println("这行代码依然会执行"); response.getWriter().write("这个输出会抛异常或者无效"); }为什么?因为forward调用之后,控制权从当前Servlet移交给了目标资源,但当前Servlet的方法栈并没有立刻终止。如果后面又往response里写东西,容器会认为你在同一个响应里重复提交内容,通常会产生IllegalStateException,或者内容直接被忽略。这是个特别经典的坑,我见过不少新手在forward后面加日志,然后发现日志打了,页面却乱码了,就是这个原因。
forward的另一个特征是它在服务端内部完成,所以可以拿到原始请求的全部信息。包括请求参数、请求头、Cookie,最关键的还有通过setAttribute放进去的对象。
request.setAttribute("user", userInfo); request.getRequestDispatcher("/profile.jsp").forward(request, response);在profile.jsp里直接用${user.username}就能拿到来。这个机制在MVC模式下特别常用:Controller负责查数据,然后把数据放到request里,forward给JSP渲染。整个链条是一次事务性的内部流转,如果中间抛了异常,整个请求也就断了。
2.2 Forward的三种代码写法,别只记得一种
在纯Servlet时代,写得最多的就是我上面那种request.getRequestDispatcher(...).forward(...)。但到了Spring MVC时代,写法发生了很大变化。如果你在Controller里写:
@RequestMapping("/login") public String login() { // 处理登录逻辑 return "home"; }这个返回值"home"在不加任何特殊前缀的情况下,默认是通过RequestDispatcher转发到名为home的视图(InternalResourceViewResolver会解析成/WEB-INF/views/home.jsp),这其实就是一次forward。也就是说,Spring MVC里绝大多数Controller返回视图名称,底层都是转发。
如果你在Servlet里想用相对路径,注意getRequestDispatcher里的路径必须以/开头,并且相对于当前应用的上下文路径(contextPath),不能跨应用转发。你没法在一个应用里forward到另一个Web应用的资源,因为RequestDispatcher只认当前ServletContext内的路径。即便你写了绝对路径http://localhost:8080/otherApp/xxx,也会报错,这是forward和redirect的又一个重要差异——转发不能跨域,重定向可以。
还有一种写法是使用<jsp:forward>标签,但现在纯JSP架构的项目已经很少了,知道存在即可,实际开发里尽量别在JSP里做控制逻辑,转发动作应该收敛在Servlet或Controller层。
2.3 Forward的适用场景:什么时候必须用它
我用一个很实际的需求来说明。假设你在做一个后台管理系统,登录成功之后要跳转到用户首页。这里如果直接redirect到首页,你会发现登录时塞到session里的用户信息还在,没问题。但如果首页展示的数据需要依赖登录校验过程中的一些中间计算结果,比如权限列表、菜单树,这些计算是基于request临时对象完成的,用forward可以一站式传递给首页渲染,省去再次查询数据库的开销。
更典型的场景是表单提交后校验失败。用户在注册页填了资料,提交到Servlet,Servlet校验发现邮箱格式不对,这时候最好的做法是forward回注册页,并且把用户填过的表单内容通过request.setAttribute("formData", formData)带回去,让页面回显。如果用redirect,因为第二次请求完全独立,你没地方放request属性(session可以放但会带来清理问题),回显就变得很别扭。
还有WEB-INF这个目录下的资源。WEB-INF下的JSP文件不能通过URL直接访问,但可以通过forward访问到。这也是MVC设计里把视图放到WEB-INF下防止直接访问的核心原因。你写request.getRequestDispatcher("/WEB-INF/views/dashboard.jsp").forward(request, response);就能访问,而浏览器直接输/WEB-INF/views/dashboard.jsp会返回404。这种"不暴露物理视图路径"的安全性,也是forward独有的优势。
3. 请求重定向(redirect)的运行原理与使用场景
3.1 Redirect的HTTP交互全流程
redirect的本质是HTTP协议中的3xx状态码应答。服务器在收到请求后,并不直接产出最终页面,而是返回一个重定向响应。最常用的是302 Found,响应头里带着Location字段。
用代码触发一个重定向:
response.sendRedirect("/home");这行代码执行后,你拿抓包工具看,会先看到一个HTTP/1.1 302,响应头Location: /home。然后浏览器自动发起第二次请求,地址变成/home。如果/home本身还要跳,那就有第三次……整个过程用户无感,但事实上产生了多次网络往返。
我用几个关键点总结一下redirect和forward在协议层的区别:
| 维度 | forward | redirect |
|---|---|---|
| 浏览器地址栏 | 不变 | 变成目标URL |
| 请求次数 | 1次 | 至少2次 |
| request属性 | 可以共享 | 无法共享 |
| session属性 | 可以共享 | 可以共享 |
| 速度 | 快(无网络往返) | 慢(多一次往返) |
| 跨域 | 不能 | 可以 |
| 访问WEB-INF | 可以 | 不可以 |
| URL暴露 | 不暴露实际路径 | 暴露目标地址 |
注意header里的Location可以是相对路径也可以是绝对路径。response.sendRedirect("home")是相对于当前请求的URL解析,response.sendRedirect("/home")是相对于整个应用的contextPath解析。这个细节特别容易出错,后面我再讲路径坑。
3.2 Redirect如何在Spring MVC中显式声明
在Spring MVC里,通过返回值前缀"redirect:"来触发重定向:
@RequestMapping("/submit") public String submit(Form form) { // 处理数据 return "redirect:/success"; }这个"redirect:/success"会被Spring解析成response.sendRedirect(request.getContextPath() + "/success"),自动加上contextPath,避免你手写路径时漏掉项目名。这个设计非常贴心。但需要注意,如果你用了RedirectAttributes,可以把参数拼到URL后面,不过Spring推荐的方式是这样:
@RequestMapping("/submit") public String submit(Form form, RedirectAttributes ra) { // 处理数据 ra.addAttribute("id", form.getId()); ra.addFlashAttribute("message", "保存成功"); return "redirect:/detail"; }addAttribute会把id拼到URL上变成/detail?id=1,addFlashAttribute会把message放进session,重定向后的请求立刻取出并删除(flash机制)。这种方式是解决redirect无法携带request属性这个问题的最佳实践,也是面试加分项。
另外,forward:前缀在Spring MVC里同样存在:return "forward:/detail"可以强制走转发,并且能保留request属性。
3.3 Redirect的核心价值:POST/Redirect/GET模式
实际开发中redirect最典型的价值是解决表单重复提交问题。假设一个转账表单POST /transfer,Servlet处理完直接forward回结果页。用户考虑一下要不要再转一次,按了F5刷新,浏览器弹窗提示"确认重新提交表单吗"?一旦确认,钱就转了两次。但如果你处理完表单后redirect到一个GET接口/transferResult,浏览器第二次请求是GET,刷新页面只是在刷新结果页,不会再次提交POST数据。
这就是经典的PRG模式(Post/Redirect/Get)。所有涉及写操作的Web应用都应该遵守:先POST处理,再重定向到GET结果页。这也是为什么很多框架在表单提交后的默认行为是重定向而不是转发。
另外一个场景是登录后的跳转。用户未登录访问/cart,拦截器发现未登录,通常会redirect到登录页/login,并在URL后面带上?target=/cart。登录成功后根据这个参数再redirect回购物车。如果用forward,浏览器地址栏还是/cart,用户会误以为登录失败,而且刷新还会重复提交。这类涉及URL变化的场景,redirect几乎是唯一正确选择。
还有跨域跳转。比如从老系统迁移到新系统,旧的URLhttp://old.example.com/path要跳到http://new.example.com/path,response.sendRedirect("http://new.example.com/path")一行搞定,forward做不到。
4. 实操对比:用一个小项目把两者跑透
4.1 环境准备与基础代码
空谈理论没意思,我直接带你做一个极简的Servlet项目,用同样的功能分别实现转发和重定向,看运行日志和浏览器行为。
项目结构就是一个标准的Maven webapp:
src/main/java/com/example/DemoServlet.java src/main/java/com/example/TargetServlet.javaDemoServlet负责入口,TargetServlet负责输出结果。启动后在浏览器访问http://localhost:8080/demo/demo。
先看DemoServlet:
package com.example; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/demo") public class DemoServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean useForward = Boolean.parseBoolean(request.getParameter("forward")); request.setAttribute("fromDemo", "我是DemoServlet放进去的数据"); if (useForward) { request.getRequestDispatcher("/target").forward(request, response); } else { response.sendRedirect("/demo/target"); } } }TargetServlet:
package com.example; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/target") public class TargetServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); response.getWriter().write("<html><body>"); response.getWriter().write("<h1>这是TargetServlet的输出</h1>"); Object fromDemo = request.getAttribute("fromDemo"); response.getWriter().write("<p>request中来自DemoServlet的数据: " + fromDemo + "</p>"); response.getWriter().write("</body></html>"); } }4.2 观察转发和重定向的行为差异
测试一:访问http://localhost:8080/demo/demo?forward=true。
你会在浏览器地址栏看到URL始终是/demo/demo,页面内容却是TargetServlet输出的那个h1标签。再看服务器日志,DemoServlet和TargetServlet的doGet方法各执行了一次,但只有一个HTTP请求。TargetServlet里打印request.getAttribute("fromDemo"),能得到"我是DemoServlet放进去的数据",证明request对象是同一个。
测试二:访问http://localhost:8080/demo/demo?forward=false。
这时浏览器地址栏立刻变成/demo/target。TargetServlet里打印request.getAttribute("fromDemo"),得到的是null。为什么?因为redirect触发后,浏览器向/demo/target发起了全新请求,旧的request对象已经销毁。
我用curl能很清楚地看到重定向的响应流程:
curl -v "http://localhost:8080/demo/demo?forward=false"输出里会有:
< HTTP/1.1 302 < Location: /demo/target然后curl默认不会跟随重定向,只会停在302响应。加上-L参数后,curl会追随Location再去请求/demo/target。这个实验也印证了一个结论:response.sendRedirect()在执行完之后,当前方法不一定会立即终止,sendRedirect后面如果还有代码,同样会继续执行,除非你主动return。这一点跟forward一样,都容易踩坑。
4.3 路径陷阱与contextPath的坑
我在排错时发现,很多人纠结于forward和redirect的路径写法。这里有一个通用法则:
forward里getRequestDispatcher(String path)的path必须以/开头,并且是相对于应用上下文根(不含contextPath)。比如应用部署在/demo下,你要转发到/WEB-INF/views/home.jsp,就写/WEB-INF/views/home.jsp,而不是/demo/WEB-INF/views/home.jsp。response.sendRedirect(String location)的location可以写绝对路径,也可以写相对路径。写/demo/target表示从域名根开始,需要自己带contextPath;写target表示相对于当前URL的上一级目录,很容易理解错。所以我建议生产代码里统一用sendRedirect(request.getContextPath() + "/target"),这样无论应用部署在根路径还是某个次级路径下,都能正确跳转。
Spring MVC中return "redirect:/target"会自动拼contextPath,而你写return "redirect:target"(不带斜杠)的解析规则又不同,可能拼出奇怪的结果。解决办法就是始终以/开头,交给框架处理。
5. 常见问题与排查技巧实录
5.1 重定向后request属性丢失怎么办
这是问得最多的一个问题。代码里明明request.setAttribute("msg", "成功"),然后sendRedirect,到了目标页取出来是null。原因上面讲过了:第二次请求是全新的,request不是同一个对象。解决方案有三个:
- 改用
forward,前提是你要跳转的资源在同一个应用内,且不需要改变URL。 - 把数据放进session,重定向后在目标页取出,然后立刻
session.removeAttribute,避免脏数据残留。 - 拼到URL后面,比如
sendRedirect("/target?msg=success"),注意敏感数据不能这么传。
Spring MVC的RedirectAttributes底层就是实现了方案2和3的结合:addFlashAttribute走session flash,addAttribute拼URL参数。这一点在框架层面已经帮我们封装好了,学习Servlet阶段理解原理会更透彻。
5.2 重定向循环(redirect loop)与retrying问题
还有一类问题在调试时特别头疼:访问一个URL,浏览器一直转圈,最后报"此页面无法正常工作"或者ERR_TOO_MANY_REDIRECTS。这通常是因为/a重定向到/b,而/b又重定向回/a,形成了死循环。
如果你用代码发起请求,比如用Python的requests库,就会看到类似这样的报错信息:
requests.exceptions.TooManyRedirects: Exceeded 30 redirects.或者像很多HTTP客户端会在日志里打印:
Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None))这个retrying日志其实是某些网络库在自动处理重定向时发出的。比如requests库默认会跟随重定向,但可以设置allow_redirects=False禁用。而一些基于urllib3的客户端会自动重试连接,当服务器不断返回重定向地址时,客户端会反复尝试,最终超过重试上限抛出异常。这个现象恰恰说明redirect会引发客户端额外的请求行为,如果你在写爬虫或者接口调用,遇到这种retry日志,先检查一下是不是服务器端配置了重定向循环。
排查重定向循环的思路也很简单:先看响应头。用curl不跟随重定向,手动打印两次:
curl -I http://localhost:8080/a看返回的Location,再请求Location指向的地址,一路跟下去,找到哪一环出了问题。最常见的场景是登录拦截器:用户未登录访问/user,拦截器重定向到/login,而/login又要求必须登录后才能访问,于是循环了。解决办法是在放行规则里排除/login。
5.3 转发页面出现IllegalStateException怎么办
还是回到forward之后继续写response的问题。出现这个异常时先检查代码里有没有在forward之后又调用response.getWriter()或response.getOutputStream()。实际开发中我一个建议是:在forward或redirect之后,立刻return,从控制流上杜绝这类问题。比如:
if (needForward) { request.getRequestDispatcher("/target").forward(request, response); return; }这样写既是良好的习惯,也避免后续代码干扰已提交的响应。如果你需要做日志记录,放在forward之前执行,或者用过滤器在请求结束前统一处理。
5.4 重定向后中文参数乱码
无论forward还是redirect,只要参数经过URL传输就存在编码问题。而redirect因为天然依赖URL,所以遇到中文参数更容易乱码。发送端用URLEncoder.encode编码,接收端解码:
String name = "张三"; response.sendRedirect("/target?name=" + URLEncoder.encode(name, "UTF-8"));Spring MVC的RedirectAttributes.addAttribute已经帮我们处理了URL编码。但如果你在Servlet里手写,千万别忘了这一步。还有一点,Tomcat 8.0之后GET请求的URI编码默认是UTF-8,所以你只要编码和解码统一用UTF-8,问题不大。老版本Tomcat可能需要在server.xml里配置URIEncoding,但新项目基本不用管了。
5.5 转发到JSP后样式和JS丢失
这个问题看着跟forward没关系,其实是路径问题。如果你的页面是http://localhost:8080/demo/user/detail.jsp,在JSP里引用CSS用了相对路径css/style.css,浏览器会解析成/demo/user/css/style.css。当通过forward跳转过来时,浏览器地址栏仍然是/demo/user/detail,所以相对路径的基准变了,样式资源自然404。
解决办法是不要让资源引用使用相对路径,改用绝对路径:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">本质上这跟forward没有直接关系,而是任何URL发生了改变后都需要重新审视基准路径。但因为redirect会直观改变地址栏,大家反而容易注意到;forward不会改地址,样式丢了才莫名其妙。这种隐蔽性更容易浪费时间排查。
6. 工具选型与框架层面的最佳实践
6.1 Servlet规范里的RequestDispatcher.forward与HttpServletResponse.sendRedirect
开发中你只需要记住两个核心API:javax.servlet.RequestDispatcher.forward()和javax.servlet.http.HttpServletResponse.sendRedirect()。前者完成服务端转发,后者让客户端重定向。再牵涉到请求包含(include)就不在这篇讨论范围了。
在Spring MVC中,这两个API被更高层的抽象封装了。ModelAndView里如果设置viewName为"redirect:/xxx",底层就会走sendRedirect;普通的视图解析器默认走forward。从框架设计的角度看,forward是默认,因为多数视图渲染场景都在一个请求周期内完成,效率更高;redirect是显式选择,因为你需要改变URL或者遵守PRG模式。
6.2 过滤器Filter里要特别注意:重定向不是返回
写过滤器时最常见的问题:用户未登录,你调用了response.sendRedirect("/login"),然后方法正常返回,但后续的chain.doFilter仍然执行了,导致请求既被重定向,又进入了目标Servlet。表现为重复执行或者响应头已提交后没法再重定向。正确写法是:
if (!isLogin) { response.sendRedirect("/login"); return; // 关键 } chain.doFilter(request, response);一旦你忘记return,后续代码继续跑,可能抛异常。这也是为什么很多Web框架的拦截器里,重定向之后必须return false或直接结束执行。
6.3 双跳(forward + redirect)的常见模式
有时候一个请求会先forward到一个中间Servlet,中间Servlet再redirect到另一个URL。这种嵌套本身不复杂,但容易让人误解。记住一条原则:一次请求可以多次forward,但只能有一次redirect决定新一轮请求的去向。forward是在当前请求内移交控制权,redirect是结束当前请求,让浏览器开始新请求。
如果你在同一个Servlet里既forward又redirect,顺序很重要。先forward,再redirect,redirect会覆盖之前写入的响应内容(如果response没有提交的话)。如果response已经提交,sendRedirect会抛IllegalStateException。所以别随意叠加,明确你的业务逻辑到底需要哪种跳转。
6.4 现代前后端分离下的redirect与forward
前后端分离的项目里,后端接口返回的不再是页面,而是JSON。此时forward几乎没有用武之地,因为不需要服务端渲染视图。而redirect体现在两种形式:一种是后端返回302,让浏览器整体跳转(比如登录失效跳转登录页),另一种是后端返回{ code: 401, redirectUrl: "/login" },由前端JS控制window.location.href跳转。
还有一种情况是网关层做重定向。Nginx里配置:
rewrite ^/old$ /new redirect;这个redirect对应的是HTTP 302。服务端网关的重定向与Java Web里的sendRedirect本质相同,都是返回一个3xx状态码。理解底层协议后,你到了任何技术栈都不会觉得陌生。
7. 面试高频追问:这些问题问不倒你
我在这里整理几个面试官最喜欢追问的点,顺便看看你对forward和redirect的理解到什么程度。
追问一:重定向是GET还是POST?
redirect最终由浏览器发起的新请求,通常都是GET请求。你无法通过sendRedirect让浏览器发起POST请求。如果业务需要在跳转后是POST,那只能用forward,或者在服务端自己发起HTTP调用(本质是另一个请求,跟重定向无关)。这也是PRG模式成立的基础:把POST重定向成GET,避免刷新重复提交。
追问二:转发时浏览器地址栏不变,那它算不算响应?
forward也形成响应,但响应内容直接由目标资源产生,浏览器收到的只是一个最终响应,没有中间过程。所以浏览器感知不到forward的存在。这带来一个安全特性:目标资源的真实路径不暴露,但同时对用户来说,刷新页面会再次提交原始请求(如果原始请求是POST),可能引发重复提交。
追问三:sendRedirect和RequestDispatcher.forward对服务器资源开销有什么不同?
forward因为不涉及网络往返,性能更高。redirect需要浏览器再发起一次连接,多一次RTT。但在用户操作场景,这个差异通常是毫秒级,几乎感知不到。真正需要关注的是redirect导致的额外数据库查询或业务计算:目标接口被独立请求一次,相比forward直接共享request数据,可能多一次数据加载。
追问四:可以forward到外部网址吗?
不可以。RequestDispatcher只能获取当前应用内部资源。外部网址必须用redirect或者自己用HttpClient发起请求再包装返回。
追问五:Servlet中forward和include有什么区别?
include是把目标资源的输出包含到当前响应中,目标资源可以写入response,但不能修改响应头。forward则是完全移交控制权,当前Servlet的输出会被清空。这个扩展问题也能看出一个人对Servlet容器的熟悉程度。
8. 我踩过的一些坑和最终建议
做跳转逻辑踩过的坑,数一数还真不少。我记得刚工作那会儿写了一个下载功能,下载完成后想回到上一步页面,我用forward跳回去,结果浏览器直接开始重复提交了上一步的POST请求,差点把测试数据搞重复。后来才知道,这种"完成后回跳"必须用redirect,并且最好配合PRG模式。
还有一次排查一个诡异的问题:客户反馈有时候页面卡住,看日志发现Servlet里forward调用后没有return,后面的代码又往response里写了一些调试输出,导致某些情况下响应已经提交,最后的跳转逻辑反而抛异常。改掉这个坏习惯后问题就消失了。
总结我个人的做法:
- 视图渲染、校验失败回显、需要共享request数据,用
forward。 - 登录跳转、表单提交成功后的跳转、跨应用跨域跳转、需要改变地址栏的场景,用
redirect。 - 任何跳转后立刻写
return,除非你有明确理由需要继续执行。 - 涉及URL路径,统一用
request.getContextPath()拼接,别写死。 - 前端分离的项目,把跳转逻辑尽量往前端收敛,后端返回状态码和跳转地址即可。
如果你在开发中能把这几个原则落实到代码里,forward和redirect的区别就已经超过了网上大部分教程的口头层面,真正成为你的实践经验。
最后再分享一个排查小技巧:当你不确定当前代码是走转发还是重定向时,别靠猜,直接在浏览器控制台打开Network面板,看初次请求的响应状态码。如果是200且响应内容是目标页的HTML,那是forward;如果是302,接下来还有一个新请求,那是redirect。一眼就分辨出来,比自己读代码快多了。这一点在实际联调和线上问题排查中特别管用。