Skip to content

视图解析与视图技术

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视图类型配置特点
InternalResourceViewResolverJSP/JSTL简单配置,前后缀拼接
ThymeleafViewResolverThymeleaf 模板配置模板引擎
FreeMarkerViewResolverFreeMarker 模板配置 FreeMarker 配置
ContentNegotiatingViewResolver内容协商根据 Accept 头选择视图
BeanNameViewResolverBean 名称直接使用容器中的 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>

视图技术对比

特性JSPThymeleafFreeMarker
学习曲线
模板语法JSTL/ELHTML 属性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 的全局异常处理机制。

基于 VitePress 构建