第3章〓Spring Boot视图技术







本章学习目标

 掌握Thymeleaf模板引擎。

 掌握Spring Boot处理JSON数据。

 掌握Spring Boot处理国际化问题。

 掌握Spring Boot文件的上传和下载。

 掌握Spring Boot的异常处理。

根据党的二十大报告,必须坚持“创新是第一动力”。创新是一个国家、一个民族发展进步的不竭动力,是推动人类社会进步的重要力量。

Web应用开发是现代软件开发中的重要部分。Spring Boot的Web开发内嵌了Servlet 和服务器,并结合Spring MVC来完成开发。Web开发是一种基于B/S架构的应用软件开发技术,分为前端和后端。前端的可视化及用户交互由浏览器实现,即以浏览器作为客户端,实现客户端与服务器远程的数据交互。

本章通过学习基于用户信息的Web层技术、集成Spring MVC技术和集成静态页面模板技术,学习创新的思维模式,并深刻体会这种时代宝贵的工匠精神。



视频讲解

3.1创建静态Web页面

Spring Boot项目在不使用任何模板引擎的前提下,想要直接访问HTML页面,是如何实现的呢?只需要把静态文件(HTML,JS,CSS)放在resource的static文件夹里面即可正常访问。下面通过示例讲解Spring Boot如何访问静态Web页面。

(1) 创建Spring Boot Web应用springboot0301。

(2) 在项目的src/main/resource/static目录下创建CSS文件夹和js文件,将文件放入对应的文件夹下。

(3) 选中static目录并右击,选择新建HTML文件,命名为index.html。HTML文件代码如下: 

<!DOCTYPE html>

<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org">

<head>

<meta charset="UTF-8">

<title>Title</title>

<link href="css/style.css" rel="stylesheet" type="text/css">

<script src="js/check.js"></script>

</head>

<body>

<div align="center"><br>

<a class="title">用户登录</a><br>

</div>

<form name="form1" method="post" >

<table width="398" height="215" border="1" align="center" cellpadding="0" cellspacing="0">

<tr>

<td width="394" height="213">

<table width="91%" height="80%" border="0" align="center" cellpadding="1" cellspacing="1">

<tr>

<td width="120" align="right">用户名: </td>

<td width="208">

<input name="userid" type="text" id="userid" size="15" maxlength="20">

</td>

</tr>

<tr>

<td width="120" align="right">密码: </td>

<td width="208">

<input name="password" type="password" id="password" size="15" maxlength="20">

</td>

</tr>

<tr>

<td width="120" height="23" align="right">&nbsp;</td>

<td width="208">

<div align="left">

<input type="submit" name="Submit" value="登录" onclick="javascript:return(checkform());">

<input type="reset" name="reset" value="重填">

</div>

</td>

</tr>

</table>

<div align="center"></div>

<div align="center"></div>

<div align="center"></div>

<div align="center"></div>

</td>

</tr>

</table>

</form>

</body>

</html>


(4) 运行程序。

index.html是默认的启动页面,在浏览器地址栏中输入http://localhost:8080/后,结果如图31所示。



图31静态Web页面的运行效果图




视频讲解

3.2Spring Boot对JSP的支持

Spring Boot默认不支持JSP,因为JSP的性能相对较低。官方推荐使用Thymeleaf,如果想在项目中使用JSP,需要进行相关初始化工作。

下面通过实例演示Spring Boot如何访问JSP页面。

(1) 创建Spring Boot Web应用springboot0302。由于Spring Boot使用JSP时需打包为WAR类型,所以在创建项目时修改打包方式,或者在后面的pom.xml文件中进行修改。

(2) 自动生成ServletInitializer类。

打开该类不难发现它继承了SpringBootServletInitializer这个父类,而SpringBootServletInitializer类是Spring Boot提供的Web程序初始化的入口,在使用外部容器(如使用外部Tomcat)运行项目时会自动加载并且装配。

实现SpringBootServletInitializer的子类需要重写一个configure方法,方法内自动根据Springboot0302Application.class的类型创建一个SpringApplicationBuilder,将其交付给Spring Boot框架来完成初始化运行配置。

(3) 整理脚本样式静态文件。

JS脚本、CSS样式、Image等静态文件默认放置在项目的src/main/resource/static目录下。

(4) 配置Spring Boot支持JSP。

打开pom.xml文件(Maven配置文件)可以看到,之前构建项目时已经添加了Web模块。因为在JSP页面中使用EL和JSTL标签显示数据,所以除了添加Servlet和Tomcat依赖外,还需要添加JSTL依赖,具体代码如下。

<!--WAR包部署到外部的Tomcat中已经包含了以下依赖,在此需要对其进行添加否则会与内嵌的Tomcat 容器发生冲突 -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

<scope>provided</scope>

</dependency>

<!-- Servlet依赖 -->

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>javax.servlet-api</artifactId>

</dependency>

<!--配置JSP JSTL的支持-->

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

</dependency>

<!-- 引入Spring Boot内嵌的Tomcat对JSP的解析包,否则无法解析JSP页面-->

<dependency>

<groupId>org.apache.tomcat.embed</groupId>

<artifactId>tomcat-embed-jasper</artifactId>

</dependency>


(5) 配置视图。

必须按照JSP文件之前的文件目录风格配置视图,因此需要在src/main目录下创建webapp/WEBINF/views文件夹。修改application.properties文件,使得Spring MVC支持视图的跳转目录指向为/main/webapp/views,代码如下。

spring.mvc.view.prefix=/WEB-INF/views/

spring.mvc.view.suffix=.jsp


(6) 创建Book实体类、BookController控制器及对应的BookDAO数据层等,具体代码见源文档。

(7) 在WEBINF/views目录下新建booklist.jsp文件,主要展示所有图书的信息,具体代码如下。

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

<%@ page language="java" contentType="text/html; charset=UTF-8"         pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html >

<head>

<title>网上书店</title>

<link href="css/style.css" rel="stylesheet" type="text/css">

</head>

<body>

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

<br><a class="title">在线购书</a><br>

<table width="700" border="1" cellpadding="0" cellspacing="0">

<tr>

<td width="43%" height="26"><div align="center">书名</div></td>

<td width="17%"><div align="center">作者</div></td>

<td width="17%"><div align="center">出版社</div></td>

<td width="5%"><div align="center">价格</div></td>

<td width="9%"><div align="center">操作</div></td>

</tr>

<c:forEach items="${bookList}" var="book">

<tr>

<td height="32"><div align="center">${book.getTitle()}  </div></td>

<td><div align="center">${book.getAuthor()}</div></td>

<td><div align="center">${book.getPublisher()}></div></td>

<td><div align="center">${book.getPrice()}></div></td>

<td><div align="center"><a href="bookdetail.jsp?isbn=${book.get综合业务数字网()}>"><img src="images/buy.gif" width="45" height="16" border="0"></a></div></td>

</tr>

</c:forEach>

</table>

<p>&nbsp;</p>

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

</body>

</html>


(8) 启动测试。

在浏览器地址栏中输入http://localhost:8080/后,在页面中输入任意用户名和密码,单击登录按钮,就可以进入网上书店的首页,如图32所示。




图32JSP页面的运行效果图


3.3Thymeleaf的基本语法 

Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎。与JSP、Velocity、FreeMaker等类似,它也可以轻易地与Spring MVC等Web框架进行集成,以作为Web应用的模板引擎。与其他模板引擎相比,Thymeleaf最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个Web应用。

Thymeleaf支持Spring Expression Language语言作为方言,也就是SpEL,SpEL是可以用于Spring中的一种EL表达式。

简而言之,与以往使用过的JSP不同,Thymeleaf使用HTML的标签来完成逻辑和数据的传入及渲染,而不用像JSP一样作为一个Servlet被编译再生成。即便是单独用Thymeleaf开发的HTML文件,依旧可以正确打开并有少量(相对)有价值的信息,并且是可以被浏览器直接打开的。用Thymeleaf完全替代JSP是可行的。

Thymeleaf的主要作用是把model中的数据渲染到HTML中,因此其语法主要是如何解析model中的数据。
在HTML页面中引入Thymeleaf命名空间,即在HTML模板文件中对动态的属性使用th:命名空间修饰。

<html lang="en" xmlns:th="http://www.thymeleaf.org">


这样才可以在其他标签里面使用th:*这样的语法,这是下面语法的前提。

3.3.1变量表达式 

通过项目springboot0303来学习Thymeleaf的语法。

(1) 新建一个实体类: User

public class User {

private String name;

private Integer age;

private String sex;

} 


(2) 在模型中添加数据: 

@GetMapping("show1")

public String show2(Model model){

User user = new User();

user.setAge(21);

user.setName("张三");

user.setSex("男");

model.addAttribute("user", user);

return "index";

} 


语法说明: 

Thymeleaf通过${}来获取model中的变量,注意这不是EL表达式,而是OGNL表达式。




图33利用OGNL表达式获取数据

示例: 

在页面获取user数据,其效果如图33所示。

<h1>

欢迎您: <span th:text="${user.name}">请登录</span>

</h1>


OGNL表达式的语法与EL表达式几乎是一样的。两者的区别在于,OGNL表达式写在一个名为th:text的标签属性中,该标签即为指令。

Thymeleaf崇尚自然模板,即模板是纯正的HTML代码,脱离模板引擎,在纯静态环境也可以直接运行。如果直接在HTML中编写${}这样的表达式,显然在静态环境下就会出错,这不符合Thymeleaf的理念。

Thymeleaf中所有的表达式都需要写在指令中,指令是HTML5中的自定义属性,在Thymeleaf中的所有指令都是以th:开头的。由于表达式${user.name}是写在自定义属性中的,因此在静态环境下,表达式的内容会被当作是普通字符串,浏览器会自动忽略这些指令,从而不会报错。

现在不经过Spring MVC,而是直接用浏览器打开页面,如图34所示。




图34用浏览器打开页面


在静态环境下,th指令不会被识别,但是浏览器也不会报错,而是把它当作一个普通属性处理。这样,span的默认值“请登录”就会显示在页面上。
如果是在Thymeleaf环境下,th指令就会被识别和解析,而th:text的含义就是替换所在标签中的文本内容,于是user.name的值就替代了span中默认的“请登录”。

指令的设计,正是Thymeleaf的高明之处,也是它优于其他模板引擎的原因。动静结合的设计,使得无论是前端开发还是后端开发都可以完美契合。

需要注意的是,如果不支持这种th:的命名空间写法,那么可以把th:text换成datathtext,Thymeleaf同样可以兼容。

另外,出于安全考虑,th:text指令会把表达式读取到的值进行处理,防止HTML的注入。例如,将字符串“你好”存入到Model中,在页面中可以使用如下两种方式获得消息文本: 

<span th:text="${msg}"></span>  <!--不对HTML标签解析,输出原始内容-->

<span th:utext="${msg}"></span> <!--对HTML标签解析,输出加粗的"你好"-->


3.3.2自定义变量

下面这个例子是分别展示获取到的用户的所有信息。

<h2>

<p>姓名: <span th:text="${user.name}">Jack</span></p>

<p>年龄: <span th:text="${user.age}">21</span></p> 

</h2>


当数据量比较多时,频繁地写user.就会非常麻烦。因此,Thymeleaf提供了自定义变量来解决该问题。

<h2 th:object="${user}">

<p>姓名: <span th:text="*{name}">Jack</span></p>

<p>年龄: <span th:text="*{age}">21</span></p>

</h2>


首先,在h2上用th:object="${user}"获取user的值,并且保存。然后,在h2内部的任意元素上,通过*{属性名}的方式获取user中的属性,这样就省去了大量的user.前缀。th:object声明变量一般情况下会与*{}一起配合使用,达到事半功倍的效果。

3.3.3方法

Thymeleaf通过${}来获取容器上下文环境中的变量,而表达式是OGNL表达式。
OGNL(ObjectGraph Navigation Language),即对象图形化导航语言,它是一种能够方便地操作对象属性的开源表达式语言。

OGNL表达式本身支持方法的调用,例如: 

<h2 th:object="${user}">

<p>姓: <span th:text="*{name.split(' ')[0]}">张</span></p>

<p>名: <span th:text="*{name.split(' ')[1]}">三</span></p>

</h2>


这里调用了name(字符串)的split方法。

Thymeleaf提供了一些内置对象,并且在这些对象中提供了一些方法,以方便调用。在获取这些对象后,需要使用“#对象名”来引用。

(1) 一些环境相关对象。

#ctx: 获取Thymeleaf自己的Context对象。

#request: 如果是Web程序,可以获取HttpServletRequest对象。

#response: 如果是Web程序,可以获取HttpServletResponse对象。

#session: 如果是Web程序,可以获取HttpSession对象。

#servletContext: 如果是Web程序,可以获取HttpServletContext对象。

(2) Thymeleaf提供的全局对象。

#dates: 处理java.util.date的工具对象。

#calendars: 处理java.util.calendar的工具对象。

#numbers: 用来对数字格式化的方法。

#strings: 用来处理字符串的方法。

#bools: 用来判断布尔值的方法。

#arrays: 用来处理数组的方法。

#lists: 用来处理List集合的方法。

#sets: 用来处理Set集合的方法。

#maps: 用来处理Map集合的方法。

例如,在环境变量中添加日期类型对象: 

@GetMapping("show2")

public String show2(Model model){

model.addAttribute("today", new Date());

return "index";

}



图35#dates全局对象

在页面中处理如下: 

<p>

今天是: <span th:text="${#dates.format(today,'yyyy-MM-dd')}">2022-01-14</span>

</p>


添加日期对象的运行效果如图35所示。

3.3.4字面值


有时需要在指令中填写基本类型,如字符串、数值、布尔等,但并不希望被Thymeleaf解析为变量,这时将其称为字面值。

(1) 字符串字面值。

使用一对单引号引用的内容就是字符串字面值。

<p>你正在观看 <span th:text="'Thymeleaf'">template</span> 的字符串常量值.</p>


th:text指令中的Thymeleaf并不会被认为是变量,而会被认为是一个字符串。字符串字面值的程序运行效果如图36所示。




图36字符串字面值的程序运行效果图


(2) 数字字面值。

数字字面值不需要任何特殊语法,直接写入内容即可,而且可以进行算术运算。

<p>今年是<span th:text="2022"></span>.</p>

<p>两年后将会是 <span th:text="2022 + 2"></span>.</p>


数字字面值的程序运行效果如图37所示。




图37数字字面值的程序运行效果图


(3) 布尔字面值。

布尔类型的字面值是true或false。

<div th:if="true">

你填的是true

</div>


这里引用了一个th:if指令。

3.3.5拼接

在指令中经常会遇到普通字符串与表达式拼接的情况: 

<span th:text="'欢迎您:' + ${user.name} "></span>


字符串字面值需要用单引号,拼接起来非常麻烦,Thymeleaf对此进行了简化,使用一对|即可。

<span th:text="|欢迎您:${user.name}|"></span>


这与上面的语法是完全等效的,同时省去了字符串字面值的书写。

3.3.6运算

需要注意的是,${}内部通过OGNL表达式引擎进行解析,而外部通过Thymeleaf的引擎进行解析,因此运算符尽量放在${}外。

(1) 算术运算。

支持的算术运算符: +、-、*、/、%。

<span th:text="${user.age}"></span><span th:text="${user.age}%2 == 0"></span>


算术运算符的程序运行效果如图38所示。




图38算术运行符的程序运行效果图


(2) 比较运算。

支持的比较运算: 、、=、=、==、!=。需要注意的是,==和!=不仅可以比较数值,而且可以比较对象,与equals的功能类似。

除了使用常规的比较运算符外,还可以使用别名: gt()、lt()、ge(=)、le(=)、not(!)、eq(==)、ne(!=)。

<span th:text="'你的年龄是否小于30:'+ (${user.age} < 30)"></span>

<span th:text="'你的年龄是否超过30:'+ (${user.age} gt 30)"></span>


比较运算符的程序运行效果如图39所示。




图39比较运算符的程序运行效果图


(3) 三元运算。

三元运算符的表达式为condition ? then :else。

condition: 条件。

then: 条件成立的结果。

else: 不成立的结果。

其中的每个部分都可以是Thymeleaf中的任意表达式。

<span th:text="'你的性别: '+(${user.sex} ? '男':'女')"></span>


三元运算符的程序运行效果如图310所示。




图310三元运算符的程序运行效果图


(4) 默认值。

指令中的取值有时可能为空,这时需要进行非空判断,可以使用表达式“?:默认值”简写。

<span th:text="'你的年龄是'+(${user.age} ?: 20)"></span>


当前面的表达式值为null时,就会使用后面的默认值。

注意: “?:”之间没有空格。

默认值的程序运行效果如图311所示。



图311默认值的程序运行效果图


3.3.7循环

当信息页面的数据格式相同时,页面通常对它们进行循环迭代。JSTL有一个c:foreach,同理Thymeleaf也有一个th:each,其作用都是一样的,用于遍历数组、List、Set、Map 等数据。

假如有用户的java.util.List集合的users在Context中。

<tr th:each="user : ${users}">

<td th:text="${user.name}"></td>

<td th:text="${user.age}"></td>

<td th:text="${user.sex}"></td>

</tr>


java.util.List类型不是可以在 Thymeleaf 中使用迭代的唯一值类型,下面这些类型的对象都可以通过 th:each 进行迭代。

(1) 任何实现 java.util.Iterable接口的对象,其值将被迭代器返回,而不需要在内存中缓存所有值。

(2) 任何实现java.util.Enumeration接口的对象。

(3) 任何实现java.util.Map接口的对象,在迭代映射时,iter变量将是 java.util.Map.Entry 类。

(4) 任何数组。

(5) 任何将被视为包含对象本身的单值列表。

循环语句的程序运行效果如图312所示。




图312循环语句的程序运行效果图


在迭代过程中,经常会使用到它的一些迭代状态,如当前迭代的索引、迭代变量中的元素的总数、当前迭代的是奇数还是偶数、当前是否为第一个元素、当前是否为最后一个元素等。在JSP的JSTL中,c:foreach items="${list}" var ="li" varStatus="status"包含status属性。在使用th:each时,Thymeleaf提供了一种用于跟踪迭代状态的机制: 状态变量。状态变量在每个th:each属性中定义,并包含以下数据。

(1) index 属性: 当前迭代索引,从0开始。

(2) count 属性: 当前迭代计数,从1开始。

(3) size 属性: 迭代变量中元素的总量。

(4) current 属性: 每次迭代的 iter 变量,即当前遍历的元素。

(5) even/odd 布尔属性: 当前迭代是偶数还是奇数。

(6) first 布尔属性: 当前迭代是否为第一个迭代。

(7) last 布尔属性: 当前迭代是否为最后一个迭代。

<tr th:each="user ,loopStatus: ${users}" th:class="${loopStatus.odd}?odd" >

<td th:text="${loopStatus.count}"></td>

<td th:text="${user.name}"></td>

<td th:text="${user.age}"></td>

 <td th:text="${user.sex}"></td>

</tr>


状态变量的程序运行结果如图313所示。




图313状态变量的程序运行结果图


迭代状态变量(本例中的loopStatus)在th:each属性中,通过在变量user后直接写其名称来定义,用逗号分隔。与iter变量一样,状态变量的作用范围也是th:each属性的标签定义的代码片段中。

如果没有显式地设置状态变量,则Thymeleaf将始终创建一个默认的迭代变量,该状态迭代变量名称为: 迭代变量+“Stat”。

<tr th:each="user : ${users}" th:class="${userStat.odd}?odd" >

<td th:text="${userStat.count}"></td>

<td th:text="${user.name}"></td>

<td th:text="${user.age}"></td>

<td th:text="${user.sex}"></td>

</tr>


3.3.8逻辑判断

很多时候只有在满足某个条件时,才将一个模板片段显示在结果中,否则不进行显示。例如,只有当用户年龄小于18岁时,才显示为未成年人,否则不显示。th:if属性用于满足逻辑判断的需求。

<tr th:each="user ,loopStatus: ${users}" th:class="${loopStatus.odd}?odd"  >

<td th:text="${loopStatus.count}"></td>

<td th:text="${user.name}"></td>

<td th:text="${user.age}"></td>

<td th:text="${user.sex}"></td>

<td><span th:if="${user.age}<18">未成年</span></td>

</tr>


逻辑判断的程序运行结果如图314所示。




图314逻辑判断的程序运行效果图



th:if属性不仅只以布尔值作为判断条件,它还将按照如下规则判定指定的表达式结果。

(1) 如果表达式结果为布尔值,则判定为true或者false。

(2) 如果表达式的值为null,th:if将判定此表达式为false。

(3) 如果值是数字且为0时,判定为false; 不为零时则判定为true。

(4) 如果值是字符串且为“false”“off”“no”时,判定为false; 否则判定为true。当字符串为空时,也判定为true。

(5) 如果值不是布尔值、数字、字符或字符串的其他对象,只要不为null,则判定为true。

th:unless是th:if的反向属性,它们判断的规则一致,只是if当结果为true时进行显示,unless当结果为false进行显示。

3.3.9分支控制switch

th:switch/th:case与Java中的switch语句等效,有条件地显示匹配的内容。只要其中一个th:case的值为true,则同一个switch语句中的其他th:case属性都将被视为false。当有多个case的值为true时,则只取第一个。

switch语句的default选项指定为th:case =“*”,即当没有case的值为true时,将显示default的内容,如果有多个default,则只取第一个。

<tr th:each="user ,loopStatus: ${users}" th:class="${loopStatus.odd}?odd"  >

<td th:text="${loopStatus.count}"></td>

<td th:text="${user.name}"></td>

<td th:text="${user.age}"></td>

<td th:text="${user.sex}"></td>

<td th:switch="${user.age}<18">

<span th:case="true">未成年</span>

<span th:case="*">成年人</span>

</td>

</tr>


分支语句程序的运行效果如图315所示。




图315分支语句程序的运行效果图


3.3.10Thymeleaf模板片段

系统中的很多页面都有公共内容,如菜单、页脚等,这些公共内容可以提取并放在一个称为“模板片段”的公共页面中,其他页面可以引用该公共页面。

模板片段可以是HTML标签,也可以使用th:fragment属性定义片段。

首先定义一个页脚片段footer.html,代码如下所示。

<span th:fragment="frag1">frag1</span>

<span th:fragment="frag2">frag2</span> 

<div id="footer1">footer1</div>

<div>

<div id="footer2">footer2</div>

</div>

<div>

<span class="content">footer3</span>

<span class="content">footer4</span>

</div>

<div th:fragment="welcome(userName)">

<span th:text="|hello,| + ${userName}"></span>

</div>


接下来就可以在Web页面中使用th:insert、th:replace、th:include属性来包含页脚片段,这3个属性的区别如下。

 th:insert: 在当前标签里面插入模板中的标签。

 th:replace: 替换当前标签为模板中的标签。

 th:include: 在标签里面插入模板的标签内容。

这里以使用th:insert属性插入片段为例,介绍其语法规则,其他两个属性类似。

(1) th:insert="~{模板名称}"。

插入模板的整个内容。

(2)  th:insert="~{模板名称::选择器}"。

插入模板的指定内容,选择器可以对应th:fragment定义的名称,也可以用类似JQuery选择器的语法选择部分片段。

片段选择器语法: 

① /name: 选择子节点中节点名称为name的节点。

② //name: 选择全部子节点中节点名称为name的节点。

③ name[@attr='value'] : 选择名称为name且属性值为value的节点,如有多个属性则用and连接。

④ //name[@attr='value'][index]: 选择名称为name且属性值为value的节点,并指定节点索引。

片段选择器的简化语法: 

① 可以省略@符号。

② 使用#符号代替id选择,如div#id等价于div[id='id']。

③ 使用.符号代替class选择,如div.class等价于div[class='class']。

④ 使用%代替片段引用,如片段节点使用了th:ref或th:fragment,则可使用div%ref来选取节点。

(3)  th:insert="~{::选择器}"。

不指定模板名称,则选择器作用于当前页面。

(4)  th:insert="~{this::选择器}"。

与"~{::选择器}"类似,不同之处是在本页面找不到片段时,会到模板引擎的process方法处理的模板中寻找片段。

(5) 模板片段也支持传入变量,其引用语法为~{footer.html::名称(参数)}。

Thymeleaf引用模板片段的代码如下所示。

<h4>th:insert引用片段</h4>

引用指定模板的整个内容

<div th:insert="~{footer.html}"></div>

引用指定模板的片段

<div th:insert="~{footer.html::frag1}"></div>

引用本页面的片段

<div th:insert="~{::frag3}"></div>

<div th:insert="~{this::frag3}"></div>

<div th:fragment="frag3">frag3</div>



<h4>th:replace、th:include与th:insert的区别</h4>

<div th:replace="~{footer.html::frag1}"></div>

<div th:include="~{footer.html::frag1}"></div>



<h4>片段选择器的部分用法</h4>

<div th:insert="~{footer.html::/div[@id='footer1']}"></div>

<div th:insert="~{footer.html:://div#footer2}"></div>

<!--<div th:insert="~{footer.html::#footer2}"></div>-->

<div th:insert="~{footer.html::span[class='content']}"></div>

<div th:insert="~{footer.html:://span[class='content'][0]}"></div>

<div th:insert="~{footer.html:://span.content}"></div>

<!--<div th:insert="~{footer.html::.content}"></div>-->

<!--<div th:insert="~{footer.html::.content[0]}"></div>-->

<div th:insert="~{footer.html::span%frag1}"></div>

<!--<div th:insert="~{footer.html::%frag1}"></div>-->

<h4>含有变量的片段引用</h4>

<div th:insert="~{footer.html::welcome('小明')}"></div>


IDEA运行后,查看网页源代码,代码如下: 

<h4>th:insert引用片段</h4>

引用指定模板的整个内容

<div>

<span>frag1</span>

<span>frag2</span>

<div id="footer1">footer1</div> 

<div>

<div id="footer2">footer2</div>

</div>

<div>

<span class="content">footer3</span>

<span class="content">footer4</span>

</div>

<div>

<span>hello,null</span>

</div>

</div>

引用指定模板的片段

<div><span>frag1</span></div>

引用本页面的片段

<div><div>frag3</div></div>

<div><div>frag3</div></div>

<div>frag3</div>



<h4>th:replace、th:include与th:insert的区别</h4>

<span>frag1</span>

<div>frag1</div>



<h4>片段选择器的部分用法</h4>

<div><div id="footer1">footer1</div></div>

<div><div id="footer2">footer2</div></div>

<div><span class="content">footer3</span><span class="content">footer4</span></div>

<div><span class="content">footer3</span></div>

<div><span class="content">footer3</span><span class="content">footer4</span></div>

<div><span>frag1</span></div>



<h4>含有变量的片段引用</h4>

<div>

<div>

<span>hello,小明</span>

</div>

</div>



视频讲解

3.4实现基于Thymeleaf的Web应用 

3.3节已经对Thymeleaf的基本语法及使用进行了介绍,接下来通过一个项目重点讲解Spring Boot与Thymeleaf模板引擎的整合使用。

(1) 创建Spring Boot Web应用springboot0304。

(2) 选择相应依赖。

使用Spring Initializr方式创建项目,并在Dependencies依赖选择中选择Web模块下的Web场景依赖和Template Engines模块下的Thymeleaf场景依赖,然后根据提示完成项目的创建。引入的场景依赖效果如图316所示。




图316引入的场景依赖效果图


项目创建成功后,在pom.xml文件中就添加了Thymeleaf依赖,代码如下。

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>


(3) 添加Thymeleaf配置。

在application.properties文件中,添加相关的配置项,代码如下。

#开发配置为false,避免修改模板后重启服务器

spring.thymeleaf.cache=false

#模板的模式,支持 HTML、XML、TEXT、JAVASCRIPT

spring.thymeleaf.mode=HTML5

#编码,可不用配置

spring.thymeleaf.encoding=utf-8

#内容类别,可不用配置

spring.thymeleaf.content-type=text/html


(4) 整理脚本样式静态文件。

为了保证页面美观效果,需要在项目的静态资源文件夹static下创建相应的目录和文件,具体代码见源文档。

(5) 创建Book实体类、BookController控制器及对应的BookDAO数据层等,具体代码见源文档。

(6) 在resources/templates目录下创建模板片段header.html和footer.html,在此展示header.html代码。footer.html代码比较简单,具体代码见源文档。

<div align="center" th:fragment="head">

<table width="700" border="1" cellpadding="0" cellspacing="0" bordercolor="#CCCCCC">

<tr>

<td width="111" height="87" align="center"><img src="images/logo.gif" width="91" height="72"><br>      </td>

<td width="509" align="center"><img src="images/banner.bmp" width="510" height="80"></td>

<td align="center" width="70">      <a href="">网站管理</a></td>

</tr>

</table>

<table width="700" border="1" cellpadding="0" cellspacing="0" bordercolor="#CCCCCC">

<tr>

<td width="164"><div align="center"><a href="">首页</a></div></td>

<td width="224"><div align="center"><a href="">在线购书</a></div></td>

<td width="304" align="right"><div align="center"><a href="">我的购物车</a>      </div>        <div align="center"></div></td>

<td width="304" align="right"><div align="center"><a href="">我的订单</a></div></td>

<td width="304" align="right"><div align="center"><span th:text="|当前日期:${dateString}|"/></div></td>

</tr>

</table>

</div>


(7) 创建模板页面booklist.html文件,并引入head和foot代码片段,主要展示所有图书的信息,具体代码如下。

<div th:insert="~{header.html::head}"></div>

<div align="center"><a class="title">在线购书</a><br>

<table width="700" border="1" cellpadding="0" cellspacing="0">

<tr>

<td width="43%" height="26"><div align="center">书名</div></td>

<td width="17%"><div align="center">作者</div></td>

<td width="17%"><div align="center">出版社</div></td>

<td width="5%"><div align="center">价格</div></td>

<td width="9%"><div align="center">操作</div></td>

</tr>

<div th:each="book:${bookList}">

<tr>

<td height="32"><div align="center"><span th:text="${book.getTitle()}"></span>   </div></td>

<td><div align="center"><span th:text="${book.getAuthor()}"></span></div></td>

<td><div align="center"><span th:text="${book.getPublisher()}"></span> </div></td>

<td><div align="center"><span th:text="${book.getPrice()}"></span> </div></td>

<td><div align="center"><a href=""><img src="images/buy.gif" width="45" height="16" border="0"></a></div></td>

</tr>

</div>

</table>

</div>

<div th:replace="~{footer.html::foot}"></div>


(8) 启动测试。

在浏览器地址栏中输入http://localhost:8080/login后,就可以进入网上书店的首页,如图317所示。




图317运行代码片段效果图




视频讲解

3.5Spring Boot中的页面国际化实现

什么是国际化?例如,dubbo.apache.org是一个默认英文的网站,单击右上角的中文就会切换成中文网站,这就是国际化。

在Spring Boot的Web应用中实现页面信息国际化非常简单,下面通过示例讲解国际化的实现过程。

(1) 创建Spring Boot Web应用springboot0305。

(2) 选择Web场景依赖和Thymeleaf场景依赖。

(3) 编写国际化配置文件。

① 在resources资源文件下新建一个i18n目录,用于存放国际化配置文件。选中此文件夹并右击,在弹出的命令选择器中选择New→Resource Bundle命令,如图318所示。




图318选择Resource Bundle命令


Resource Bundle是一堆前缀名称相同但后缀名称不同的属性文件的集合,且至少包含两个有着相似前缀名称的属性文件,如book_en_US.properties和book_zh_CN.properties。Resource Bundle从字面上理解其实就是资源包,为了方便统一管理繁多的国际化文件,只是在IntelliJ IDEA内显示上多了一层名为Resources的Resource Bundle目录,但在实际物理目录下的book_*.properties等文件仍在i18n目录下。



② 选择Resource Bundle后会弹出如图319所示的窗口,在窗口中填写Resource Bundle的基础名称book,勾选User XMLbased properties files则会创建XML格式的属性文件。Project locale表示项目里已经存在的区域,Locales to add表示添加相应的区域,单击右边的+号即可添加,多个区域用英文的逗号隔开,如图320所示。




图319新建Resource Bundle




图320添加Locales




③ 区域添加完成后,可以在Locales to add看到已经添加的区域,如图321所示。单击OK按钮,生成Resource Bundle,如图322所示。




图321添加完成区域效果




图322生成Resource Bundle




book.properties为自定义默认语言配置文件,book_zh_CN.properties为自定义中文国际化文件,book_en_US.properties为自定义英文国际化文件。

需要说明的是,Spring Boot默认识别的语言配置文件为类路径resources下的messages.properties,其他语言国际化文件的名称必须严格按照“文件前缀名_语言代码_国家代码.properties”的形式命名。

④ 点进3个文件中的任意一个,单击左下角的可视化工具,同时对3个文件进行编写,如图323所示。先单击Resource Bundle再单击+号进行属性的添加,只需要添加一个即可,另外两个会自动加上。




图323利用可视化工具添加属性



⑤ 查看配置文件。

book.properties和book_zh_CN.properties配置文件: 


book.author=作者

book.bookname=书名

book.buyBook=在线购书

book.cart=购物车

book.date=当前日期

book.development=科技工作室开发

book.fristPage=首页

book.management=网站管理

book.operation=操作

book.order=订单

book.price=价格

book.publisher=出版社



book_en_US.properties配置文件: 

book.author=Author

book.bookname=BookName

book.buyBook=Purchase Books

book.cart=hopping Cart

book.date=Date

book.development=Technology Studio Development

book.fristPage=Home Page

book.management=Website Management

book.operation=Operation

book.order=Order

book.price=Price

book.publisher=Publisher


(4) 启用国际化配置。

Spring Boot对国际化的自动配置涉及MessageSourceAutoConfiguration类的方法,这里Spring Boot已经自动配置好了管理国际化资源文件的组件 ResourceBundleMessageSource。资源文件messages存放在i18n目录下,所以要配置messages的路径。在application.properties文件里配置国际化信息,代码如下。

# 关闭Thymeleaf模板缓存

spring.thymeleaf.cache=false

# 绑定国际化配置文件

spring.messages.basename=i18n.book


注意: 如果配置文件可以直接放在类路径下的messages.properties中,则不用将配置文件再在application.properties里配置。

(5) 以springboot0304为基础,创建相应的JS、CSS、图片、对应的类和页面。修改head.html页面,并获得国际化信息,代码如下。

<table width="700" border="1" cellpadding="0" cellspacing="0" bordercolor="#CCCCCC">

<tr align="center">

<td width="164"> <a href="">[[#{book.firstPage}]]</a> </td>

<td width="224"> <a href=""><span th:text="#{book.buyBook}"></span></a> </td>

<td width="304"> <a href=""><span th:text="#{book.cart}"></span></a></td>

<td width="304" > <a href=""><span th:text="#{book.order}"></span></a></td>

<td width="304" > <span th:text="#{book.date}+':'+${dateString}"/></td>

</tr>

</table>


代码中使用Thymeleaf模块的#{}消息表达式设置了国际化展示的一部分信息。当对购物车进行国际化设置时,需要在span标签中设置国际化信息。当对首页进行国际化设置时,这里使用了行内表达式[[#{book.firstPage}]]动态获取国际化文件中的信息。

此时国际化配置完成了一半,需要根据浏览器的请求头AcceptLanguage的语言进行页面的回显,但与程序无关,如图324所示。



图324AcceptLanguage的语言为中文


在浏览器设置中切换需要显示的语言为英语置顶,重新刷新页面就可以显示出英文的信息,如图325所示。




图325AcceptLanguage的语言为英文


(6) 实现国际化的方式有两种,一种是根据浏览器的语言变化; 另一种是单击链接来改变语言。接下来需要手动对页面语言进行自定义切换。修改foot.html页面,增加中英文切换选项,代码如下所示。

<div align="center" th:fragment="foot">

<table width="700" border="0" cellspacing="2" cellpadding="2">

<tr>

<td align="center"><span th:text="#{book.development}"></span><br>

CopyRight@2004<br>

Email:<a href="mailto:123@163.net">123@163.net</a></td>

</tr>

<tr>

<td align="center">

<a th:href="@{/login(l='zh_CN')}">中文</a> &nbsp;&nbsp;&nbsp;

<a th:href="@{/login(l='en_us')}">English</a>

</td>

</tr>

</table>

</div>


LocaleResolver (获取区域信息对象)解析器位于国际化的Locale(区域信息对象),
在Web MVC自动配置文件中可以看到Spring Boot的默认配置。

@Bean

@ConditionalOnMissingBean

@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")

public LocaleResolver localeResolver() {   

//容器中没有就自己配置,有的话就使用已有配置    

if (this.mvcProperties.getLocaleResolver() ==WebMvcProperties.LocaleResolver.FIXED) 

{ return new FixedLocaleResolver(this.mvcProperties.getLocale());}       

//接收头国际化分解    

AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();    

localeResolver.setDefaultLocale(this.mvcProperties.getLocale());    

return localeResolver;

}

AcceptHeaderLocaleResolver 类中获取Locale的方法如下: 

public Locale resolveLocale(HttpServletRequest request) {

//默认为根据请求头带来的区域信息获取Locale进行国际化    

Locale defaultLocale = this.getDefaultLocale();

if (defaultLocale != null && request.getHeader("Accept-Language") == null) {

return defaultLocale;

} else {

Locale requestLocale = request.getLocale();

List<Locale> supportedLocales = this.getSupportedLocales();

if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {

Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);

if (supportedLocale != null) {

return supportedLocale;

} else {

return defaultLocale != null ? defaultLocale : requestLocale;

}

} else {

return requestLocale;

}

}

}


如果想单击链接让国际化资源生效,就需要让自己的Locale生效。
此时在it.com.boot.springboot03_05.config包下创建一个用于定制国际化功能区域信息解析器的自定义配置类MyLocaleResolver,通过链接获取携带区域的信息。

public class MyLocaleResolver implements LocaleResolver {

//解析请求    

@Override

public Locale resolveLocale(HttpServletRequest request) {

String language = request.getParameter("l");   //获取header页面传过来的参数

String head = request.getHeader("Accept-Language");

//Accept-Language: zh-CN, zh;q=0.9,en-US;q=0.8,en;q=0.7

Locale locale;       

if (!StringUtils.isEmpty(language)) {  //如果请求链接不为空    

//由于格式为en_us,所以用"_"分割请求参数               

String[] split = language.split("_");    

//截取后把新的属性(国家,地区)封装给locale

locale = new Locale(split[0], split[1]); 

}

else {

String[] splits = head.split(",");

String[] split = splits[0].split("-");

locale = new Locale(split[0], split[1]);

}

return locale;

}

@Override

public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

}

}


(7) 为了让区域化信息能够生效,需要再配置一下这个组件。修改MyLocaleResolver类为配置类,并在该类下重新注册一个类型为LocaleResolver 的Bean组件,这样就可以覆盖默认的LocaleResolver组件。

@Configuration

public class MyLocaleResolver implements LocaleResolver {

//省略解析请求

@Bean

public  LocaleResolver localeResolver()

{

return new MyLocaleResolver();

}

}


(8) 重启服务器,发现单击按钮也可以实现成功切换,中英文页面分别如图326和图327所示。




图326中文页面




图327英文页面


3.6Spring Boot集成Spring MVC

如果想在Spring Boot中自己定义一些Handler、Interceptor、ViewResolver和MessageConverter等,该如何完成呢?在Spring Boot 1.5版本中,依靠重写WebMvcConfigurerAdapter的方法来添加自定义拦截器和消息转换器等。在SpringBoot 2.0版本后,该类被标记为@Deprecated,因此只能靠WebMvcConfigurer接口来实现。

WebMvcConfigurer是一个接口,提供很多自定义的拦截器,如跨域设置、类型转换器等。此接口为开发者预想了很多拦截层面的需求,方便开发者自由选择使用。

Spring Boot推荐使用WebMvcConfigurer接口的实现类来实现代码配置,具体代码如下。

@Configuration

public class MyConfigurer implements WebMvcConfigurer {}


3.6.1配置自定义拦截器Interceptor

Java里的拦截器是动态拦截Action调用的对象,它提供了一种机制,可以使开发者在一个Action执行的前后执行一段代码,也可以在一个Action执行前阻止其执行。此外,拦截器还提供了一种可以提取Action中可重用部分代码的方式。在Spring AOP中,拦截器用于在某个方法或字段被访问前进行拦截,然后在其前后加入某些操作。

下面使用WebMvcConfigurer接口中的addInterceptors()方法注册自定义拦截器。

(1) 以项目springboot0301和 springboot0305为基础,创建项目springboot0306。

(2) 在config包下创建一个自定义拦截器类MyInterceptor,并编写拦截业务代码。实现拦截器需要一个HandlerInterceptor接口的实现类,并重写其中的3个方法,具体代码如下。

@Component

public class MyInterceptor  implements HandlerInterceptor {

//在请求处理前进行调用(Controller方法调用前)

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

Object user=request.getSession().getAttribute("user");

if (user==null) {

response.sendRedirect("/index");

return false;

}

return true;

}

//请求处理后进行调用(Controller方法调用后),但需要在视图被渲染前

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

//在整个请求结束后被调用,即在DispatcherServlet 渲染了对应的视图后执行(主要是用于进行资源清理工作)

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}

}


只有拦截器的preHandle方法返回true,postHandle、afterCompletion才有可能被执行。如果preHandle方法返回false,则该拦截器的postHandle、afterCompletion必然不会被执行。

(3) 在实现拦截器后,还需要将拦截器注册到Spring容器中。在config包下创建一个实现实现WebMvcConfigurer接口的配置类MyConfigurer,覆盖其addInterceptors(InterceptorRegistry registry)方法。将Bean注册到Spring容器中,可以选择@Component或@Configuration,具体代码如下。

@Configuration

public class MyConfigurer implements WebMvcConfigurer {

@Autowired

private MyInterceptor myInterceptor;

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(myInterceptor)

.addPathPatterns("/**")         //所有路径都被拦截

.excludePathPatterns("/main")    //main请求不被拦截         

.excludePathPatterns("/index")  

.excludePathPatterns("/**/*.js")    //JS静态资源不被拦截

.excludePathPatterns("/**/*.css");  //CSS静态资源不被拦截

}

}



其中,addPathPatterns用于设置拦截器的过滤路径规则,
excludePathPatterns用于设置不需要拦截的过滤规则。



(4) 修改BookController,具体代码如下。

@Controller

public class BookController {

@RequestMapping("/login")

public String findAll(Model model)

{

Calendar calendar=Calendar.getInstance();

SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd");

BookDAO bookDao = new BookDAO();

List bookList = bookDao.queryAllBook();

model.addAttribute("dateString", dateFormat.format(calendar.getTime()));

model.addAttribute("bookList", bookList);

return "booklist";

}

@RequestMapping("/index")

public String toIndex() {

return "index";

}

@RequestMapping("/main")

public String toMain(HttpSession session) {

session.setAttribute("user", "userLoginSuccess");

return "redirect:login";

}

}


(5) 运行程序。当访问http://localhost:8080/login路径时,系统会自动跳转到用户登录页面,这就说明此处定制的自定义拦截器生效了。由于对CSS和JS进行了不拦截设置,所以在登录页面中可以进行用户名和密码的验证。只有用户信息填入完整,单击“登录”按钮,才能回到图书首页。

3.6.2跳转指定页面

用传统方式写Spring MVC时,如果需要访问一个页面,必须要写Controller类,然后再写一个方法跳转到页面,这样会很麻烦。例如,“/index”可以通过重写WebMvcConfigurer中的addViewControllers方法达到效果。

addViewControllers方法可以实现将一个无业务逻辑的请求直接映射为视图,不需要编写控制器,从而简化了页面跳转。

修改3.6.1节的MyConfigurer配置类,在其实现类中重写addViewControllers方法,具体代码如下。

@Configuration

public class MyConfigurer implements WebMvcConfigurer {

@Override

public void addViewControllers(ViewControllerRegistry registry) {

registry.addViewController("/index").setViewName("index");

registry.addViewController("/index.html").setViewName("index");

}

}


此处重写addViewControllers并不会覆盖WebMvcAutoConfiguration中的addViewControllers(在此方法中,Spring Boot将“/”映射至index.html),这意味着自己的配置和Spring Boot的自动配置同时有效,这也是推荐该配置方式的原因。



视频讲解

3.7Spring Boot处理JSON数据  

在后台的开发过程中,不可避免地会遇到一系列对JSON数据的返回,此时需要提供各种各样的数据。一般情况下数据类型最常用的是JSON和XML,这里主要讲解Spring Boot怎样进行JSON数据的返回及对一些特殊情况的处理。

JSON是目前主流的前后端数据传输方式,在Spring MVC中使用消息转换器HttpMessageConverter对JSON的转换提供了很好的支持,在Spring Boot中对相关配置做了进一步简化。

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>


上述依赖中默认加入了Jacksondatabind作为JSON处理器,此时不需要添加额外的JSON处理器就可以返回JSON。
这是Spring Boot自带的处理方式,如果采用这种方式,对于字段忽略、日期格式化等都可以使用注解实现。

public class Book { 

@JsonIgnore //过滤该属性

protected Float price;

@JsonFormat(pattern = "yyyy-MM-dd") //格式化输出该属性

private Date publicationDate;

}


Spring Boot在处理JSON数据时,需要用到两个重要的JSON格式转换注解,分别是@ResponseBody和@RequestBody。

(1) @ResponseBody注解的作用: 将controller的方法返回的对象通过适当的转换器转换为指定的格式,并将其写入response对象的body区,通常用来返回JSON数据或XML数据。

(2) @RequestBody注解的作用: 在形参列表上,将前台发送的固定格式的数据(XML数据或JSON数据等)封装为对应的JavaBean对象,封装时使用系统默认配置的 HttpMessageConverter(消息转换器)进行解析,然后封装到形参上。

常见的JSON处理器除了Jacksondatabind外,还有Gson和FastJson两种,在使用时需要添加相应的依赖。这里以FastJson为例,讲解JSON处理器的使用方法。

fastjson.jar是阿里巴巴开发的一款专门用于Java开发的包,可以方便地实现JSON对象与JavaBean对象的转换、JavaBean对象与JSON字符串的转换,以及JSON对象与JSON字符串的转换。

下面通过示例讲解JSON数据的处理过程。

(1) 以项目springboot0306为基础,创建项目springboot0307。

(2) 打开pox.xml文件,去除jacksondatabind依赖,加入FastJson依赖。

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<exclusions>

<exclusion>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>fastjson</artifactId>

<version>1.2.75</version>

</dependency>


(3) FastJson需要自己配置HttpMessageConverter,打开MyConfigurer配置类,在其实现类中,重写configureMessageConverters方法,具体代码如下。

@Override

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

//创建FastJson的消息转换器

FastJsonHttpMessageConverter convert = new FastJsonHttpMessageConverter();

//创建FastJson的配置对象

FastJsonConfig config = new FastJsonConfig();

//对JSON数据进行格式化

formatting(SerializerFeature.PrettyFormat);

convert.setFastJsonConfig(config);

converters.add(convert);

}


(4) 修改“/login”请求,使其返回JSON格式的数据。

@RequestMapping("/login")

@ResponseBody

public String findAll( )

{

Calendar calendar=Calendar.getInstance();

SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd");

BookDAO bookDao = new BookDAO();

List bookList = bookDao.queryAllBook();

JSONObject jsonObject=new JSONObject();

jsonObject.put("dateString", dateFormat.format(calendar.getTime()));

jsonObject.put("booklist",bookList );

return jsonObject.toJSONString();

}


(5) 修改booklist.html页面,使其在页面中显示AJAX请求到的数据。

<script type="application/javascript">

$(function () {

var tbody =document.getElementById("tbody-result");

var date = document.getElementById("date");

$.ajax({

url: "login",

type: "get",

//如果当前为JSON格式,需要修改此属性,定义发送请求的数据格式为JSON字符串

contentType: "application/json; charset=UTF-8",

//定义回调响应的数据格式为JSON字符串,此属性可以省略

dataType: "json",

//成功响应的结果

success: function (data) {

var inf = "";

var bookList = data.booklist

for (var i = 0; i < bookList.length; i++) {

var book = bookList[i];

inf += "<tr> <td height='32'><div align='center'>" + book.title +

"</div></td><td><div align='center'>" + book.author +

"</div></td><td><div align='center'>" + book.publisher +

"</div></td> <td><div align='center'>" + book.price +

"</div></td><td><div align='center'><a href=''><img src='images/buy.gif' width='45' height='16' border='0'></a></div></td></tr>"

}

tbody.innerHTML = inf;

date.innerText += data.dateString;

},

error: function () {

alert(("操作失败"));

}

}

);

})

</script>



<table width="700" border="1" cellpadding="0" cellspacing="0">

<tr>省略标题行</tr>

<tbody id="tbody-result"></tbody>

</table>


(6) 运行程序,效果图如图326所示。



视频讲解

3.8Spring Boot实现RESTful风格的Web应用


RESTful架构风格是目前最流行的一种架构风格,它结构清晰、符合标准、易于理解、扩展方便,所以在Web开发中经常被使用。REST的全称是Representational State Transfer,译作“表现层状态转化”。

RESTful架构是对MVC架构改进后所形成的一种架构,通过使用事先定义好的接口与不同的服务联系起来。在RESTful架构中,浏览器使用POST、DELETE、PUT和GET这4种请求方式分别对指定的URL资源进行增、删、改、查操作。因此,RESTful通过URI实现对资源的管理和访问,具有扩展性强、结构清晰的特点。

RESTful架构将服务器分成前端服务器和后端服务器两部分,前端服务器为用户提供无模型的视图,后端服务器为前端服务器提供接口。浏览器向前端服务器请求视图,通过视图中包含的AJAX函数发起接口请求并获取模型。

RESTful是一种对URL进行规范的编码风格,通常一个网址对应一个资源,访问形式类似http://xxx.com/xx/{id}/{id}。

举个例子,在某购物网站上买手机时会有很多品牌选择,而每种品牌下又有很多型号,此时https://mall.com/mobile/iPhone/6可以代表iphone6,而https://mall.com/mobile/iPhone/7和https://mall.com/mobile/iPhone/8分别代表iphone7和iphone8。

在进行Web开发的过程中,method常用的值是GET和POST。可事实上,method值还可以是PUT和DELETE等其他值。既然method值如此丰富,那么就可以考虑使用同一个URL,但是约定不同的method来实施不同的业务,这就是RESTful的基本考虑。

CRUD是最常见的操作,在使用RESTful风格前,通常的增加做法如下: 

/addCategory?name=xxx


可是在使用RESTful风格后,增加做法就变为: 

/categories


传统风格和RESTful风格的对比如表31所示,URL使用相同的"/categories"语句,区别只是在于method值不同,服务器根据 method 的不同来判断浏览器期望进行的业务行为。


表31传统风格和RESTful风格的对比



功能传 统 风 格RESTful风格
URLmethodURLmethod

增加/addCategory?name=xxxPOST/categoriesPOST
删除/deleteCategory?id=123GET/categories/123DELETE
修改/updateCategory?id=123&name=yyyPOST/categories/123PUT
获取/getCategory?id=123GET/categories/123GET
查询/listCategoryGET/categoriesGET


为了实现RESTful API接口,Spring Boot提供了以下注解,对请求参数和返回数据格式进行封装,方便用户快速开发。

(1) @RestController一般用于Controller类上,指定所有接口返回的数据都是text/json格式。

(2) @ResponseBody用于方法上,指定接口返回text/json格式,如果使用@RestController就没有必要使用@ResponseBody。

(3) @GetMapping用于方法上,是一个组合注解,与@RequestMapping(method=RequestMethod.GET)作用一致。

(4) @PostMapping用于方法上,是一个组合注解,与@RequestMapping(method=RequestMethod.POST)作用一致。

(5) @PutMapping用于方法上,是一个组合注解,与@RequestMapping(method=RequestMethod.PUT)作用一致。

(6) @DeleteMapping用于方法上,是一个组合注解,与@RequestMapping(method=RequestMethod.DELETE)作用一致。

(7) @RequestParam用于方法上,映射请求参数到Java方法的参数,当前端传递的参数与后台自定义的参数不一致时,可以使用name属性来标记。

(8) @PathVariable用于方法上,映射URL片段到Java方法的参数。

下面通过一个例题来讲解Spring Boot如何实现RESTful。

(1) 以项目springboot0307为基础,创建项目springboot0308。

(2) 创建Controller类。

@RestController

public class UserController {

@GetMapping("/user/{username}")

public String toMain(@PathVariable String username, String password,HttpSession session   ) {

JSONObject jsonObject = new JSONObject();

if (username.equals("admin")&&password.equals("123456")) {

session.setAttribute("user", "userLoginSuccess");

jsonObject.put("flag", true);

}

else

jsonObject.put("flag", false);

return jsonObject.toJSONString();

}

@DeleteMapping("/user/{username}")

public String deleteUser( @PathVariable String username  ,HttpSession session  ) {

JSONObject jsonObject = new JSONObject();

System.out.println(username);

jsonObject.put("flag", "删除成功");

return jsonObject.toJSONString();

}

@PostMapping("/user")

public String addUser( @RequestBody String json ) {

JSONObject jsonObject=JSONObject.parseObject(json);

String username=jsonObject.getString("username");

String password=jsonObject.getString("password");

System.out.println(username);

System.out.println(password);

jsonObject.put("flag", "增加成功");

return jsonObject.toJSONString();

}

@PutMapping("/user")

public String updateUser(User user) {

JSONObject jsonObject = new JSONObject();

System.out.println(user);

jsonObject.put("flag", "修改成功");

return jsonObject.toJSONString();

}

}


(3) 编写相应的AJAX提交,这里只列出查询和添加两类,修改和删除类似,具体代码如下。

function checkform() {

var username = document.getElementById("userid").value;

var password = document.getElementById("password").value;

var json = {"password": password};

$.ajax({

url: "user/" + username,

type: "get",

data: json,

dataType: "json",

//成功响应的结果

success: function (data) {

if (data.flag) {

alert("登录成功");

window.location = "tologin";

}

else

alert("登录失败")

},

error: function () {

alert(("操作失败"));

}

}

);

}

function addform() {

var username = $("#userid").val();

var password =  $("#password").val() ;

alert(username)

var json = {"username":username,"password": password};

$.ajax({

url: "user"  ,

type: "post",

data: JSON.stringify(json),

contentType: "application/json; charset=UTF-8",

dataType: "json",

//成功响应的结果

success: function (data) {

if (data.flag) {

alert("添加成功");

}

else

alert("添加失败")

},

error: function () {

alert(("操作失败"));

}

}

);

}


(4) 运行程序,效果图如图328~图331所示。




图328登录成功效果图    




图329添加成功效果图







图330修改成功效果图      




图331删除成功效果图






视频讲解

3.9Spring Boot文件上传和下载 

在Web应用中,对多媒体文件的操作非常常见,文件的上传与下载尤为如此。本节将通过项目的新增图片和附件的上传、下载模块,带领大家熟悉该类型功能的开发流程。

3.9.1文件上传

Spring Boot通常为服务提供者,但是在一些场景中还是要用到文件上传下载这种“非常规”操作。如何在Spring Boot中实现文件的上传与下载功能呢?回顾一下在Spring MVC中的操作,需要在Spring MVC的配置文件中增加文件上传的Bean的配置。

<bean id="multipartResolver" 

class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>


在Spring MVC中完成Bean配置后,在后台对应的处理方法中就可以直接获取文件的输入流。而对于Spring Boot来说,不需要配置文件上传的解析类,因为Spring Boot已经注册好了。

Java中的文件上传涉及两个组件,一个是 CommonsMultipartResolver,另一个是 StandardServletMultipartResolver。CommonsMultipartResolver使用commonsfileupload 来处理multipart请求,而StandardServletMultipartResolver则基于Servlet 3.0来处理 multipart请求。因此,若使用StandardServletMultipartResolver处理请求,则不需要添加额外的JAR包。Tomcat 7.0开始支持Servlet 3.0,而 Spring Boot 2.6.3内嵌的Tomcat为Tomcat 9.0.56,因此可以直接使用StandardServletMultipartResolver。在Spring Boot 提供的文件上传自动化配置类MultipartAutoConfiguration中,默认也是采用 StandardServletMultipartResolver。因此,在Spring Boot中上传文件甚至可以做到零配置,具体上传过程如下。

(1) 创建Spring Boot Web应用springboot0309,并添加springbootstarterweb和springbootstarterthymeleaf依赖。

(2) 打开application.properties文件,添加如下配置,完成对上传文件大小的限制。

#设置单个文件大小

spring.servlet.multipart.max-file-size=5MB

#设置单次请求文件的总大小

spring.servlet.multipart.max-request-size=500MB


(3) 创建User类。

public class User {

private String username,password; 

private String tupian;    //上传头像需要保存相对地址

//省略get和set方法

}


(4) 创建MyConfig配置类。

图片上传无法立即显示而需要重启才能访问,这是因为服务器拥有保护措施,服务器不能对外部暴露真实的资源路径。为了解决该问题,需要配置虚拟路径映射进行访问。

@Configuration

public class MyConfig implements WebMvcConfigurer {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

//获取文件的真实路径

String path = System.getProperty("user.dir") + "\\src\\main\\resources\\static\\upload\\";

///upload/**是对应resource下的工程目录

registry.addResourceHandler("/upload/**").addResourceLocations("file:" + path);

}

}


其中System.getProperty("user.dir")是当前项目路径,如图332所示。




图332当前项目路径


(5) 创建上传页面。

在resources目录下的static目录中创建一个zhuce.html文件,完成个人信息的注册,代码如下。上传接口是/zhuce,注意请求方法是POST,enctype是multipart/formdata。

<form th:action="@{/zhuce}" method="post" enctype="multipart/form-data">

<label>姓名</label><input type="text" name="username"><br>

<label>密码</label><input type="password" name="password"><br>

<label>图片</label><input type="file" name="file"><br>

<input type="submit" value="上传">

</form>


(6) 创建文件上传处理接口。

@Controller

public class UserController {

@GetMapping("/zhuce")

public String zhuce() {

return "zhuce";

}

@PostMapping("/zhuce")

public String tijiao(User user, MultipartFile file, Model model) {

if (!file.isEmpty()) {

String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();

String path = System.getProperty("user.dir");

File filePath = new File(path, "\\src\\main\\resources\\static\\upload");

if (!filePath.isDirectory()) {

filePath.mkdirs();

}

try {

File userFile = new File(filePath, fileName);

file.transferTo(userFile);

user.setTupian("/upload/" + fileName);

} catch (IOException e) {

e.printStackTrace();

}

}

model.addAttribute("user", user);

return "permanager";

}

}


规划上传文件的保存路径为项目运行目录下的 upload文件夹,如果该文件夹不存在,则新建upload文件夹。为了避免上传文件重名,这里通过随时生成uid给上传文件名重新命名。将上传文件保存到目标文件中,把头像的相对地址赋值给当初用户的图片属性上。

(7) 创建permanager.html,显示上传成功的结果,代码如下。

<div th:object="${user}">

<label>姓名</label><span th:text="*{username}"></span><br>

<label>密码</label><span th:text="*{password}"></span> <br>

<label>图片</label><img th:src="@{*{tupian}}"><br>

</div>


(8) 运行程序,在浏览器地址栏中输入http://localhost:8080/zhuce,选择要上传的头像,上传界面如图333所示。单击“上传”按钮,成功上传头像后显示注册结果,如图334所示。




图333上传界面






图334显示上传成功界面


3.9.2文件下载

在Spring Boot项目中,可能会存在让用户下载文档的需求,比如让用户下载 readme文档来更好地了解该项目的概况或使用方法。因此,需要为用户提供可以下载文件的 API,将用户希望获取的文件作为下载资源返回给前端。

下面以项目springboot0309为基础,继续分析下载功能。

(1) 配置application.xml。

file.docDir: /src/main/resources/static/upload


该路径就是待下载文件存放在服务器上的目录,即相对路径。

(2) 将属性与实体类自动绑定。

Spring Boot中的注解@ConfigurationProperties可以将application中定义的属性与POJO类自动绑定,为此需要定义一个POJO类来进行application中file.docDir的配置绑定。

@ConfigurationProperties(prefix = "file")

@Component

public class FileProperties {

private String docDir;

public String getDocDir() {

return docDir;

}

public void setDocDir(String docDir) {

this.docDir = docDir;

}

}


注解@ConfigurationProperties(prefix = "file")在Spring Boot应用启动时将以file为前缀的属性与POJO类绑定,也就是将application.xml中的file.docDir与FileProperties中的字段docDir进行绑定。

(3) 编写自定义配置类。

打开MyConfig配置类,生成下载文件所在目录的配置类,其返回值对象返回值会作为组件添加到Spring容器中。

@Autowired

private FileProperties fileProperties;

@Bean

public File getFile() {

String path = System.getProperty("user.dir");

File fileDir = new File(path, fileProperties.getDocDir());

return fileDir;

}


(4) 编写控制器。

@Controller

public class UserController {

@Autowired

private File fileDir;

@GetMapping("/showdownload")

public String showDownLoad(Model model) {

File[] fileList = fileDir.listFiles();

model.addAttribute("fileList", fileList);

return "download";

}

@GetMapping("/download")

public void toDownLoad(String filename, HttpServletResponse response) {

try {

//通过流读取文件

FileInputStream is = new FileInputStream(new File(fileDir, filename));

//获得响应流

ServletOutputStream os = response.getOutputStream();

//设置响应头信息

response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(filename, "UTF-8"));

//通过响应流将文件输入流读取的文件写出

IOUtils.copy(is, os);

//关闭流

IOUtils.closeQuietly(is);

IOUtils.closeQuietly(os);

} catch (Exception ex) {

}

}

}




(5) 编写文件下载程序。

在前端download.html页面中,可以使用a标签来下载文件,但要注意在a标签中定义filename参数来规定这是下载文件。

<table>

<tr>

<th>序号</th>

<th>文件</th>

</tr>

<tr th:each="file,fileState:${fileList}">

<td><span th:text="${fileState.count}"></span></td>

<td >

<a th:href="@{/download(filename=${file.name})}">

<span th:with="tempFileName=${file.name.substring(file.name.lastIndexOf('_')+1)}">

<span th:text="${tempFileName}"></span>

</span> 

</a>

</td>

</tr>

</table>


(6) 运行程序,在浏览器地址栏中输入http://localhost:8080/showdownload,进入下载页面,选择所需要下载的文件进行下载,如图335所示。




图335文件下载页面效果


3.10Spring Boot的异常统一处理 

在程序中出现错误是难以避免的,经验再丰富的程序员编写的程序也会出现错误。在 Java 中,程序出现错误会抛出“不正常信息”(Throwable)。Throwable 又被分为“错误”(Error)和“异常”(Exception)。有别于人为失误造成的“故障”(Bug),异常在程序中代表的是出现了当前代码无法处理的状况。例如,在一个对象不存在(值为 Null)的情况下,调用该对象的某个方法引发了空指针; 用户输入了一段 URL,但并没有找到对应的资源; 在一段计算过程中,0 被当作除数,等等。完善的错误处理,使程序不会意外崩溃甚至能友好地提示用户进行正确操作,这是让程序变得越发健壮的重要处理步骤。

在 Java 开发中,异常特别是检查型异常(Checked Exception),通常需要进行 try/catch 处理。而在基于 Spring Boot 的开发过程中,异常处理有了更多的处理方式。

3.10.1自定义error页面 

使用Web应用时,在请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射/error,在抛出异常后,会转到该请求中处理,并且该请求有一个全局的错误页面来展示异常内容。例如,启动某个项目,在浏览器中随便输入一个访问地址,由于地址不存在,Spring Boot会跳转到错误页面,如图336所示。




图336错误页面


虽然Spring Boot提供了默认的错误页面映射,但是在实际应用中,图336所示的错误页面对用户来说并不友好,需要自己实现异常提示。接下来将演示自己如何实现错误提示页面。

(1) 创建Spring Boot项目springboot03_10,并添加相应的依赖。

(2) 创建控制器类,该类即为了演示错误而定制。

@Controller

public class TestController {

@GetMapping("/index")

public String toIndex()

{

int result=1/0;

return "index";

}

}


(3) 定制错误页面。

在Spring Boot中定制错误页面有3种方法,分别如下。

① 使用精确匹配的方式,将错误页面命名为“错误状态码.html”,并放在模板引擎templates/error文件夹下。
当发生访问错误时,会跳转到对应状态码的页面,如404.html和500.html。也可以使用模糊匹配的方式来定义错误页面的名称,将错误页面命名为4xx.html和5xx.html,以此匹配对应类型的所有错误。精确匹配的查找方式要优先于模糊匹配的方式。

② 如果templates/error目录下没有自定义的错误页面,那么需要在static/error目录下定义4xx.html或5xx.html页面。

③ 直接在templates目录下创建error.html页面,这样当访问错误或异常时,可以自动将该页面作为错误页面。

Spring Boot为错误页面提供了以下属性。

  timestamp: 时间戳。

  status: 状态码。

  error: 错误提示。

  exception: 异常对象。

  message: 异常消息。  

  errors: JSR303数据校验的错误。

在5xx.html页面中编写如下代码。

<body background="500.jpeg">

<div>

<p><b>错误发生时间: </b>

<span th:text="${#dates.format(timestamp,'yyyy-MM-dd')}"></span></p>

<p><b>错误状态码: </b><span th:text="${status}"></span></p>

<p><b>异常消息: </b>[[${message}]]</p>

<p><b>错误提示: </b>[[${error}]]</p>

</div>

</body>


(4) 运行程序。

在成功运行项目后,访问http://localhost:8080/hello。由于服务器找不到请求的网页,Spring Boot便会找到src/main/resources/templates/error目录中的404.html页面,运行效果如图337所示。




图337404错误页面


继续访问http://localhost:8080/index。该请求中计算除法发生了异常,而该方法仅拋出了exception异常,并没有处理异常。当Spring Boot发现有异常拋出且没有处理时,将在src/main/resources/templates/error目录下找到500.html页面并显示异常信息,运行效果如图338所示。




图338500错误页面


从上述运行结果可以看出,使用自定义错误页面并没有真正处理异常,只是将异常或错误信息显示给客户端,因为在服务器控制台上同样抛出了异常,如图339所示。




图339异常信息


3.10.2@ExceptionHandler注解 

不难发现,使用自定义error页面并没有真正处理异常,在本节将@ExceptionHandler注解处理异常。该注解主要用于在Controller层面进行相同类型的异常处理,在对应Controller类中定义异常处理方法,并为其使用@ExceptionHandler注解。Spring会检测到该注解,并将该方法注册为对应异常类及其子类的异常处理程序。异常处理的示例代码如下。

@ExceptionHandler()

public String handleException2(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: 默认";

return resultStr;

}


使用该注解的方法可以拥有非常灵活的签名,包括以下类型: 

 异常类型(Throwable): 可以选择一个大概的异常类型。例如,示例里的签名可
以改为“Throwable e”或“Exception e”,也可以改为一个具体的异常类型。

 请求与响应对象(Request/Response): 比如 ServletRequest和HttpServletRequest。

 InputStream/Reader: 用于访问请求的内容。

 OutputStream/Writer: 用户访问响应的内容。

 Model: 作为从该方法返回 Model 的替代方案。

在@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常,示例代码如下: 
@ExceptionHandler(NumberFormatException.class)

public String handleException(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: NumberFormatException";

return resultStr;

}


此时注解的参数是NumberFormatException.class,表示只有方法抛出NumberFormatException时,才会调用该方法。

当异常发生时,Spring会选择最接近抛出异常的处理方法。

例如,NumberFormatException异常,该异常有父类RuntimeException和Exception,如果分别定义异常处理方法,@ExceptionHandler将分别使用这3个异常作为参数,示例代码如下: 

@ExceptionHandler(NumberFormatException.class)

public String handleException(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: NumberFormatException";

return resultStr;

}



@ExceptionHandler()

public String handleException2(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: 默认";

return resultStr;

}



@ExceptionHandler(RuntimeException.class)

public String handleException3(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: RuntimeException";

return resultStr;

}


当代码抛出NumberFormatException时,调用的方法将是注解参数NumberFormatException.class的方法,即handleException(); 而当代码抛出IndexOutOfBoundsException时,调用的方法将是注解参数RuntimeException的方法,即handleException3()。

标识了@ExceptionHandler注解的方法,其返回值类型与标识了@RequestMapping注解的方法是相同的,可参见@RequestMapping的说明。例如,默认返回Spring的ModelAndView对象,也可以返回String,这时的String是ModelAndView的路径,而不是字符串本身。

有些情况下会给标识了@RequestMapping的方法添加@ResponseBody,比如使用AJAX的场景会直接返回字符串。异常处理类也可以如此操作,在添加@ResponseBody注解后,可以直接返回字符串,示例代码如下: 

@ExceptionHandler(NumberFormatException.class)

@ResponseBody

public String handleException(Exception ex) {

System.out.println("抛出异常:" + ex);

ex.printStackTrace();

String resultStr = "异常: NumberFormatException";

return resultStr;

}



图340控制器父类和控制器

子类的关系

一个Spring Boot应用中往往存在多个控制器,不适合在每个控制器中添加使用@ExceptionHandler注解修饰的方法进行异常处理。传统的做法是定义一个控制器父类(如BaseController),它包含了执行共同操作的方法,其他的控制器类(如ControllerA和ControllerB)继承这个控制器父类。图340显示了控制器父类和控制器子类的关系。

3.10.3@ControllerAdvice注解


继承是提高控制器类的代码可重用性的有效手段,但是它有一个缺陷,那就是由于Java语言不支持多继承,当控制器类继承了一个控制器父类后,就不能再继承其他的类。

Spring MVC框架提供了另一种方式来为多个控制器类提供共同的方法: 利用@ControllerAdvice注解定义一个控制器增强类。

控制器增强类并不是控制器类的父类。在程序运行时,Spring MVC框架会把控制器增强类的方法代码块动态注入其他控制器类中,通过这种方式增强控制器类的功能。图341显示了控制器增强类(如MyControllerAdvice)和控制器类的关系。




图341控制器增强类和
控制器类的关系

@ControllerAdvice注解是Spring 3.2中新增的注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理。

对于@ControllerAdvice,比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不止于此。ControllerAdvice拆开来就是Controller Advice,Advice在Spring的AOP中是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行切面环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有3点,灵活使用这3个功能可以简化很多工作。需要注意的是,这是Spring MVC提供的功能,在Spring Boot中可以直接使用。下面介绍@ControllerAdvice的具体用法。

1. 全局异常处理

使用@ControllerAdvice实现全局异常处理,只需要定义类并添加该注解即可。定义类的方式如下: 

@ControllerAdvice

public class MyGlobalExceptionHandler {

@ExceptionHandler(Exception.class)

public ModelAndView customException(Exception e) {

ModelAndView mv = new ModelAndView();

mv.addObject("message", e.getMessage());

mv.setViewName("myerror");

return mv;

}

}


在该类中,可以定义多个方法,不同的方法处理不同的异常,如专门处理空指针的方法、专门处理数组越界的方法等,也可以直接像上面代码一样,在一个方法中处理所有的异常信息。

@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进入该方法。

2. 全局数据绑定

全局数据绑定功能可以用来做一些初始化的数据操作,可以将一些公共的数据定义在添加了@ControllerAdvice 注解的类中。这样,在每一个Controller的接口中,就都能够访问这些数据。

定义全局数据如下: 

@ControllerAdvice

public class MyControllerAdvice {

@ModelAttribute(name = "colors")

public Map<String,String> setColors() {

HashMap<String, String> colors = new HashMap<String,String>();

colors.put("RED", "红色");

colors.put("BLUE", "蓝色");

colors.put("GREEN", "绿色");

return colors;

}}


当程序运行时,Spring MVC框架会把MyControllerAdvice类的setColors()方法动态注入其他控制器类中,因此其他控制器类自动拥有了该方法。例如,在TestAttributeController类中可以直接访问Model中的colors属性。

@RequestMapping(value="/testColor")

public String testColor(@ModelAttribute("colors") Map<String,String> colors,      @ModelAttribute("userName") String name){

System.out.println(name+"'s favourite color:"+colors.get("RED"));

return "result";

}


通过浏览器访问http://localhost:8080/helloapp/testColor?name=Tom,
testColor()方法会在服务器端打印“TOMs favourite color:红色”。

对控制器添加通知有以下几种方式: 


(1) 只对一部分控制器添加通知,如某个包下的控制器。

@ControllerAdvice(basePackages = {"com.example"})

//@ControllerAdvice(com.example)

public class MyControllerAdvice2{…}


basePackages: 指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice管理。

(2) 不固定包名,只想把包里的某个类传进去。

@ControllerAdvice(basePackageClasses =MainController.class)

public class MyControllerAdvice2{…}


basePackageClasses: basePackages的一种变形,指定一个或多个Controller 类,这些类所属的包及其子包下的所有Controller都被该 @ControllerAdvice 管理。

(3) 只对某几个控制器添加通知。

@ControllerAdvice(assignableTypes={PersonController.class,MainController.class})             

public class MyControllerAdvice2{…}


3. 全局数据预处理

考虑有两个实体类为Car和Driver,分别定义如下: 

public class Car {

private String name;

private String type;

//省略get和set方法

}

public class Driver {

private String name;

private Integer age;

//省略get和set方法

}


定义一个数据添加接口如下: 

@PostMapping("/car")

public void addCar(Car car , Driver driver ) {

System.out.println(car);

System.out.println(driver);

}


此时,添加操作会产生问题,因为两个实体类都有一个 name 属性,从前端传递时无法区分。通过@ControllerAdvice的全局数据预处理可以解决该问题,解决步骤如下: 

(1) 给接口中的变量取别名。

@RestController

public class TestController {

@GetMapping("/index")

public void index(@ModelAttribute("c") Car car, @ModelAttribute("d") Driver driver) {

System.out.println(car);

System.out.println(driver);

}

}



(2) 进行请求数据预处理。

在@ControllerAdvice 标记的类中添加如下代码: 

@ControllerAdvice(value = "com.example.controller")

public class TestControllerAdvice {

@InitBinder("c")

public void b(WebDataBinder binder) {

binder.setFieldDefaultPrefix("c.");

}

@InitBinder("d")

public void a(WebDataBinder binder) {

binder.setFieldDefaultPrefix("d.");

}

}


@InitBinder("c") 注解表示该方法用来处理与Car相关的参数,在方法中给参数添加一个c前缀,即请求参数要有c前缀。

(3) 发送请求。

请求发送时,通过给不同对象的参数添加不同的前缀,可以实现参数的区分。

请求地址 http://127.0.0.1:8080/index?c.name=保时捷&c.type=赛跑&d.name=全爷&d.age=35

输出{"Driver.age":35,"Car.type":"赛跑","Driver.name":"全爷","Car.name":"保时捷"}


本章小结 

本章首先介绍了Spring Boot的Web开发支持,然后详细讲述了Spring Boot推荐使用的Thymeleaf模板引擎,包括Thymeleaf的基础语法、常用属性和国际化。同时,本章还介绍了Spring Boot对JSON数据的处理、文件上传下载、异常统一处理和对JSP的支持等Web应用开发的常用功能。



在线测试

习题 
一、 单选题

1. 以下关于Thymeleaf模板引擎常用标准表达式的说法,错误的是()。



A. 变量表达式#{…}主要用于获取上下文中的变量值

B. 使用th:text="${#locale.country}"动态获取当前用户所在国家信息

C. 使用消息表达式#{…}进行国际化设置时,还需要提供一些国际化配置文件

D. 片段表达式~{…}用来标记一个片段模板,并根据需要移动或传递给其他模板

2. 以下关于Spring Boot整合Thymeleaf的相关配置说法,正确的是()。

A. spring.thymeleaf.cache表示是否开启Thymeleaf模板缓存,默认为false

B. spring.thymeleaf.prefix指定了Thymeleaf模板页面的存放路径,默认为resources/

C. spring.thymeleaf.suffix指定了Thymeleaf模板页面的名称后缀,默认为.html

D. spring.thymeleaf.encoding表示模板页面变化格式,默认为iso88591

3. 以下关于Thymeleaf模板引擎页面标签的说法,错误的是()。

A. th:each用于元素遍历,类似JSP中的c:forEach标签

B. th:value用于属性值修改,指定标签属性值

C. th:utext用于指定标签显示的文本内容,对特殊标签进行转义

D. th:href用于设定链接地址

4. IE不同版本UserAgent中出现的关键词不同,以下不属于IE UserAgent关键词的是()。

A. MSIE  B. Mozilla   C. Edge   D. Trident

5. Thymeleaf支持处理多种模板视图,不包括()。

A. CSS  B. XML  C. JS   D. EXE

6. 在Spring Boot中使用路径扫描的方式整合内嵌式Servlet三大组件时,不包括的注解或属性是()。

A. @WebServlet注解             B. @EnableWebMvc注解 

C. @ServletComponentScan注解    D. value属性

二、  多选题

1. 以下关于Spring Boot整合Spring MVC框架实现Web开发中文件上传功能的相关说法,错误的是()。

A. 实现文件上传功能,还需要提供文件上传相关依赖

B. 必须在配置文件中对文件上传功能进行配置

C. 多文件上传处理类中的方法中必须由MultipartFile[]类型参数进行多文件接收

D. spring.servlet.multipart.maxfilesize用来设置所有上传文件的大小限制,默认值为10MB

2. 以下关于Thymeleaf主要标准表达式语法及说明,正确的是()。

A. Thymeleaf模板页面中的th:text="${#locale.country}"动态获取当前用户所在国家信息

B. ${#object.firstName}使用Thymeleaf模板提供的内置对象object获取当前上下文对象中的firstName属性值

C. &lt; div th:insert="~{thymeleafDemo::title}"&lt; /div中的title为引入的模板名称

D. 使用th:insert或th:replace属性可以插入Thymeleaf模板片段

3. Spring Boot框架整合MVC实现Web开发支持的前端模板引擎包括()。

A. Mustache    B. FreeMarker    C. Thymeleaf    D. Groovy

三、  判断题(对的打“√”,错的打“×”)

1. 在Spring Boot项目的classpath:/static/目录下编写一个index.html页面,可以作为Spring Boot默认欢迎页。()

2. 在Spring Boot中进行中文名文件下载处理时,如果内核信息是IE,则转码为ISO88591进行处理。()

3. Thymeleaf支持处理6种模板视图,包括HTML、XML、TEXT、JAVASCRIPT、CSS和RAW。()

4. 使用datath*属性定制Thymeleaf模板页面时,不需要引入Thymeleaf标签。()

5. Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎。()

四、  填空题

1. Spring Boot为整合Spring MVC框架实现Web开发,支持静态项目首页。

2. Spring Boot整合Spring MVC实现Web开发,需要引入依赖启动器。

3. 在Spring Boot中,使用组件注册方式整合内嵌Servlet容器的Filter组件时,只需将自定义组件通过类注册到容器中即可。

4. Spring Boot为整合Spring MVC框架实现Web开发,内置了ContentNegotiatingViewResolver和两个视图解析器。

5. 消息表达式主要用于Thymeleaf模板页面国际化内容的动态替换和展示。