ViewResolver
本章将视角从 RESTful API 转回传统服务器端渲染。在前后端分离成为主流之前,Spring MVC 的核心工作模式是"Controller 处理数据 → View 渲染页面 → 返回 HTML"。
ViewResolver就是这个链路中承上启下的关键组件——它把 Controller 返回的逻辑视图名(如"employee-list")解析为物理视图路径(如"/WEB-INF/views/employee-list.jsp"),让视图引擎找到正确的模板文件。
定义与作用
ViewResolver 是 Spring MVC 的视图解析器接口,职责是:根据 Controller 返回的逻辑视图名,查找并返回对应的 View 对象。
Controller 方法通常返回 String 类型的视图名:
@GetMapping("/employees")
public String list(Model model) {
model.addAttribute("employees", empList);
return "employee-list"; // 这是逻辑视图名,不是文件路径
}
"employee-list" 只是一个逻辑名称,真正的模板文件可能位于:
/WEB-INF/views/employee-list.jsp(JSP)/templates/employee-list.html(Thymeleaf)/WEB-INF/freemarker/employee-list.ftl(FreeMarker)
ViewResolver 负责把这个逻辑名映射到实际的物理位置,并创建对应的 View 实例。
生活类比:酒店房间预订
想象飞翔科技的行政人员为员工预订酒店:
- Controller:行政人员说"订一间标准间"(返回逻辑视图名
"standard-room") - ViewResolver:预订系统根据"标准间"这个逻辑名称,查询到实际房间是"3 楼 305 号房"(解析为物理路径)
- View:305 号房本身,包含床、电视、卫生间等设施(模板文件中的标签、样式、脚本)
- 渲染:员工入住后,房间里的设施被实际使用(数据填充到模板,生成最终 HTML)
ViewResolver 就是那位"预订系统"——你只需说房间类型,它负责找到具体房号。
核心原理
视图解析过程
关键机制:
- 前缀后缀拼接:
ViewResolver通过prefix和suffix将逻辑名包装为完整路径 - 视图缓存:解析后的
View对象通常会被缓存,避免重复查找文件 - 链式解析:Spring MVC 支持配置多个
ViewResolver,按顺序尝试,第一个成功即停止 - Locale 支持:部分
ViewResolver支持国际化,根据用户语言解析不同语言的模板
适用位置与常用属性
ViewResolver 作为 Spring Bean,在 配置类 或 XML 配置 中声明。
常见实现对比
| 实现类 | 模板技术 | 典型前缀 | 典型后缀 | 适用场景 |
|---|---|---|---|---|
InternalResourceViewResolver | JSP | /WEB-INF/views/ | .jsp | 传统 JSP 项目 |
ThymeleafViewResolver | Thymeleaf | /templates/ | .html | Spring Boot 默认 |
FreeMarkerViewResolver | FreeMarker | /templates/ | .ftl | 高性能模板需求 |
UrlBasedViewResolver | 通用 | 自定义 | 自定义 | 特殊路径规则 |
InternalResourceViewResolver 常用属性
| 属性 | 说明 | 示例 |
|---|---|---|
prefix | 视图文件路径前缀 | /WEB-INF/views/ |
suffix | 视图文件扩展名 | .jsp |
viewClass | 视图实现类 | JstlView.class |
cache | 是否缓存视图 | true(默认) |
order | 解析器优先级 | 数值越小优先级越高 |
完整示例
场景
飞翔科技员工管理系统使用 Thymeleaf 作为模板引擎。运维李眉部署时发现,所有视图文件放在 src/main/resources/templates/ 目录下。架构师白歌配置了 ThymeleafViewResolver,将 Controller 返回的逻辑名解析为 .html 文件。
代码实现
Spring Boot 自动配置(零配置):
Spring Boot 自动注册 ThymeleafViewResolver,默认配置:
- 前缀:
classpath:/templates/ - 后缀:
.html
// EmployeeController.java
package com.feixiang.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Arrays;
import java.util.List;
@Controller
@RequestMapping("/employees")
public class EmployeeController {
@GetMapping
public String list(Model model) {
List<Employee> employees = Arrays.asList(
new Employee("张三", "技术部", 25000),
new Employee("李四", "产品部", 22000)
);
model.addAttribute("employees", employees);
return "employee-list"; // 解析为 /templates/employee-list.html
}
@GetMapping("/detail")
public String detail(Model model) {
model.addAttribute("employee", new Employee("张三", "技术部", 25000));
return "employee/detail"; // 解析为 /templates/employee/detail.html
}
}
传统 Spring MVC 显式配置:
// WebConfig.java
package com.feixiang.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
public class WebConfig {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setCache(true);
return resolver;
}
}
前缀后缀拼接示例
| 逻辑视图名 | 前缀 | 后缀 | 物理路径 |
|---|---|---|---|
employee-list | /templates/ | .html | /templates/employee-list.html |
employee/detail | /templates/ | .html | /templates/employee/detail.html |
admin/dashboard | /WEB-INF/views/ | .jsp | /WEB-INF/views/admin/dashboard.jsp |
report | /WEB-INF/freemarker/ | .ftl | /WEB-INF/freemarker/report.ftl |
HTTP 请求示例
示例 1:Thymeleaf 视图渲染
curl "http://localhost:8080/employees"
响应(Content-Type: text/html):
<!DOCTYPE html>
<html>
<head><title>员工列表</title></head>
<body>
<h1>员工列表</h1>
<table>
<tr><th>姓名</th><th>部门</th><th>薪资</th></tr>
<tr><td>张三</td><td>技术部</td><td>25000</td></tr>
<tr><td>李四</td><td>产品部</td><td>22000</td></tr>
</table>
</body>
</html>
示例 2:视图名包含子目录
curl "http://localhost:8080/employees/detail"
ViewResolver 将 "employee/detail" 解析为 /templates/employee/detail.html,支持多级目录结构。
易错场景与面试考点
误区一:视图文件放错位置
现象:Controller 返回 "employee-list",但报错 TemplateInputException: Error resolving template。
排查清单:
| 检查项 | Thymeleaf | JSP |
|---|---|---|
| 文件位置 | src/main/resources/templates/employee-list.html | src/main/webapp/WEB-INF/views/employee-list.jsp |
| 前缀配置 | 默认 /templates/ | 需显式设置 /WEB-INF/views/ |
| 后缀配置 | 默认 .html | 需显式设置 .jsp |
| 缓存问题 | 开发时关闭缓存:spring.thymeleaf.cache=false | 检查 cache 属性 |
误区二:@RestController 下配置 ViewResolver 无效
现象:@RestController 类中的方法返回视图名,但客户端收到纯文本。
纠正:@RestController 的所有方法默认带 @ResponseBody,返回值直接序列化,不经过 ViewResolver。需要视图渲染时,必须使用 @Controller。
误区三:多个 ViewResolver 顺序问题
现象:配置了 Thymeleaf 和 JSP 两个 ViewResolver,但总是用 JSP 解析,Thymeleaf 模板被忽略。
纠正:通过 order 属性控制优先级,数值越小越优先:
@Bean
public ViewResolver thymeleafViewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setOrder(1); // 优先尝试 Thymeleaf
return resolver;
}
@Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setOrder(2); // Thymeleaf 失败后再尝试 JSP
return resolver;
}
面试高频:ViewResolver 的工作流程
标准回答:
- Controller 方法返回逻辑视图名(如
"employee-list") - DispatcherServlet 将视图名和 Locale 传给 ViewResolver
- ViewResolver 通过
prefix + 视图名 + suffix拼接物理路径 - 检查文件是否存在,存在则创建对应的
View对象返回 - DispatcherServlet 调用
View.render(),传入 Model 数据 - 视图引擎读取模板文件,用 Model 数据替换变量,生成 HTML
- HTML 写入 HTTP 响应体返回给客户端
小结
ViewResolver 是 Spring MVC 视图渲染链路的"寻址系统"。它将 Controller 返回的抽象逻辑视图名,通过前缀后缀规则映射为具体的模板文件路径,让视图引擎能够找到并渲染正确的页面。
核心要点:
ViewResolver只负责"找文件",不负责"渲染",渲染由View完成- 常见实现:
InternalResourceViewResolver(JSP)、ThymeleafViewResolver(Thymeleaf) - 前缀 + 视图名 + 后缀 = 物理路径
- 多个
ViewResolver可通过order属性设置优先级 @RestController不走视图解析,需要渲染时用@Controller
本章与全局的关系:本章讲解了"视图名如何映射到模板文件"。下一节 ModelAndView 将讲解如何在 Controller 中同时控制视图名和数据,适合动态决定视图的场景。