目录
[四、请求转发 和 请求重定向](#四、请求转发 和 请求重定向)
[2.请求转发 / 重定向的代码演示:](#2.请求转发 / 重定向的代码演示:)
一、拾枝杂谈
1.什么是视图?
- 定位 :视图是 MVC 架构中的 "V" 层。它的核心职责是 "渲染"。简单来说,它负责把模型数据(Model)通过某种技术手段(如 JSP、Thymeleaf等)转化为浏览器能够识别的 响应内容。
- 种类 :如下:
- InternalResourceView:最常见的 JSP 视图。
- JstlView:支持 JSTL 标签库的 JSP 视图。
- RedirectView :处理 重定向的特殊视图。
- MappingJackson2JsonView:处理 JSON 响应的视图(常用于 RESTful 接口)。
2.视图解析器是干什么用的?
- 核心任务 :Controller 方法通常只返回一个简单的字符串(即 逻辑视图名 ,如 "success
")。但 DispatcherServlet 并不认得这个字符串。视图解析器的作用,就是 根据这个逻辑名,找到或创建对应的视图对象(View)。- 多解析器机制:SpringMVC 允许同时配置多个视图解析器。它们像"接力赛"一样工作:DispatcherServlet 会按顺序询问每个解析器:"这个视图名你能解析吗?"。如果能,就返回 View 对象;如果不能,就交给下一个解析器处理。
3.为什么需要自定义视图?
在默认情况下,我们最常用的是 InternalResourceViewResolver。它非常"固执":它总是机械地把你的逻辑视图名加上前缀 和后缀(例如:/WEB-INF/pages/ + success + .jsp)。
既然有了默认,为什么还要自定义? 默认的处理方式本质上只能处理"页面跳转"。但在复杂的业务场景下,我们会有特殊的需求:比如 特殊格式的输出 :有时候我们不想返回 HTML,而是想根据数据生成一个 Excel 报表 、一个 PDF 文档 ,或者一个动态二维码 。再比如,对于某些复杂的渲染逻辑(比如 权限过滤 后再展示)如果不适合写在 Controller 里,也不适合写在 JSP 里,那么封装成一个独立的 "自定义视图类" 是最好的选择,可以实现 解耦 的目的。
4.自定义视图基本介绍:
简而言之,自定义视图 就是由开发者手动编写一个 Java 类来实现 org.springframework.web.servlet.View 接口,从而接管渲染逻辑。
想要在 SpringMVC 中跑通自定义视图,需要完成以下++"三部曲":++
- 定义自己的视图类 : 创建一个 Java 类,继承 AbstractView(这是 View 接口的常用适配器类)。然后我们需要重写 renderMergedOutputModel 方法。在这个方法里,我们可以通过 request和 response这两个形参列表中的对象随心所欲地控制输出内容,比如手动转发请求,或者直接往输出流里写数据。
- 将自己的视图类 注册为 Spring 容器组件 : 在我们的自定义视图类上标注 @Component,并给它起一个唯一的名字(这个名字就是未来的逻辑视图名)。
- 配置 BeanName 视图解析器 : 这是串联的关键!我们需要在 Spring 配置文件中配置一个名为 BeanNameViewResolver的解析器。例如,当 Controller 返回一个字符串(如 "cyanView")时,BeanNameViewResolver会去 Spring 容器中找 id="cyanView" 的那个 Bean。如果找到了,并且这个 Bean 实现了 View接口,它就会调用该 Bean 的渲染方法。
二、自定义视图实例
1.需求:
先是有一个请求页面 ,然后请求页面发出 REST 请求打到我们的 Handler ;这时候 Handler 会调用我们提前配置好的SpringMVC 的一个 自定义视图解析器 ;然后这个自定义视图解析器就会找到我们的 自定义视图 ;自定义视图进一步找到 结果页面 ,并最终将结果页面返回给发出请求的页面(浏览器)。如下图所示:
2.代码实现:
好的,接下来我们就按照上图的步骤,依次完成一个 自定义视图 的简单应用案例。
首先我们来定义一个发出请求的 view.jsp 页面。view.jsp 代码如下:
html
<%--
User : Cyan_RA9
Version : 24.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>自定义视图测试</title>
</head>
<body>
<h3 style="color: purple">测试自定义视图</h3>
<a href="student/homework">点我 帮我找到自定义视图 哈哈哈~</a>
</body>
</html>
然后,我们在 viewresolver 包下定义一个 Handler,up 这里以 StudentHandler 作为测试,如下图所示:

StudentHandler 类代码如下:
java
package com.cyan.web.viewresolver;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author : Cyan_RA9
* @version : 23.0
*/
@RequestMapping(value = "/student")
@Controller
public class StudentHandler {
@RequestMapping(value = "/homework")
public String submitHomework() {
System.out.println("submitHomework - 交作业?");
return "cyanView";
}
}
接着,就是配置我们的 自定义视图解析器,applicationContext-mvc.xml 代码如下:
XML
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置要扫描的包 -->
<context:component-scan base-package="com.cyan.web"/>
<!-- 激活注解(开启 Spring MVC 注解驱动) --> <!-- IMPORTANT -->
<mvc:annotation-driven/>
<!-- 配置静态资源放行 -->
<mvc:default-servlet-handler/>
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--
视图解析器需要配置两个属性------prefix(前缀) 和 suffix(后缀),
视图解析器收到Servlet的 "return "login_OK";" 时,会用prefix和suffix与返回的资源名拼接。
-->
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 配置自定义的视图解析器 -->
<!--
1. SpringMVC 提供的 BeanNameViewResolver 可以解析自定义视图.
2. order 属性表示 执行顺序, order 值越小, 执行优先级反而越高.
3. order 默认值的大小是 Integer.MAX_VALUE, 即 2147483647
-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="77"/>
</bean>
</beans>
继续,下一步是 定义我们的 自定义视图 ,CyanView 类代码如下:
java
package com.cyan.web.viewresolver;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.view.AbstractView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* 1. Decorate with @Component annotation to register it as a component
* 2. Inheriting components from the AbstractView class can be used as a View
* 3. The purpose of this class is to complete the view rendering
* and determine the result page to be redirected to.
*/
@Component(value = "cyanView")
public class CyanView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
System.out.println("~Enter Custom View~");
//request forwarding
/*
Notice:
1. In the field of request forwarding, / denotes the root
directory of current Web application.
2. So /WEB-INF/pages/view_OK.jsp will be resolved to
/SpringMVC/WEB-INF/pages/view_OK.jsp
*/
httpServletRequest.getRequestDispatcher("/WEB-INF/pages/view_OK.jsp")
.forward(httpServletRequest, httpServletResponse);
}
}
最后一步,定义返回结果的页面 view_OK.jsp,代码如下:
html
<%--
User : Cyan_RA9
Version : 24.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>原神牛逼</title>
</head>
<body>
<h2 style="color: darkslateblue">This is the final returned result page</h2>
</body>
</html>
测试结果如下 GIF图所示:

三、视图解析器的执行流程
1.自定义视图解析器的执行流程:
当我们配置了自定义视图 (如 cyanView)并使用了 自定义视图解析器 BeanNameViewResolver 时,其核心逻辑在于从 Spring IOC 容器中寻找同名的 Bean。
首先是入口调度,在 DispatcherServlet .java 的核心方法中,会遍历所有已注册的视图解析器,相关源码如下:
java
// 源码摘自 DispatcherServlet.java
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
// 依次调用每个解析器的 resolveViewName
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
当循环执行到 BeanNameViewResolver 时,它会进入其内部实现,相关源码如下:
java
// 源码摘自 BeanNameViewResolver.java
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
if (!context.containsBean(viewName)) {
// 如果容器中没有这个名字的 Bean,返回 null,DispatcherServlet 会继续找下一个解析器
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
// 如果找到了但不是 View 类型,也返回 null
return null;
}
// 从容器中获取该 View 实例并返回
return context.getBean(viewName, View.class);
}
这么来看,BeanNameViewResolver 的流程是很清晰的------查容器、验类型、返结果。找不到就报 null,也不会阻塞后续流程。
2.默认视图解析器的执行流程:
InternalResourceViewResolver 继承自 UrlBasedViewResolver吗,它的逻辑是 "预判式"的,即无论物理文件是否存在,它通常都会构建出一个视图对象。
所以它的第一步就是 创建视图对象, 它会进入 UrlBasedViewResolver.java 的 createView 方法,相关源码如下:
java
// 源码摘自 UrlBasedViewResolver.java
@Override
protected View createView(String viewName, Locale locale) throws Exception {
// 检查是否能处理该视图名(比如是否有指定的转发前缀)
if (!canHandle(viewName, locale)) {
return null;
}
// 处理 "forward:" 前缀逻辑
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
// ...处理 "redirect:" 逻辑等
// 调用父类方法进行标准的视图构建
return super.createView(viewName, locale);
}
第二步,它会开始拼接前后缀,最终会调用 buildView 方法,这里完成了我们熟悉的字符串拼接,相关源码如下:
java
// 源码摘自 InternalResourceViewResolver.java (通过父类实现)
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 实例化具体的视图类,即 InternalResourceView
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
// 拼接前缀和后缀:getPrefix() + viewName + getSuffix()
view.setUrl(getPrefix() + viewName + getSuffix());
return view;
}
总结的话就是,默认解析器不会检查 JSP 文件是否真实存在。它只要通过了 canHandle 校验,就会强行拼接路径并返回一个 View 对象。
3.多个视图解析器执行流程:
当项目中同时存在多个解析器时,顺序(Priority/Order)决定了生死。对于执行顺序的问题,核心就在于 DispatcherServlet 的循环逻辑,如下:
java
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
注意,这里的 if (view != null) 是所有逻辑的关键。
如果 默认解析器 优先级高于 自定义解析器 ,去处理 自定义视图,当循环开始时,会++先调用 InternalResourceViewResolver++ ,即便我们传入的是自定义视图 "cyanView",它也会认为这是一个普通的 JSP 请求。
但是这里的 关键点 就在于它返回了一个非空的 View 对象。此时 循环由于 view != null 直接 return,我们配置的自定义视图解析器 BeanNameViewResolver 根本没有出场机会。这样的话,由于 /WEB-INF/pages/cyanView.jsp 并不存在,最终会报 404 错误。
但是呢,如果自定义视图解析器此时的优先级高于默认视图解析器,那么同样是处理自定义视图,自然就不会报错了。
还有一种情况------
如果 自定义解析器 优先级高于 默认解析器 ,去处理 普通视图 ,当循环开始,++先调用 BeanNameViewResolver++,它在容器中查找名为 "success" 的 Bean,发现没有,此时它会直接返回一个 null,那么循环继续,接着调用下一个视图解析器,也就是默认视图解析器 InternalResourceViewResolver,拼接出 /WEB-INF/pages/success.jsp,返回 View 对象,最终解析成功,正常跳转。
这么来看,我们可以总结出下面两条结论:
1. 默认视图解析器必须放到最后。因为它太"霸道"了,由于它在逻辑上总是能通过字符串拼接生成一个 View 对象(返回值不为 null),因此一旦它被执行,后续的所有视图解析器都会被"屏蔽"。
2. 具有"明确识别能力"的解析器(如 BeanNameViewResolver、FreeMarkerViewResolver)应该放在前面。因为它们找不到会返回 null,从而触发 DispatcherServlet 循环的下一次迭代。而"兜底"的 InternalResourceViewResolver 必须放在末尾。
四、请求转发 和 请求重定向
1.请求转发和请求重定向的回顾:
我们之前在讲到 JavaWeb 时,已经讲过了 请求转发 和 请求重定向,包括代码演示、使用细节。大家如果不太清晰了,可以点击下面链接,回顾一下:
JavaWeb 速通Servlet(请求转发和请求重定向)_请求转发代码-CSDN博客https://blog.csdn.net/tyra9/article/details/131851094
2.请求转发 / 重定向的代码演示:
先来演示一下请求转发。
在刚刚用过的 view.jsp 页面新增一个 <a> 标签来发出请求,代码如下:
html
<a href="student/do">点我 测试请求转发 到WEB-INF目录下面的资源~</a>
在 StudentHandler 类中新增一个方法用于测试 请求转发 ,代码如下:
java
@RequestMapping(value = "/do")
public String doHomework() {
System.out.println("doHomework - 写作业?");
return "forward:/WEB-INF/pages/view_OK.jsp";
}
运行结果如下 GIF 所示:

再来演示一下 请求重定向。
继续,在 view.jsp 页面,新增一个 <a> 标签来发出请求,代码如下:
html
<a href="student/doEX">点我 测试请求重定向 到一个不知道什么的页面~</a>
在 StudentHandler 类中新增一个方法用于测试 请求转发 ,代码如下:
java
@RequestMapping(value = "/doEX")
public String doHomeworkEX() {
System.out.println("doHomeworkEX - 写作业EX");
//注意, 这里的 重定向 的URL 和传统 Servlet 不一样!
return "redirect:/model_data.jsp";
}
运行结果如下 GIF 所示:

3.请求转发的流程分析:
在 SpringMVC 中,Handler 的处理默认 行为就是**"请求转发"**。当你返回一个简单的逻辑视图名(如 "success")时,最终都会由 InternalResourceView通过转发实现。当然,我们也可以显式地使用 forward: 前缀。
当我们在方法中返回 "forward:/target.jsp" 时,UrlBasedViewResolver 会截获此请求。相关源码如下:
java
// 源码摘自 UrlBasedViewResolver.java
@Override
protected View createView(String viewName, Locale locale) throws Exception {
// 检查 "forward:" 前缀
if (viewName.startsWith(FORWARD_URL_PREFIX)) { // FORWARD_URL_PREFIX = "forward:"
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
// 创建一个 InternalResourceView,此时它不会被拼接前缀和后缀
InternalResourceView view = new InternalResourceView(forwardUrl);
return view;
}
// ...
}
随后,在渲染阶段,InternalResourceView 会调用其核心方法执行真正的转发,相关源码如下:
java
// 源码摘自 InternalResourceView.java
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. 将模型数据放入 Request 域对象中
exposeModelAsRequestAttributes(model, request);
// 2. 获取转发路径
String dispatcherPath = prepareForRendering(request, response);
// 3. 获取 RequestDispatcher
RequestDispatcher rd = request.getRequestDispatcher(dispatcherPath);
// 4. 执行原生的 Servlet 转发动作
rd.forward(request, response);
}
在 forward:/WEB-INF/pages/ok.jsp 这种写法中,开头的 / 代表的是当前 Web 应用的根目录。由于转发是在服务器内部进行的,服务器"已经身在工程内",因此这个 / 能够直接定位到工程根部,并且能够突破 WEB-INF 目录的访问限制。
4.请求重定向的流程分析:
当我们使用 redirect:/index.jsp 时,SpringMVC 会通过 RedirectView来处理。需要特别注意:请求重定向绝对不能定向到 WEB-INF 目录下。因为 WEB-INF 是 Tomcat 保护的内部目录,重定向是浏览器发起的第二次全新请求,浏览器作为"外部人员"是无法穿透服务器安全防线访问该目录的。
RedirectView 是处理重定向的核心,它的渲染逻辑如下:
java
// 源码摘自 RedirectView.java
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 创建目标 URL
String targetUrl = createTargetUrl(model, request);
// 2. 执行重定向
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
// 核心跳转方法
protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String targetUrl, boolean http10Compatible) throws IOException {
// 关键优化:检查 contextRelative 属性
if (isContextRelative() && targetUrl.startsWith("/")) {
// Spring 自动为路径拼接了当前项目的 ContextPath (工程名)
targetUrl = getContextPath(request) + targetUrl;
}
// 调用原生 response 执行跳转,发送 302 状态码
response.sendRedirect(response.encodeRedirectURL(targetUrl));
}
需要注意的是,以前在传统的 response.sendRedirect() 中,开头的 / 代表的是服务器根目录(localhost:8080/),因此我们必须手动拼接工程名(如 /SpringMVC/index.jsp)。但在 SpringMVC 中,RedirectView 默认开启了 contextRelative 优化。当我们写 redirect:/index.jsp 时:Spring 会首先识别出开头的 /,它会自动通过 request.getContextPath() 获取当前的工程名,它会在底层默默地为你拼好了完整路径。因此,在 SpringMVC 中进行重定向,绝对不要手动写工程名,直接以 / 开头即可,剩下的交给框架自动适配。
Δ总结
- 🆗,以上就是本篇博文的全部内容了,感谢阅读!
- 我们先是简单了解了视图 和 视图解析器的概念,以及自定义视图的实现步骤,随后立马便进行了自定义视图的代码演示。后面up 还分享了视图解析器的执行流程,以及Spring MVC 中请求转发和请求重定向的流程分析,并且在关键步骤也贴出了源码,可以说是很详实了。
- 大家也不要眼高手低,可以自己动手跟着写写、敲一敲。我们下期再见!
