Spring MVC 视图技术和数据处理教程

目录

  1. 视图技术概述
  2. 视图解析器详解
  3. JSP技术深入
  4. JSTL标签库使用
  5. Thymeleaf模板引擎
  6. FreeMarker模板引擎
  7. 数据绑定机制
  8. 模型数据处理
  9. 国际化支持
  10. 静态资源处理
  11. 视图技术对比
  12. 最佳实践
  13. 常见问题解决
  14. 总结

视图技术概述

在Spring MVC中,视图技术负责将控制器处理后的数据渲染为最终的HTML页面或其他格式的响应。Spring MVC支持多种视图技术,每种都有其特点和适用场景。

视图技术分类

1. 传统视图技术

  • JSP (JavaServer Pages)
  • JSTL (JSP Standard Tag Library)
  • Servlet
  • HTML

2. 现代模板引擎

  • Thymeleaf
  • FreeMarker
  • Velocity (已停止维护)
  • Mustache

3. 客户端渲染技术

  • Angular
  • React
  • Vue.js
  • 原生JavaScript

Spring MVC支持的视图技术

<!-- 传统JSP支持 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

<!-- Thymeleaf模板引擎 -->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    <version>3.0.15.RELEASE</version>
</dependency>

<!-- FreeMarker模板引擎 -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

视图解析器详解

ViewResolver接口层次结构

public interface ViewResolver {
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

// 主要实现类
- AbstractCachingViewResolver     // 抽象缓存视图解析器
  - UrlBasedViewResolver         // 基于URL的视图解析器
    - InternalResourceViewResolver // JSP视图解析器
  - AbstractView
    - AbstractTemplateView
      - FreeMarkerView           // FreeMarker视图
      - ThymeleafView           // Thymeleaf视图

InternalResourceViewResolver (JSP)

基本配置

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"/>
    <property name="suffix" value=".jsp"/>
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="order" value="1"/>
</bean>

高级配置

@Configuration
public class ViewConfig implements WebMvcConfigurer {
    
    @Bean
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        resolver.setExposedContextBeanNames(new String[]{"appName"});
        resolver.setOrder(1);
        return resolver;
    }
}

ThymeleafViewResolver

配置示例

@Configuration
public class ThymeleafConfig {
    
    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        engine.setDialect(new SpringSecurityDialect());
        return engine;
    }
    
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("/WEB-INF/thymeleaf/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false); // 开发时禁用缓存
        return resolver;
    }
    
    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
}

FreeMarkerViewResolver

配置示例

@Configuration
public class FreeMarkerConfig {
    
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/WEB-INF/freemarker/");
        configurer.setDefaultEncoding("UTF-8");
        
        Properties props = new Properties();
        props.put("template_exception_handler", "rethrow");
        props.put("number_format", "0.######");
        configurer.setFreemarkerSettings(props);
        
        Map<String, Object> variables = new HashMap<>();
        variables.put("appName", "Spring MVC App");
        configurer.setFreemarkerVariables(variables);
        
        return configurer;
    }
    
    @Bean
    public FreeMarkerViewResolver freeMarkerViewResolver() {
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
        resolver.setSuffix(".ftl");
        resolver.setCache(true);
        resolver.setContentType("text/html; charset=UTF-8");
        resolver.setOrder(1);
        return resolver;
    }
}

多个视图解析器配置

配置多个视图解析器

@Configuration
public class MultiViewConfig implements WebMvcConfigurer {
    
    @Bean
    @Primary
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setOrder(1);
        return resolver;
    }
    
    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setOrder(2);
        return resolver;
    }
    
    @Bean
    public FreeMarkerViewResolver freeMarkerViewResolver() {
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
        resolver.setSuffix(".ftl");
        resolver.setOrder(3);
        return resolver;
    }
}

JSP技术深入

JSP基础语法

1. JSP指令

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.Date, com.example.model.User" %>
<%@ page session="false" %>
<%@ page errorPage="error.jsp" %>
<%@ page isErrorPage="false" %>

2. JSP脚本元素

<!-- 表达式:输出到页面 -->
用户姓名:<%= user.getName() %>

<!-- 脚本片段:Java代码 -->
<%
    String theme = request.getParameter("theme");
    if ("dark".equals(theme)) {
        session.setAttribute("theme", "dark");
    }
%>

<!-- 声明:定义方法和变量 -->
<%!
    private String formatDate(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(date);
    }
%>

JSP内置对象

常用内置对象使用

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>JSP内置对象示例</title>
</head>
<body>
    <h1>JSP内置对象</h1>
    
    <!-- request对象 -->
    <p>请求URI: <%= request.getRequestURI() %></p>
    <p>请求方法: <%= request.getMethod() %></p>
    <p>用户代理: <%= request.getHeader("User-Agent") %></p>
    
    <!-- response对象 -->
    <p>响应状态: <%= response.getStatus() %></p>
    
    <!-- session对象 -->
    <p>Session ID: <%= session.getId() %></p>
    <%
        Integer visitCount = (Integer) session.getAttribute("visitCount");
        if (visitCount == null) {
            visitCount = 1;
        } else {
            visitCount++;
        }
        session.setAttribute("visitCount", visitCount);
    %>
    <p>访问次数: <%= visitCount %></p>
    
    <!-- application对象 -->
    <p>应用名称: <%= application.getServletContextName() %></p>
    <p>上下文路径: <%= application.getContextPath() %></p>
    
    <!-- out对象 -->
    <%
        out.println("<p>使用out对象输出的内容</p>");
    %>
    
    <!-- pageContext对象 -->
    <p>当前页面: <%= pageContext.getPage().getClass().getName() %></p>
</body>
</html>

JSP指令详解

1. page指令

<%@ page 
    contentType="text/html;charset=UTF-8"    // 响应内容类型
    language="java"                          // 使用的语言
    import="java.util.*,com.example.*"     // 导入的包
    session="true"                          // 是否使用session
    buffer="8kb"                            // 缓冲区大小
    autoFlush="true"                        // 自动刷新缓冲区
    isThreadSafe="true"                     // 是否线程安全
    errorPage="error.jsp"                   // 错误页面
    isErrorPage="false"                     // 是否是错误页面
    info="用户详情页面"                      // 页面描述信息
    pageEncoding="UTF-8"                    // 页面编码
%>

2. include指令

<!-- 静态包含 -->
<%@ include file="header.jsp" %>
<%@ include file="navigation.jsp" %>

<body>
    <!-- 页面主要内容 -->
</body>

<%@ include file="footer.jsp" %>

3. taglib指令

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

JSTL标签库使用

JSTL核心标签库

1. 条件处理标签

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!-- if标签:条件判断 -->
<c:if test="${not empty user}">
    <p>欢迎,${user.name}!</p>
</c:if>

<!-- choose标签:多条件选择 -->
<c:choose>
    <c:when test="${user.role == 'admin'}">
        <p>管理员界面</p>
    </c:when>
    <c:when test="${user.role == 'user'}">
        <p>普通用户界面</p>
    </c:when>
    <c:otherwise>
        <p>游客界面</p>
    </c:otherwise>
</c:choose>

2. 循环处理标签

<!-- forEach标签:遍历集合 -->
<table border="1">
    <thead>
        <tr>
            <th>ID</.th>
            <th>姓名</th>
            <th>邮箱</th>
            <th>状态</th>
        </tr>
    </thead>
    <tbody>
        <c:forEach items="${users}" var="user" varStatus="status">
            <tr class="${status.index % 2 == 0 ? 'even' : 'odd'}">
                <td>${user.id}</td>
                <td>${user.name}</td>
                <td>${user.email}</td>
                <td>
                    <c:choose>
                        <c:when test="${user.active}">
                            <span class="text-success">活跃</span>
                        </c:when>
                        <c:otherwise>
                            <span class="text-danger">非活跃</span>
                        </c:otherwise>
                    </c:choose>
                </td>
            </tr>
        </c:forEach>
    </tbody>
</table>

<!-- 数字循环 -->
<c:forEach begin="1" end="10" var="i">
    <p>第${i}项</p>
</c:forEach>

3. 对象操作标签

<!-- set标签:设置变量 -->
<c:set var="loggedInUser" value="${sessionScope.user}" scope="session"/>
<c:set var="pageTitle" value="用户管理" scope="request"/>

<!-- remove标签:移除变量 -->
<c:remove var="tempData" scope="session"/>

<!-- url标签:URL构造 -->
<a href="<c:url value='/user/edit/${user.id}'/>">编辑用户</a>
<a href="<c:url value='/logout' context='/app'/>">退出登录</a>

<!-- redirect标签:重定向 -->
<c:if test="${empty sessionScope.user}">
    <c:redirect url="/login"/>
</c:if>

4. 异常处理标签

<%@ page isErrorPage="true" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>
<head>
    <title>错误页面</title>
</head>
<body>
    <h1>发生错误</h1>
    
    <!-- catch标签:异常捕获 -->
    <c:catch var="exception">
        <%
            // 可能抛出异常的代码
            String result = "Hello".substring(10);
        %>
    </c:catch>
    
    <c:if test="${not empty exception}">
        <p>错误信息: ${exception.message}</p>
    </c:if>
</body>
</html>

JSTL格式标签库

引入fmt标签库

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

日期时间格式化

<!-- 时间戳 -->
<jsp:useBean id="now" class="java.util.Date"/>
<p>当前时间: <fmt:formatDate value="${now}" pattern="yyyy-MM-dd HH:mm:ss"/></p>

<!-- 用户创建时间 -->
<p>创建时间: <fmt:formatDate value="${user.createTime}" 
                              pattern="yyyy年MM月dd日 EEEE"/> </p>

<!-- 生日显示 -->
<p>生日: <fmt:formatDate value="${user.birthday}" pattern="yyyy-MM-dd"/></p>

数字格式化

<!-- 货币格式化 -->
<p>价格: <fmt:formatNumber value="${product.price}" type="currency" currencyCode="CNY"/></p>

<!-- 数字格式化 -->
<p>得分: <fmt:formatNumber value="${score}" type="number" maxFractionDigits="2"/></p>

<!-- 百分比格式化 -->
<p>完成度: <fmt:formatNumber value="${progress}" type="percent" maxFractionDigits="1"/></p>

JSTL函数标签库

引入fn标签库

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

字符串处理

<!-- 字符串长度 -->
<p>消息长度: ${fn:length(message)}</p>

<!-- 字符串替换 -->
<p>处理后的内容: ${fn:replace(content, '\n', '<br/>')}</p>

<!-- 字符串分割 -->
<c:set var="tags" value="${fn:split(user.tags, ',')}"/>
<c:forEach items="${tags}" var="tag">
    <span class="tag">${fn:trim(tag)}</span>
</c:forEach>

<!-- 字符串判断 -->
<c:choose>
    <c:when test="${fn:contains(message, '重要')}">
        <p class="alert">重要消息</p>
    </c:when>
    <c:when test="${fn:startsWith(message, '紧急')}">
        <p class="emergency">紧急消息</p>
    </c:when>
</c:choose>

<!-- 字符串大小写转换 -->
<p>大写: ${fn:toUpperCase(user.name)}</p>
<p>小写: ${fn:toLowerCase(user.email)}</p>

<!-- 数组连接 -->
<c:set var="fullName" value="${fn:join([user.firstName, user.lastName], ' ')}"/>

Thymeleaf模板引擎

Thymeleaf基础

为什么选择Thymeleaf

  1. 自然模板: HTML模板可以直接在浏览器中预览
  2. Spring集成: 与Spring Boot完美集成
  3. 表达式语法: 强大的表达式语言
  4. 类型安全: 编译时类型检查
  5. 国际化: 内置国际化支持

基本示例

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
</head>
<body>
    <h1 th:text="${pageTitle}">页面标题</h1>
    
    <div th:if="${not empty users}">
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>姓名</th>
                    <th>邮箱</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="user : ${users}">
                    <td th:text="${user.id}">1</td>
                    <td th:text="${user.name}">张三</td>
                    <td th:text="${user.email}">zhangsan@example.com</td>
                </tr>
            </tbody>
        </table>
    </div>
    
    <div th:unless="${not empty users}">
        <p>暂无用户数据</p>
    </div>
</body>
</html>

Thymeleaf语法

1. 文本处理

<!-- 文本内容 -->
<p th:text="${user.name}">用户名</p>

<!-- HTML内容 -->
<p th:utext="${user.description}">用户描述</p>

<!-- 无转义文本 -->
<span th:inline="text">欢迎 [[${user.name}]]!</span>

<!-- 条件文本 -->
<p th:text="${user.active} ? '活跃' : '非活跃'">状态</p>

2. 属性处理

<!-- 设置属性 -->
<input type="text" th:value="${user.name}" th:id="user-${user.id}"/>

<!-- 条件属性 -->
<img th:src="${user.avatar}" th:alt="${user.name}" 
     th:onerror="this.src='/images/default-avatar.png'"/>

<!-- 动态属性名 -->
<div th:attrappend="class=${'user-' + user.role}">用户信息</div>

<!-- 多值属性 -->
<select multiple th:attr="multiple='multiple'">
    <option th:each="option : ${options}" 
            th:value="${option.value}" 
            th:text="${option.label}">选项</option>
</select>

3. 条件和循环

<!-- 条件判断 -->
<div th:if="${user.role == 'admin'}">
    管理员专用功能
</div>

<div th:unless="${user.role == 'admin'}">
    普通用户功能
</div>

<!-- switch语句 -->
<div th:switch="${user.status}">
    <p th:case="'active'" class="success">用户活跃</p>
    <p th:case="'inactive'" class="warning">用户非活跃</p>
    <p th:case="*" class="info">状态未知</p>
</div>

<!-- 循环遍历 -->
<ul th:each="item : ${items}">
    <li th:text="${item}">项目</li>
</ul>

<!-- 带状态的循环 -->
<table>
    <tr th:each="user, stat : ${users}" 
        th:class="${stat.odd} ? 'odd' : 'even'">
        <td th:text="${stat.index + 1}">1</td>
        <td th:text="${user.name}">姓名</td>
        <td th:text="${user.email}">邮箱</td>
    </tr>
</table>

4. 表单处理

<!-- Spring表单绑定 -->
<form th:action="@{/user/save}" method="post">
    <input type="hidden" th:field="*{id}"/>
    
    <div>
        <label>用户名:</label>
        <input type="text" th:field="*{username}" 
               th:class="${#fields.hasErrors('username')} ? 'error'"/>
        <span th:if="${#fields.hasErrors('username')}" 
              th:errors="*{username}" class="error">错误信息</span>
    </div>
    
    <div>
        <label>邮箱:</label>
        <input type="email" th:field="*{email}"/>
    </div>
    
    <button type="submit">保存</button>
</form>

<!-- 单选按钮 -->
<div th:each="role : ${roles}">
    <input type="radio" th:field="*{role}" 
           th:value="${role.value}" th:id="${role.value}"/>
    <label th:for="${role.value}" th:text="${role.label}">角色</label>
</div>

<!-- 复选框 -->
<fieldset>
    <legend>爱好</legend>
    <div th:each="hobby : ${hobbies}">
        <input type="checkbox" th:fifield="*{hobbies}" 
               th:value="${hobby}" th:id="${hobby}"/>
        <label th:for="${hobby}" th:text="${hobby}">爱好</label>
    </div>
</fieldset>

Thymeleaf Spring集成

Spring标签集成

<!-- Spring URL生成 -->
<a th:href="@{/user/edit/{id}(id=${user.id})}">编辑用户</a>
<a th:href="@{/logout}">退出登录</a>

<!-- Spring消息国际化 -->
<h1 th:text="#{page.title}">页面标题</h1>
<p th:text="#{user.welcome(${user.name})}">欢迎消息</p>

<!-- Spring安全检查 -->
<div sec:authorize="hasRole('ADMIN')">
    管理员专用功能
</div>

<div sec:authorize="hasPermission(#user.id, 'com.example.User', 'write')">
    用户编辑功能
</div>

FreeMarker模板引擎

FreeMarker基础配置

依赖配置

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

FreeMarker配置

@Configuration
public class FreeMarkerConfig {
    
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/WEB-INF/freemarker/");
        configurer.setDefaultEncoding("UTF-8");
        
        // FreeMarker设置
        Properties props = new Properties();
        props.put("template_exception_handler", "rethrow");
        props.put("number_format", "0.######");
        props.put("locale", "zh_CN");
        props.put("date_format", "yyyy-MM-dd HH:mm:ss");
        props.put("datetime_format", "yyyy-MM-dd HH:mm:ss");
        configurer.setFreemarkerSettings(props);
        
        return configurer;
    }
    
    
    @Bean
    public FreeMarkerViewResolver freeMarkerViewResolver() {
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
        resolver.setSuffix(".ftl");
        resolver.setPrefix("");
        resolver.setCache(true);
        resolver.setContentType("text/html; charset=UTF-8");
        resolver.setOrder(1);
        return resolver;
    }
}

FreeMarker语法

1. 变量和表达式

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户详情</title>
</head>
<body>
    <!-- 简单变量 -->
    <h1>${user.name}</h1>
    <p>邮箱: ${user.email}</p>
    
    <!-- 复杂表达式 -->
    <p>注册时间: ${user.createTime?string("yyyy年MM月dd日")}</p>
    
    <!-- 条件表达式 -->
    <p>状态: ${user.active?string("活跃", "非活跃")}</p>
    
    <!-- 安全操作符 -->
    <p>等级: ${user.level!0}</p>
    <p>部门: ${(user.department.name)!'未分配'}</p>
</body>
</html>

2. 内置函数

<!-- 字符串处理 -->
<p>姓名长度: ${user.name?length}</p>
<p>大写姓名: ${user.name?upper_case}</p>
<p>小写邮箱: ${user.email?lower_case}</p>

<!-- 数字格式化 -->
<p>得分: ${score?number}</p>
<p>价格: ${price?currency}</p>
<p>百分比: ${progress?string.percent}</p>

<!-- 列表处理 -->
<p>列表大小: ${users?size}</p>
<p>第一个用户: ${users?first.name}</p>
<p>最后一个用户: ${users?last.name}</p>

<!-- 日期格式化 -->
<p>生日: ${user.birthday?date}</p>
<p>创建时间: ${user.createTime?datetime}</p>
<p>最后登录: ${user.lastLoginTime?string("yyyy-MM-dd HH:mm")}</p>

3. 条件判断

<!-- if标签 -->
<#if user.role == "admin">
    <p>管理员功能</p>
<#elseif user.role == "manager">
    <p>经理功能</p>
<#else>
    <p>普通用户功能</p>
</#if>

<!-- switch标签 -->
<#switch user.status>
    <#case "active">
        <span class="status-active">活跃</span>
        <#break>
    <#case "inactive">
        <span class="status-inactive">非活跃</span>
        <#break>
    <#default>
        <span class="status-unknown">未知</span>
</#switch>

4. 循环处理

<!-- list循环 -->
<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        <#list users as users>
            <tr>
                <td>${user_index}</td>  <!-- 索引(从0开始) -->
                <td>${user.name}</td>
                <td>${user.email}</td>
            </tr>
        </#list>
    </tbody>
</table>

<!-- 带状态的循环 -->
<#list items as item>
    <div class="item ${item_index?string}</item> <!-- 奇偶数样式 -->
        <p>项目 ${item_index + 1}: ${item.name}</p>
    </div>
</#list>

<!-- 条件循环 -->
<#list users as user>
    <#if user.active>
        <p>${user.name} (活跃)</p>
    </#if>
</#list>

5. 宏定义

<!-- 定义宏 -->
<#macro printUser user>
    <div class="user-card">
        <h3>${user.name}</h3>
        <p>${user.email}</p>
        <#if user.description??>
            <p>${user.description}</p>
        </#if>
    </div>
</#macro>

<!-- 使用宏 -->
<@printUser user=currentUser/>

<!-- 带参数的宏 -->
<#macro formatDate date format="yyyy-MM-dd">
    ${date?string(format)}
</#macro>

<p>注册日期: <@formatDate user.createTime/></p>
<p>生日: <@formatDate user.birthday "yyyy年MM月dd日"/></p>

<!-- 递归宏 -->
<#macro menuTree items level=0>
    <#list items as item>
        <li style="margin-left: ${level * 20}px">
            ${item.name}
            <#if item.children?? && item.children?size &gt; 0>
                <ul><@menuTree item.children level+1/></ul>
            </#if>
        </li>
    </#list>
</#macro>

数据绑定机制

Model数据传递

1. 使用Model接口

@GetMapping("/user/list")
public String listUsers(Model model) {
    List<User> users = userService.getAllUsers();
    model.addAttribute("users", users);          // 添加对象列表
    model.addAttribute("pageTitle", "用户列表"); // 添加字符串
    model.addAttribute("currentTime", new Date()); // 添加日期对象
    return "user/list";
}

@GetMapping("/user/{id}")
public String userDetail(@PathVariable Long id, Model model) {
    User user = userService.findById(id);
    if (user != null) {
        model.addAttribute(user); // 自动使用类名小写作为属性名
        model.addAttribute("userDetail", user); // 手动指定属性名
    }
    return "user/detail";
}

2. 使用ModelAndView

@RequestMapping("/dashboard")
public ModelAndView getDashboard() {
    ModelAndView mav = new ModelAndView();
    
    // 设置视图名
    mav.setViewName("dashboard");
    
    // 添加数据
    mav.addObject("userCount", userService.getTotalCount());
    mav.addObject("onlineUsers", userService.getOnlineUserCount());
    mav.addObject("statistics", getStatistics());
    
    // 批量添加
    Map<String, Object> data = new HashMap<>();
    data.put("title", "控制面板");
    data.put("timestamp", System.currentTimeMillis());
    mav.addAllObjects(data);
    
    return mav;
}

// 构造函数方式
@RequestMapping("/profile/{id}")
public ModelAndView getUserProfile(@PathVariable Long id) {
    User user = userService.findById(id);
    return new ModelAndView("user/profile", "user", user);
}

@ModelAttribute注解

1. 方法级别@ModelAttribute

@Controller
public class UserController {
    
    // 在每个请求处理前都会执行
    @ModelAttribute("currentUser")
    public User getCurrentUser(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            return (User) session.getAttribute("user");
        }
        return null;
    }
    
    // 只在指定URL的请求前执行
    @ModelAttribute
    public void populateModel(Model model) {
        model.addAttribute("appName", "Spring MVC Demo");
        model.addAttribute("version", "1.0.0");
    }
}

2. 参数级别@ModelAttribute

@PostMapping("/user/create")
public String createUser(@ModelAttribute("userForm") User user, 
                         BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "user/create-form";
    }
    
    userService.save(user);
    return "redirect:/user/list";
}

// 自动绑定(属性名为类名小写)
@PostMapping("/user/update/{id}")
public String updateUser(@PathVariable Long id,
                         @ModelAttribute User user) {
    user.setId(id);
    userService.update(user);
    return "redirect:/user/detail/" + id;
}

数据绑定验证

1. 使用@Valid注解

public class UserForm {
    @NotNull(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度至少6位")
    private String password;
    
    // getters and setters
}

@PostMapping("/user/create")
public String createUser(@Valid @ModelAttribute UserForm userForm,
                         BindingResult bindingResult,
                         Model model) {
    if (bindingResult.hasErrors()) {
        // 验证失败,返回表单页面
        return "user/create-form";
    }
    
    // 验证成功,保存用户
    User user = new User();
    BeanUtils.copyProperties(userForm, user);
    userService.save(user);
    
    return "redirect:/user/list";
}

2. 自定义验证器

@Component
public class UniqueUsernameValidator implements Validator {
    
    @Autowired
    private UserService userService;
    
    @Override
    public boolean supports(Class<?> clazz) {
        return UserForm.class.isAssignableFrom(clazz);
    }
    
    @Override
    public void validate(Object target, Errors errors) {
        UserForm userForm = (UserForm) target;
        
        // 检查用户名唯一性
        if (userForm.getUsername() != null && 
            !userForm.getUsername().trim().isEmpty()) {
            if (userService.existsByUsername(userForm.getUsername())) {
                errors.rejectValue("username", "username.exists", "用户名已存在");
            }
        }
    }
}

最佳实践

1. 视图组织结构

合理的目录结构

WEB-INF/
└── views/
    ├── common/           # 公共组件
    │   ├── header.jsp
    │   ├── footer.jsp
    │   └── navigation.jsp
    ├── layouts/          # 布局模板
    │   ├── default.jsp
    │   └── admin.jsp
    ├── user/             # 用户相关页面
    │   ├── list.jsp
    │   ├── detail.jsp
    │   ├── form.jsp
    │   └── profile.jsp
    ├── admin/             # 管理页面
    │   ├── dashboard.jsp
    │   └── settings.jsp
    └── error/             # 错误页面
        ├── 404.jsp
        ├── 500.jsp
        └── general.jsp

2. 模板复用技术

JSP include机制

<!-- header.jsp -->
<html>
<head>
    <title>${pageTitle} - 网站名称</title>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/common.css"/>
</head>
<body>
    <header>
        <nav><!-- 导航菜单 --></nav>
    </header>
    <main>

<!-- footer.jsp -->
    </main>
    <footer>
        <p>&copy; 2023 Spring MVC Demo</p>
    </footer>
</body>
</html>

<!-- 页面使用 -->
<%@ include file="/WEB-INF/views/common/header.jsp" %>
<div class="content">
    <!-- 页面内容 -->
</div>
<%@ include file="/WEB-INF/views/common/footer.jsp" %>

Thymeleaf布局

<!-- layout/layout.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="${pageTitle} + ' - Site Name'">页面标题</title>
    <link rel="stylesheet" th:href="@{/css/common.css}"/>
</head>
<body>
    <header th:fragment="header">
        <nav><!-- 导航内容 --></nav>
    </header>
    
    <main layout:fragment="content">
        <!-- 主要内容区域 -->
    </main>
    
    <footer th:fragment="footer">
        <p>&copy; 2023 Spring MVC Demo</p>
    </footer>
</body>
</html>

<!-- 页面使用layout -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/layout">
<head>
    <title>用户列表</title>
</head>
<body>
    <div layout:fragment="content">
        <h1 th:text="${pageTitle}">用户列表</h1>
        <table><!-- 用户表格 --></table>
    </div>
</body>
</html>

3. 数据安全处理

防止XSS攻击

<!-- JSP中的输出转义 -->
<p>用户名: <c:out value="${user.name}"/></p>
<!-- 等同于 -->
<p>用户名: ${fn:escapeXml(user.name)}</p>

<!-- HTML内容输出 -->
<p>描述: <c:out value="${user.description}" escapeXml="false"/></p>

Thymeleaf安全输出

<!-- 自动转义 -->
<p th:text="${user.description}">描述</p>

<!-- 不转义HTML -->
<p th:utext="${user.description}">描述</p>

<!-- 不安全的表达式(谨慎使用) -->
<p>消息: <span th:inline="text">[[${message}]] </span></p>

4. 性能优化

JSP编译优化

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>development</param-name>
        <param-value>false</param-value>  <!-- 生产环境关闭开发模式 -->
    </init-param>
    <init-param>
        <param-name>checkInterval</param-name>
        <param-value>0</param-value>      <!-- 不检查文件变更 -->
    </init-param>
</servlet>

缓存配置

// Thymeleaf缓存配置
@Bean
public SpringResourceTemplateResolver templateResolver() {
    SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
    resolver.setCacheable(true);  // 开启缓存
    resolver.setCacheTTLMs(60000L); // 缓存1分钟
    return resolver;
}

// FreeMarker缓存配置  
@Bean
public FreeMarkerViewResolver freeMarkerViewResolver() {
    FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
    resolver.setCache(true);
    return resolver;
}

常见问题解决

1. 中文乱码问题

JSP页面中文乱码

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%
    // 确保响应编码
    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/html; charset=UTF-8");
%>

中文数据显示问题

// 控制器中设置正确的编码
@GetMapping("/search")
public String search(@RequestParam String keyword, Model model) {
    // URL解码中文参数
    try {
        keyword = URLDecoder.decode(keyword, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        // 处理异常
    }
    
    model.addAttribute("keyword", keyword);
    return "search/results";
}

2. JSTL标签库问题

JSTL标签无法识别

<!-- 正确的taglib引入 -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!-- Maven依赖检查 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

自定义标签库创建

// 创建自定义标签处理器
public class HelloTag extends TagSupport {
    private String name;
    
    @Override
    public int doStartTag() throws JspException {
        try {
            pageContext.getOut().print("Hello " + name + "!");
        } catch (IOException e) {
            throw new JspException(e);
        }
        return SKIP_BODY;
    }
    
    // getter and setter for name
}

3. 条件渲染问题

空值检查

<!-- 错误的空值检查 -->
<c:if test="${user == null}">用户不存在</c:if>

<!-- 正确的空值检查 -->
<c:if test="${empty user}">用户不存在</c:if>
<c:if test="${not empty user}">用户: ${user.name}</c:if>
<!-- Thymeleaf中的条件判断 -->
<div th:if="${user != null}">
    <p th:text="${user.name}">用户名</p>
</div>

<div th:unless="${user != null}">
    <p>用户不存在</p>
</div>

4. 循环处理问题

防止空集合错误

<c:if test="${not empty users}">
    <table>
        <c:forEach items="${users}" var="user">
            <tr>
                <td>${user.name}</td>
                <td>${user.email}</td>
            </tr>
        </c:forEach>
    </table>
</c:if>

<c:if test="${empty users}">
    <p>暂无用户数据</p>
</c:if>

数组越界处理

<!-- 安全的数组访问 -->
<c:if test="${fn:length(users) > 0}">
    <p>第一个用户: ${users[0].name}</p>
</c:if>

总结

Spring MVC视图技术和数据处理模块提供了强大的页面渲染能力。本教程涵盖了:

核心技术要点

1. 视图解析器

  • InternalResourceViewResolver用于JSP
  • ThymeleafViewResolver用于HTML模板
  • FreeMarkerViewResolver用于FTL模板
  • 多视图解析器的优先级配置

2. JSP技术

  • JSP基础语法和内置对象
  • JSTL标签库的完整使用
  • 表达式语言(EL)
  • 指令和脚本元素

3. 现代模板引擎

  • Thymeleaf的自然模板特性
  • FreeMarker的强大功能
  • 模板语法的比较和选择
  • Spring集成配置

4. 数据绑定

  • Model和ModelAndView的使用
  • @ModelAttribute注解应用
  • 数据验证和错误处理
  • 安全的数据输出

5. 最佳实践

  • 合理的目录结构设计
  • 模板复用和组件化
  • 性能优化和缓存配置
  • 安全防护措施

技术选择建议

  1. 中小型项目: 推荐使用JSP + JSTL,学习成本低,功能完整
  2. 现代化应用: 推荐使用Thymeleaf,与Spring Boot完美集成
  3. 复杂模板: 推荐使用FreeMarker,模板功能强大,性能优秀
  4. API项目: 使用@RestController + JSON,避免视图技术复杂度

下一步学习方向

掌握了视图技术和数据处理模块后,建议继续学习:

  1. 表单处理和验证模块 - 了解复杂的Web表单处理
  2. 高级特性模块 - 掌握拦截器、异常处理、文件上传等
  3. 测试和集成模块 - 学习如何测试和部署Spring MVC应用
  4. Spring Boot集成 - 了解更现代化的配置和开发方式

选择合适的技术组合,遵循最佳实践,将帮助您构建出高质量、可维护的Web应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小凯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值