视图解析与视图技术
Controller 执行完毕,返回一个字符串 "user/list",这个字符串是怎么变成用户看到的页面的?
答案就是 ViewResolver——视图解析器。
视图解析的工作原理
┌─────────────────────────────────────────────────────────────────────────┐
│ 视图解析流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Controller 返回 "user/list" │
│ │ │
│ ▼ │
│ DispatcherServlet 调用 ViewResolver │
│ │ │
│ ▼ │
│ ViewResolver 根据配置拼接完整路径 │
│ │ │
│ │ 例如:prefix + "user/list" + suffix │
│ │ = "/WEB-INF/views/" + "user/list" + ".jsp" │
│ │ = "/WEB-INF/views/user/list.jsp" │
│ ▼ │
│ 返回 View 对象 │
│ │ │
│ ▼ │
│ View 渲染数据到模板 │
│ │ │
│ ▼ │
│ 响应返回给浏览器 │
│ │
└─────────────────────────────────────────────────────────────────────────┘ViewResolver 体系
接口定义
java
public interface ViewResolver {
// 根据视图名解析为 View 对象
View resolveViewName(String viewName, Locale locale) throws Exception;
}
public interface View {
// 渲染视图
void render(@Nullable Map<String, <?> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception;
String getContentType();
}常见的 ViewResolver 实现
| ViewResolver | 视图类型 | 配置特点 |
|---|---|---|
InternalResourceViewResolver | JSP/JSTL | 简单配置,前后缀拼接 |
ThymeleafViewResolver | Thymeleaf 模板 | 配置模板引擎 |
FreeMarkerViewResolver | FreeMarker 模板 | 配置 FreeMarker 配置 |
ContentNegotiatingViewResolver | 内容协商 | 根据 Accept 头选择视图 |
BeanNameViewResolver | Bean 名称 | 直接使用容器中的 View Bean |
JSP 视图解析
配置 InternalResourceViewResolver
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// 方式一:简洁配置
registry.jsp("/WEB-INF/views/", ".jsp");
// 方式二:完整配置
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setContentType("text/html;charset=UTF-8");
// 设置视图顺序(数值越小优先级越高)
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
registry.viewResolver(resolver);
}
}JSP 页面示例
jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>用户列表</title>
</head>
<body>
<h1>用户列表</h1>
<table>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
</tr>
<c:forEach items="${users}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
</tr>
</c:forEach>
</table>
</body>
</html>Thymeleaf 视图解析
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>Thymeleaf 配置
java
@Configuration
public class ThymeleafConfig {
@Bean
public ITemplateEngine templateEngine(ISpringWebFluxTemplateEngine webFluxEngine) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver());
// 启用 Spring 5 的文本模板模式(更安全)
engine.setEnableSpringELCompiler(true);
return engine;
}
@Bean
public ITemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/"); // 模板目录
resolver.setSuffix(".html"); // 模板后缀
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false); // 开发环境禁用缓存
return resolver;
}
}Thymeleaf 模板示例
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}">用户列表</title>
</head>
<body>
<h1>用户列表</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}">1</td>
<td th:text="${user.username}">zhangsan</td>
<td th:text="${user.email}">zhangsan@example.com</td>
<td>
<a th:href="@{/user/edit/{id}(id=${user.id})}">编辑</a>
<a th:href="@{/user/delete/{id}(id=${user.id})}">删除</a>
</td>
</tr>
</tbody>
</table>
<!-- 条件渲染 -->
<div th:if="${not #lists.isEmpty(users)}">
共 <span th:text="${users.size()}">0</span> 条记录
</div>
<!-- 表单提交 -->
<form th:action="@{/user/save}" method="post">
<input type="text" name="username" th:value="${user?.username}">
<button type="submit">提交</button>
</form>
</body>
</html>Thymeleaf 常用语法
html
<!-- 文本渲染 -->
<span th:text="${user.name}">默认值</span>
<span th:utext="${user.htmlContent}">HTML 内容(不转义)</span>
<!-- URL 链接 -->
<a th:href="@{/user/{id}(id=${user.id})}">查看详情</a>
<a th:href="@{/user(id=${user.id}, action='delete')}">删除</a>
<!-- 条件判断 -->
<div th:if="${user.status == 1}">正常</div>
<div th:unless="${user.status == 1}">禁用</div>
<!-- 循环 -->
<tr th:each="user, stat : ${users}">
<td th:text="${stat.count}">1</td>
<td th:text="${user.name}"></td>
</tr>
<!-- 局部替换 -->
<div th:insert="~{fragment :: footer}"></div>
<div th:replace="~{fragment :: footer}"></div>
<!-- 内联表达式 -->
<script th:inline="javascript">
var name = [[${user.name}]]; // 自动转 JSON
var url = /*[[@{/api/user}]]*/ '/default/url';
</script>FreeMarker 视图解析
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>FreeMarker 配置
java
@Configuration
public class FreeMarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker/");
configurer.setDefaultEncoding("UTF-8");
Properties settings = new Properties();
settings.setProperty("template_update_delay", "0");
settings.setProperty("default_encoding", "UTF-8");
configurer.setFreemarkerSettings(settings);
return configurer;
}
@Bean
public ViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setSuffix(".ftl");
resolver.setContentType("text/html;charset=UTF-8");
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
}FreeMarker 模板示例
freemarker
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<h1>用户列表</h1>
<table>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
</tr>
<#list users as user>
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
</tr>
</#list>
</table>
<!-- 条件判断 -->
<#if users?has_content>
<p>共 ${users?size} 条记录</p>
<#else>
<p>暂无数据</p>
</#if>
<!-- 内建函数 -->
<p>当前时间: ${now?string('yyyy-MM-dd HH:mm:ss')}</p>
<p>数字格式化: ${price?string.currency}</p>
</body>
</html>视图技术对比
| 特性 | JSP | Thymeleaf | FreeMarker |
|---|---|---|---|
| 学习曲线 | 低 | 中 | 中 |
| 模板语法 | JSTL/EL | HTML 属性 | FreeMarker 语法 |
| 与 HTML 融合 | 一般 | 优秀 | 一般 |
| 处理空值 | 需要 JSTL | 自动转义 | 需要处理 |
| 性能 | 高 | 中 | 高 |
| 生态 | 成熟 | 活跃 | 成熟 |
| 推荐指数 | 旧项目 | 新项目首选 | 中 |
内容协商视图解析器
ContentNegotiatingViewResolver 可以根据请求的 Accept 头选择合适的视图:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
// 基于文件扩展名
.favorParameter(true)
.parameterName("format")
// 支持的媒体类型
.mediaType("html", MediaType.TEXT_HTML)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}java
// 请求示例
// GET /user/list.json 返回 JSON
// GET /user/list.xml 返回 XML
// GET /user/list 根据 Accept 头决定自定义视图
实现 View 接口
java
@Component("myView")
public class MyCustomView implements View {
@Override
public void render(@Nullable Map<String, ?> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
response.getWriter().write("<h1>自定义视图</h1>");
for (Map.Entry<String, ?> entry : model.entrySet()) {
response.getWriter().write("<p>" + entry.getKey() + ": " + entry.getValue() + "</p>");
}
}
@Override
public String getContentType() {
return "text/html;charset=UTF-8";
}
}返回自定义视图
java
@GetMapping("/custom")
public String customView(Model model) {
model.addAttribute("message", "Hello Custom View");
return "myView"; // 返回 Bean 名称
}视图解析的优先级
如果配置了多个 ViewResolver,它们会按顺序遍历:
java
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// 先 JSP
registry.jsp("/WEB-INF/views/", ".jsp");
}java
@Override
public void extendViewResolvers(List<ViewResolver> resolvers) {
// 可以手动调整顺序
resolvers.add(0, myCustomViewResolver);
}ViewResolver 的 order 属性决定了优先级,数字越小优先级越高。
面试追问
Q1: forward 和 redirect 在视图解析中的区别?
java
// forward:服务器内部转发,不改变 URL
// 视图名前加 "forward:" 前缀
return "forward:/user/list";
// redirect:客户端重定向,改变 URL
// 视图名前加 "redirect:" 前缀
return "redirect:/user/list";Q2: Model、ModelMap、ModelAndView 的区别?
| 类型 | 特点 |
|---|---|
Model | 接口,只提供添加属性的方法 |
ModelMap | 类,实现了 Map 接口,可以当 Map 使用 |
ModelAndView | 同时包含模型数据和视图名 |
java
// 三种方式效果相同
public String method1(Model model) {
model.addAttribute("data", "value");
return "view";
}
public String method2(ModelMap modelMap) {
modelMap.addAttribute("data", "value");
return "view";
}
public ModelAndView method3() {
ModelAndView mav = new ModelAndView("view");
mav.addObject("data", "value");
return mav;
}Q3: Thymeleaf 为什么比 JSP 更适合前后端分离?
Thymeleaf 使用 HTML 属性(如 th:text),模板本身是合法的 HTML 文件,可以直接在浏览器中打开预览。而 JSP 需要服务器渲染才能看到效果。
下节预告:异常处理:@ExceptionHandler 与 @ControllerAdvice —— 掌握 Spring MVC 的全局异常处理机制。
