目录
视图技术概述
在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
- 自然模板: HTML模板可以直接在浏览器中预览
- Spring集成: 与Spring Boot完美集成
- 表达式语法: 强大的表达式语言
- 类型安全: 编译时类型检查
- 国际化: 内置国际化支持
基本示例
<!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 > 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>© 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>© 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. 最佳实践
- 合理的目录结构设计
- 模板复用和组件化
- 性能优化和缓存配置
- 安全防护措施
技术选择建议
- 中小型项目: 推荐使用JSP + JSTL,学习成本低,功能完整
- 现代化应用: 推荐使用Thymeleaf,与Spring Boot完美集成
- 复杂模板: 推荐使用FreeMarker,模板功能强大,性能优秀
- API项目: 使用@RestController + JSON,避免视图技术复杂度
下一步学习方向
掌握了视图技术和数据处理模块后,建议继续学习:
- 表单处理和验证模块 - 了解复杂的Web表单处理
- 高级特性模块 - 掌握拦截器、异常处理、文件上传等
- 测试和集成模块 - 学习如何测试和部署Spring MVC应用
- Spring Boot集成 - 了解更现代化的配置和开发方式
选择合适的技术组合,遵循最佳实践,将帮助您构建出高质量、可维护的Web应用程序。
1127

被折叠的 条评论
为什么被折叠?



