SpringBoot入门
1、SpringBoot简介
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
小结:Spring Boot是基于Spring框架的,也正是为了简化Spring框架中繁琐大量的配置文件而诞生。
2、SpringBoot特点
- 基于spring,使开发者快速入门,门槛低。SpringBoot可以创建独立运行的spring应用而不依赖于容器。
- 不需要打包成war包,可以放入内嵌的tomcat中直接运行。
- 提供maven极简配置,缺点是会引入很多并不需要的jar包,导致项目臃肿。因此可以根据项目来依赖,从而配置spring,需要什么配置什么。
- 提供生产就绪型功能,如指标,健康检查和外部配置等。
- spring自动配置,简化了大量配置,不用再配置过多的xml文件,对xml也没有配置要求。
- 为微服务SpringCloud铺路,SpringBoot可以整合很多各式各样的框架来构建服务,比如dubbo、thrift等。
3、SpringBoot使用场景
- 任何有Spring的地方都行。
- J2EE/web项目。
- 微服务。
4、SpringBoot的使用
4.1、下载安装STS
这里使用的ide是eclipse,需要事先下载STS插件,即Spring-Tool-Suite。STS是一个定制版的Eclipse,专为Spring开发定制的,方便创建调试运行维护Spring应用。可以在eclipse里面的market下载,也可以离线下载。
由于网速原因,这里选择离线下载好。下载完毕后,Help-Install new software-add添加,设置好name和location位置即可。
4.2、创建SpringBoot应用
推荐使用new快速生成一个springboot项目。当然你也可以在Maven项目的基础上添加一些东西,改造一下就可以了,但是不推荐。
点击Spring Starter Project:
本质上是依赖https://start.spring.io/接口创建的,因为是外网,所以有时候会获取失败,可以多试几次。填写相关信息后点击Next:
可以选择Spring Boot的版本,也可以勾选需要用到的模块,下面那些,那这里先不勾选,后面需要时再手动依赖进来,版本的话选默认的,这里默认为2.3.9。点击Finish:
可以看到,创建成功后,项目后面有一个boot,表示这是一个Spring Boot项目,同时它也是Maven项目。默认的目录结构如下:
// 项目的源目录
src/main/java
- 默认创建的源目录的包
-- 项目的启动类,必选放在包的最外层,否则扫描不到。
// 项目的资源目录,存放配置文件以及静态资源
src/main/resources
// 这是项目的全局配置文件,一般只需要这一个配置文件就足够了,可以另行添加
- application.properties或application.yml //必须放在最外层
- static // 此目录下存放静态资源文件,比如css、js、图片等
- templates // 此目录下用来存放模板文件,比如thymeleaf模板文件
// 可以在此包下进行代码和功能的测试
src/test/java
- 源目录的包 // 注意,这里的包一定要和源目录包一样,否则测试不了
-- 测试类 // 可以在测试类中进行测试
4.3、项目启动类
每个Spring Boot项目都有且只有一个启动类,这是程序的入口,没有这个启动类,程序无法启动,这个启动类必须放在包的最外层,如下:
包为com.ycz.demo,那么启动类必须放在这个包的最外层,否则扫描不到,也就启动不了。
启动类的具体内容:
@SpringBootApplication注解是必须的,表明这是一个SpringBoot应用,这是个组合注解。然后下面有一个main方法,说明整个应用是在主线程中运行的。启动类中可以添加一些额外内容,比如包的扫描,在项目启动时向Spring容器中注入一些必要的Bean组件等。
4.4、全局配置文件
SpringBoot应用有一个全局配置文件:
这个配置文件名为application.properties或application.yml,一般是用yml格式较多。该配置文件一般放在classpath的最外层,即src/main/resources目录的最外层,项目中的所有配置都可以在这个文件中进行。
默认是空的,启动一下项目测试,选中启动类,右键–Run As–Spring Boot App。
启动成功。默认情况下,如果不配置端口的话,那么SpringBoot应用默认运行在内嵌Tomcat的8080端口上。一般都会更改端口:
4.5、测试类
SpringBoot中集成了测试Test模块,可以很方便的进行代码功能测试:
需要注意的是必须在源目录下进行测试,也就是包名必须和src/main/java中源目录包一样,否则测试不了。测试类如下:
测试类上需要标注@SpringBootTest注解,测试方法上需要标注@Test注解。可以创建其他的测试类:
可以根据需要来使用@RunWith来配置相应的测试环境。测试时需要选中测试方法名,然后右键–Run As–JUnit Test:
那么这个测通了,控制台:
正确输出内容。
4.6、pom依赖
自动创建的Spring Boot项目默认的pom配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父工程,这个只用来管理Spring Boot相应依赖版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.ycz.demo</groupId>
<artifactId>springboot-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-test</name>
<description>Demo project for Spring Boot</description>
<!-- JDK版本 -->
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot的核心启动器,包含了自动配置、日志和YAML等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring boot程序测试依赖,如果是自动创建项目默认添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 排除了依赖冲突 -->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven的打包插件,能够将Spring Boot应用打包为可执行的jar或war文件,
然后以通常的方式运行Spring Boot应用。 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
依赖只有一个Spring Boot核心启动器和Spring Boot的测试依赖,插件有Maven的打包插件。
pom中手动添加web模块依赖和log4j模块依赖:
<!-- 引入web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入log4j模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
关于父工程spring-boot-starter-parent,这里控制了所有相关依赖的版本:
父工程还有一个父工程spring-boot-dependencies,实际上真正规定了依赖版本的,是在这里:
这里控制了大量的依赖版本,此举的目的非常有必要,因为我们自己引入依赖的时候,有时候会造成各个依赖之间的版本冲突,因此官方为我们提供了一个标准的依赖版本,按照官方指导,可以有效的避免依赖冲突,因为排除依赖版本冲突是很令人头痛的一个问题。我们在引入依赖的时候,如果这里面有,那么我们无需指定依赖的version属性,不用规定版本,它会自动应用父工程规定的版本,这样会尽可能避免版本之间的冲突,这里没有规定的,我们再指定依赖的版本号。
4.7、热部署devtools
SpringBoot可以使用devtools工具进行热部署。有一个单独的spring-boot-devtools的模块,来使Spring Boot应用支持热部署,提高开发者的开发效率,无需手动重启Spring Boot应用,只要保存了那么就会自动部署。
devtools使用了两个ClassLoader,一个base Classloader加载那些不会改变的类(第三方Jar包),另一个ClassLoader加载会更改的类,称为restart ClassLoader,这样在有代码更改的时候,原来的restart ClassLoader被丢弃,会重新创建一个restart ClassLoader,由于需要加载的类比较少,所以实现了较快的重启时间。
需要使用devtools工具开发时,先引入依赖:
<!-- 引入devtools热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
然后在application.yml配置文件中进行配置:
## spring配置
spring:
## 配置热部署
devtools:
restart:
enabled: true ## 热部署生效
additional-paths: ## 设置监听目录,即需要重启的目录,可以配置多个
- src/main/java
exclude: static/**,public/**,WEB-INF/** ## 排除掉不需要重启的目录,提高重启效率
4.8、资源文件属性的获取
有时候需要获取yml或者properties属性文件中的某些属性,这里分开来说。
4.8.1、获取yml属性
获取yml中某些属性的话,直接使用@Value注解就可以了。如下:
## 自定义属性
person:
ycz:
name: yanchengzhi
sex: man
age: 25
address: hubei
## 多个值的话,是以数组元素的形式配置的,可以用数组来接收这个属性值
likes: run,swim,sing,read,walk
在测试类中进行测试:
// 获取yml配置文件中的属性
// 使用@Value注解
@Value("${person.ycz.name}")
private String name;
@Value("${person.ycz.sex}")
private String sex;
@Value("${person.ycz.age}")
private Integer age;
@Value("${person.ycz.address}")
private String address;
@Value("${person.ycz.likes}")
private String []likes;
@Test
public void t1() {
System.out.println("姓名:" + name);
System.out.println("性别:" + sex);
System.out.println("年龄:" + age);
System.out.println("住址:" + address);
System.out.println("喜欢的东西:");
for(int i=0;i<likes.length;i++) {
System.out.print(likes[i] + "\t");
}
}
控制台:
获取成功。这种方式只适用于获取少量的属性,大量属性的话会很麻烦,代码也会冗余。
4.8.2、获取properties文件属性
一般如果是大量的属性配置的话,会以properties属性文件的形式来进行配置。如下:
## 自定义配置属性
## 如果是大量的自定义配置,会配置到properties属性文件中
com.ycz.demo.url=http://www.baidu.com
com.ycz.demo.language=java
com.ycz.demo.want=chongqing
com.ycz.demo.address=hubei
com.ycz.demo.work=coder
读取properties属性文件需要用到spring-boot-configuration-processor组件,先在pom中引入该依赖:
<!-- 引入processor依赖,读取properties属性文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
然后需要建立一个对应的实体类demo来进行属性的映射。先建包:
源目录下建立4个包:pojo、dao、service、controller。
然后在启动类中开启包扫描:
创建映射的实体类:
// 配置注解
@Configuration
// 要引用的资源文件路径
@PropertySource(value = "classpath:demo.properties")
// 属性的前缀
@ConfigurationProperties(prefix = "com.ycz.demo")
public class Demo {
private String url;
private String language;
private String want;
private String address;
private String work;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getWant() {
return want;
}
public void setWant(String want) {
this.want = want;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getWork() {
return work;
}
public void setWork(String work) {
this.work = work;
}
}
测试类中进行测试:
// 注入属性映射的实体类
@Autowired
private Demo demo;
@Test
public void t1() {
System.out.println("url:" + demo.getUrl());
System.out.println("语言:" + demo.getLanguage());
System.out.println("去的地方:" + demo.getWant());
System.out.println("住址::" + demo.getAddress());
System.out.println("工作:" + demo.getWork());
}
4.9、server配置
在application.yml中进行配置,如下:
## 配置server
server:
port: 9999 ## 配置服务端口号
servlet:
## 配置应用的上下文路径,即访问路径,一般来说,这个配置在正式发布的时候不会配置
context-path: /ycz
## 配置session的最大超时时间(分钟),默认为30分钟
session:
timeout: 40
error:
## 错误页,发生错误时要跳转的错误页面
path: /error
## 为服务绑定指定ip,如果不是在该ip上运行,服务会报错
## 一般只在有特殊要求时才会进行配置
address: 127.0.0.1
## 配置服务的Tomcat参数
tomcat:
## 配置Tomcat的最大线程数,默认是200
threads:
max: 300
## 指定编码
uri-encoding: UTF-8
## 指定临时文件夹,存放tomcat的日志、Dump等文件,默认为系统的tmp文件夹
basedir: E:/springboot-test/temp
## 配置Access日志
accesslog:
enabled: true ## Access日志开启
pattern: ## Access日志格式
directory: ## Access日志目录
启动项目:
查看E盘:
生成了一个临时目录,用来存放Tomcat的日志文件等内容:
4.10、整合模板引擎
SpringBoot中是不支持JSP的,也不是不支持,只是官方强烈不推荐这么做。SpringBoot支持模板引擎,在SpringBoot中使用的最多的两大模板是Freemarker和Thymeleaf。实际开发时可以选择其中一种模板进行使用。
4.10.1、整合Freemarker模板引擎
FreeMarker是一款模板引擎。即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件,此模板在页面静态化时使用的较多。
先在pom中引入依赖:
<!-- 引入Freemarker模板依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
然后在application.yml全局配置文件中进行配置:
## spring配置
spring:
## 配置热部署
devtools:
restart:
enabled: true ## 热部署生效
additional-paths: ## 设置监听目录,即需要重启的目录,可以配置多个
- src/main/java
exclude: static/**,public/**,WEB-INF/** ## 排除掉不需要重启的目录,提高重启效率
## 配置springMVC
mvc:
## 配置静态文件路径
static-path-pattern: /static/**
## 配置freemarker模板
freemarker:
## 模板文件的加载路径
template-loader-path:
- classpath:/templates
cache: false ## 关闭缓存,及时刷新页面,测试环境设为false,生产环境设为true
charset: UTF-8 ## 统一设置模板文件的编码格式,避免乱码
check-template-location: true ## 模板位置检查
content-type: text/html ## 设置模板的内容类型
## 设定所有request的属性在merge到模板的时候,是否要都添加到model中
expose-request-attributes: true
## 设定所有HttpSession的属性在merge到模板的时候,是否要都添加到model中
expose-session-attributes: true
## 指定RequestContext属性的名
request-context-attribute: request
## 指定freemarker模板的后缀名,方便视图解析
suffix: .ftl
classpath目录下创建一个templates目录:
此目录下创建一个freemarker目录:
此目录下创建一个center目录和index.ftl文件:
center目录下创建一个center.ftl文件:
index.ftl模板内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body bgcolor="orange">
<h2 align="center">欢迎来到本页面!</h2>
FreeMarker模板引擎 <br>
<h2>URL地址:${demo.url}</h2>
<h2>后端语言:${demo.language}</h2>
<h2>想去的地儿:${demo.want}</h2>
h2>住址:${demo.address}</h2>
h2>职业:${demo.work}</h2>
</body>
</html>
center.ftl模板内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body bgcolor="pink">
<h2 align="center">欢迎来到本页面!</h2>
FreeMarker模板引擎
</body>
</html>
创建一个控制器:
@Controller
@RequestMapping("/free")
public class FreemarkerController {
@Autowired
Demo demo;
@RequestMapping("/index")
public String index(ModelMap map) {
map.addAttribute("demo",demo);
return "/freemarker/index";
}
@RequestMapping("/center")
public String center() {
return "/freemarker/center/center";
}
}
启动应用,进行测试:
测试另一个路径:
OK,Freemarker视图模板被成功解析了,那么Freemarker模板引擎就整合成功了,当然实际应用中不会这么简单,掌握最基础的东西,复杂的就不会太难。
4.10.2、整合Thymeleaf模板引擎
Thymeleaf是一个Java XML / XHTML / HTML5 模板引擎 ,可以在Web(基于servlet )和非Web环境中工作。 它更适合在基于MVC的Web应用程序的视图层提供XHTML / HTML5,但它甚至可以在脱机环境中处理任何XML文件。 它提供完整的Spring Framework。
在Web应用程序中,Thymeleaf旨在成为JavaServer Pages (JSP)的完全替代品,并实现自然模板的概念:模板文件可以直接在浏览器中打开,并且仍然可以正确显示为网页。Thymeleaf是开源软件、许可下 Apache许可证2.0。
先在pom中引入依赖:
<!-- 引入thymeleaf模板依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后在application.yml中进行配置:
## spring配置
spring:
## 配置热部署
devtools:
restart:
enabled: true ## 热部署生效
additional-paths: ## 设置监听目录,即需要重启的目录,可以配置多个
- src/main/java
exclude: static/**,public/**,WEB-INF/** ## 排除掉不需要重启的目录,提高重启效率
## 配置springMVC
mvc:
## 配置静态文件路径
static-path-pattern: /static/**
## 配置thymeleaf模板
thymeleaf:
prefix: classpath:/templates/ ## 模板前缀
suffix: .html ## 模板文件后缀
mode: HTML5 ## 使用H5作为模型
encoding: UTF-8 ## 统一设置模板的编码
servlet:
## 设置内容类型
content-type: text/html
cache: false ## 关闭模板缓存,即时刷新
在templates目录下创建一个thymeleaf目录:
该目录下创建一个center目录和index.html文件:
index.html内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body bgcolor="pink">
<h2 align="center">欢迎来到本页面!</h2>
<h3>themeleaf模板引擎</h3>
<h3 th:text="${name}"></h3>
</body>
</html>
center目录下创建一个center.html文件:
内容:
创建一个控制器:
@Controller
@RequestMapping("/thyme")
public class ThymeleafController {
@RequestMapping("/center")
public String center() {
return "thymeleaf/center/center";
}
@RequestMapping("/index")
public String index(ModelMap map) {
map.addAttribute("name","yanchengzhi");
return "thymeleaf/index";
}
}
测试:
Thymeleaf模板在开发中经常使用,以下简单介绍一些thymeleaf常用的th标签。
a、基本标签
创建一个pojo:
public class User {
private String name;
private String sex;
private Integer age;
private String password;
private Date birth;
private String desc;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
创建一个test.html页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body bgcolor="pink">
<h2 align="center">欢迎来到本页面!</h2>
<div>
<label>用户姓名:</label>
<input type="text" th:id="${user.name}" th:name="${user.name}" th:value="${user.name}"/><br/>
<label>用户年龄:</label>
<input th:value="${user.age}"/><br/>
<label>简介:</label>
<input th:value="${user.desc}"/><br/>
<label>生日:</label>
<input th:value="${user.birth}" />
</div>
</body>
</html>
控制器中添加:
@RequestMapping("/test")
public String test(ModelMap map) {
User u = new User();
u.setName("ycz");
u.setSex("男");
u.setAge(25);
u.setBirth(new Date());
u.setDesc("低端码农!");
map.addAttribute("user",u);
return "thymeleaf/test";
}
测试:
查看网页元素:
可以看到,th:id替换原来的id属性值,th:name替换原来的name属性值,th:value填充文本框。但是这里的生日,文本框里面是Date类型而不是String类型的,作一些处理:
刷新页面:
现在日期是字符串格式了。
b、th:object对象引用
修改test.html:
<!-- 对象引用模式 -->
<div th:object="${user}">
<label>用户姓名:</label>
<input type="text" th:id="*{name}" th:name="*{name}" th:value="*{name}"/><br/>
<label>用户年龄:</label>
<input th:value="*{age}"/><br/>
<label>简介:</label>
<input th:value="*{desc}"/><br/>
<label>生日:</label>
<!-- 日期直接转换格式处理 -->
<input th:value="*{#dates.format(birth,'yyyy/MM/dd')}" />
</div>
测试:
内容正确显示,没问题。
c、日期格式转换
这个在上面已经演示过了:
可以记住用法:${#dates.format(日期格式,指定文本格式)}
。
d、th:text和th:utext标签
修改控制器代码:
修改test.html:
<body bgcolor="pink">
<h2 align="center">欢迎来到本页面!</h2>
测试text和 utext的区别:<br>
<h2 th:text="${user.desc}">yanchengzhi</h2>
<h2 th:utext="${user.desc}">yanchengzhi</h2>
</body>
测试:
得出结论:th:text标签不会解析htm标签,只把它当做普通的字符串,而th:utext会解析html标签,按照html标签处理,而不是普通字符串。
e、th:href标签
修改test.html:
<body bgcolor="pink">
URL的用法:<br>
普通的链接写法:<a href="http://www.baidu.com">百度首页</a><br>
使用themeleaf的写法:<a th:href="@{http://www.baidu.com}">百度首页</a><br>
<br>
</body>
测试:
这两个链接的效果是一样的。
f、th:src标签
这个标签用于引入js文件。创建一个test.js:
然后在test.html中引入:
刷新页面:
查看控制台:
这个是正确引用。改成普通引入js的方式:
控制台:
可以看到这个引用不正确,因为没有加上应用的访问路径/ycz,所以报错。而th:src会自动在路径前面加上/ycz,从而引用正确;
g、th:if标签
这个标签用来做判断,和jsp的c:if标签类似。修改test.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body bgcolor="pink">
测试th:if判断标签:<br>
<div th:if="${user.age} lt 18">未成年!</div>
<div th:if="${user.age} == 18">刚好够格!</div>
<div th:if="${user.age} gt 18">老油条!</div>
</body>
</html>
测试:
OK,age的属性值是25,满足第3个条件。
h、th:each标签
这个标签用来循环遍历集合或数组,类似jsp的c:foreach标签。修改控制器:
@RequestMapping("/test")
public String test(ModelMap map) {
List<User> users = new ArrayList<>();
User u = new User();
u.setName("ycz");
u.setSex("男");
u.setAge(25);
u.setBirth(new Date());
u.setDesc("低端码农");
User u2 = new User();
u2.setName("david");
u2.setSex("男");
u2.setAge(28);
u2.setBirth(new Date());
u2.setDesc("中端码农");
User u3 = new User();
u3.setName("mike");
u3.setSex("男");
u3.setAge(31);
u3.setBirth(new Date());
u3.setDesc("高端码农");
users.add(u);
users.add(u2);
users.add(u3);
map.addAttribute("users",users);
return "thymeleaf/test";
}
修改test.html:
<body bgcolor="pink">
each循环遍历:<br>
<table style="border:2px solid yellow;">
<thead align="center">
<tr>
<th>姓名</th>
<th>年龄</th>
<th>生日</th>
<th>备注</th>
</tr>
</thead>
<tbody align="center">
<!-- 遍历集合 -->
<tr th:each="user:${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.age}"></td>
<td th:text="${user.age} gt 23?你个老光棍!:小年轻们">23岁</td>
<td th:text="${#dates.format(user.birth,'yyyy/MM/dd')}"></td>
<td th:text="${user.desc}"></td>
</tr>
</tbody>
</table>
<br><br>
</body>
测试:
i、th:switch和th:case标签
这个是选择分支标签,和Java里面的switch case功能类似。
在application.yml中添加以下信息:
## 配置资源文件,供thymeleaf读取
messages:
basename: ycz/message ## 资源文件路径
cache-duration: 3600 ## 缓存时间
encoding: UTF-8 ## 编码格式
创建message.properties资源文件:
修改控制器:
修改test.html:
测试:
修改控制器:
刷新页面:
user的name属性值不一样,会进入不同的分支。其他th标签可以参考thymeleaf使用手册。
4.11、配置全局异常捕获
应用在运行的时候,由于各种不确定的原因,经常会发生异常。对于异常发生,一般有以下几种处理方式:
- 页面跳转(错误页)
- 助手类(异常捕捉,然后跳到错误页)
- 处理ajax异常
- 统一捕获系统中所有异常
4.11.1、页面跳转
这种在控制器中手动跳转的方式用的不多。先建立一个错误页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>捕获全局异常</title>
</head>
<body>
<h1 style="color:red;" align="center">发生错误!</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>
控制器:
@Controller
@RequestMapping("/err")
public class ErrorController {
@RequestMapping("/error")
public String error() {
int a = 2 / 0;
return "error";
}
}
测试:
如果发生异常,都返回一个页面,不分类的话,其实可以在配置中配置一个错误页就行了:
可以通过server.error.path
属性值来指定错误页。控制器里面返回null或不返回也可以:
再测试:
还是可以跳转到这个错误页面,因为错误页配置起了作用。
4.11.2、助手类
这种实际上是页面跳转的改进,只是在错误页面中添加了异常的一些信息,让人知道是哪里出了异常,不至于一脸懵。
如下定义一个增强的控制器:
/*
* 错误助手类
*/
//增强控制器
// 这个注解一般会和@ExceptionHandler注解一起使用,进行全局异常捕捉
@ControllerAdvice
public class MyExceptionHanlder {
// 定义错误页
private static final String ERROR_VIEW = "error";
// 捕获异常的方法
@ExceptionHandler(value = Exception.class)//捕获的异常类型
public ModelAndView errorHandler(HttpServletRequest request,
HttpServletResponse response,Exception e) throws Exception{
ModelAndView modelAndView = new ModelAndView();
// 异常对象存到视图中
modelAndView.addObject("exception",e);
// 获取此次请求的uri
StringBuffer url = request.getRequestURL();
// 存到视图中
modelAndView.addObject("url",url);
modelAndView.setViewName(ERROR_VIEW);
return modelAndView;
}
}
使用@ControllerAdvice + @ExceptionHandler这两个注解组合,定义一个增强的控制器,这个控制器用于捕获系统中的异常,对异常进行统一捕获处理。
测试:
在页面中获取到了发生异常的访问url和异常原因。控制台:
可以看到只要发生了异常,就会自动进入增强控制器中标注了@ExceptionHandler注解的方法,正是这个注解使得异常统一捕获成为了可能。
4.11.3、处理Ajax异常
定义一个统一的实体类,将异常或成功信息以json格式返回。
/**
* 自定义响应数据结构
* 这个类是提供给门户,ios,安卓,微信商城用的
* 200:表示成功
* 500:表示错误,错误信息在msg字段中
* 501:bean验证错误,不管多少个错误都以map形式返回
* 502:拦截器拦截到用户token出错
* 555:异常抛出信息
*/
public class JSONResult {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
// 响应状态码
private Integer status;
// 响应消息
private String msg;
// 响应数据
private Object data;
// 不使用
private String ok;
// 提供的get和set方法
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getOk() {
return ok;
}
public void setOk(String ok) {
this.ok = ok;
}
// 构造方法
public JSONResult() {
}
// 发生异常时的构造方法
public JSONResult(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
// 成功的构造方法
public JSONResult(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
// 发生异常时返回的对象
public static JSONResult build(Integer status, String msg, Object data) {
return new JSONResult(status, msg, data);
}
// 成功时返回的对象
public static JSONResult ok(Object data) {
return new JSONResult(data);
}
// 不使用
public static JSONResult ok() {
return new JSONResult(null);
}
// 发生500错误时返回
public static JSONResult errorMsg(String msg) {
return new JSONResult(500, msg, null);
}
// 发生501错误时返回
public static JSONResult errorMap(Object data) {
return new JSONResult(501, "error", data);
}
// 发生502错误时返回
public static JSONResult errorTokenMsg(String msg) {
return new JSONResult(502, msg, null);
}
// 发生555错误时返回
public static JSONResult errorException(String msg) {
return new JSONResult(555, msg, null);
}
// 状态码200时返回true,其余返回false
public Boolean isOK() {
return this.status == 200;
}
/**
* 将json结果集转换成JSONResult对象
* 需要转换的对象是一个类
*/
public static JSONResult formatToPojo(String jsonData, Class<?> clazz) {
try {
if (clazz == null) {
return MAPPER.readValue(jsonData, JSONResult.class);
}
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (clazz != null) {
if (data.isObject()) {
obj = MAPPER.readValue(data.traverse(), clazz);
} else if (data.isTextual()) {
obj = MAPPER.readValue(data.asText(), clazz);
}
}
return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
/**
* 将json串转换为JSONResult对象
*/
public static JSONResult format(String json) {
try {
return MAPPER.readValue(json, JSONResult.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转换成JSONResult对象
* 需要转换的对象是一个list
*/
public static JSONResult formatToList(String jsonData, Class<?> clazz) {
try {
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (data.isArray() && data.size() > 0) {
obj = MAPPER.readValue(data.traverse(),
MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
}
return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
}
修改助手类MyExceptionHanlder:
@ControllerAdvice
public class MyExceptionHanlder {
// 定义错误页
private static final String ERROR_VIEW = "error";
// 捕捉异常
@ExceptionHandler(value = Exception.class)
public Object errorHandler(HttpServletRequest request,
HttpServletResponse response,Exception e) throws Exception{
// 判断是否Ajax请求
// Ajax请求异常,返回json格式
if(isAjax(request)) {
return JSONResult.errorException(e.getMessage());
} else {//非Ajax请求异常,返回视图
ModelAndView mav = new ModelAndView();
mav.addObject("exception",e);
mav.addObject("url",request.getRequestURL());
mav.setViewName(ERROR_VIEW);
return mav;
}
}
// 定义一个方法,判断是否为Ajax请求
public static boolean isAjax(HttpServletRequest request) {
if(request.getHeader("X-Requested-With")!=null &&
request.getHeader("X-Requested-With").toString().
equals("XMLHttpRequest")) {
return true;
}
return false;
}
}
定义一个ajaxerror.js:
$.ajx({
url:'/ycz/err/getAjaxError',
type:'GET',
async:false,
// 结果返回成功时
success:function(data){
debugger;
// 正常时
if(data.status==200 && data.msg=="OK"){
alert('success!');
}else {//发生异常时
alert('发生异常:' + data.msg);
}
},
// 结果返回失败时
error:function(response,ajaxOptions,thrownError){
debugger;
alert('error!');
}
});
定义一个ajaxerror.html:
<!DOCTYPE html >
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>测试</title>
</head>
<body>
<h1>测试ajax错误异常</h1>
<!-- 引入jquery -->
<script th:src="@{/static/js/jquery.min.js}"></script>
<!-- 引入自定义js -->
<script th:src="@{/static/js/ajaxerror.js}"></script>
</body>
</html>
测试:
说明结果返回失败,查看控制台:
进入了增强控制器的异常捕捉方法,查看network:
Ajax请求出了异常。
Ajax请求异常,以json格式返回异常信息。
4.11.4、统一异常捕获
在实际的项目中,一般都会对系统中的所有异常进行统一捕获处理,返回指定格式信息。
修改MyExceptionHandler类:
/*
* 统一异常捕获
* 统一处理返回异常信息
*/
@ControllerAdvice
public class MyExceptionHandler {
// 捕捉异常
@ResponseBody
@ExceptionHandler(value = Exception.class)
public JSONResult defaultErrorResult(Exception e) {
return new JSONResult(99999,"发生异常!",e.getMessage());
}
}
修改控制器:
测试:
控制台:
异常被增强控制器中的方法捕获到了,然后执行@ExceptionHandler标注的方法。这种用的是最多的,一般项目上都会进行系统异常的统一捕获处理。
4.12、项目启动的默认页
SSM项目启动时,访问项目会有一个欢迎页面,因为在web.xml中配置了。但是在springboot中并没有自动配置,我们需要自己进行设置。一般有两种方法,设置默认的视图跳转控制器和继承WebMvcConfigurer类来实现。
4.12.1、控制器实现
先去掉配置文件中上下文路径,直接使用端口访问。
然后定义一个欢迎页面welcome.html:
定义默认视图跳转控制器:
@Controller
public class DefaultViewController {
@RequestMapping("/")
public String welcome() {
return "thymeleaf/welcome";
}
}
测试:
跳转成功。
4.12.2、继承WebMvcConfigurerAdapter类
先注释掉刚才定义的控制器:
原理是定义一个类直接继承WebMvcConfigurerAdapter类,然后覆写addViewControllers方法,如下:
// 配置形式注册到Spring容器中
@Configuration
public class DefaultViewInterceptor extends WebMvcConfigurerAdapter{
// 覆写addViewControllers方法
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/") // 设置路径
.setViewName("thymeleaf/welcome"); // 设置视图
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers(registry);
}
}
测试:
成功访问,这两种方式的效果是一样的。
4.13、整合MyBatis
先在pom.xml中引入依赖:
<!-- 德鲁伊数据源依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.0</version>
</dependency>
<!-- mysql驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- mybatis和spring的整合包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>
<!-- pagehelper分页依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
在application.yml中进行配置:
## 配置数据源
datasource:
url: jdbc:mysql://rm-m5e130nm7h37n6v982o.mysql.rds.aliyuncs.com:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: xxxxxx
password: xxxxxx
## 配置mybatis
mybatis:
type-aliases-package: com.ycz.demo.pojo
mapper-locations:
- classpath:mapper/*.xml
## 配置pagehelper分页
pagehelper:
helper-dialect: mysql ## 设置分页方言
## 设置为false时分页超限会返回null,设为true超限时始终返回最后一页内容
## 这里应设置合理,一般会设为false
reasonable: false
support-methods-arguments: true
params: countSql
以这张表为参照,先定义pojo:
public class Person {
private Integer id;
private String name;
private String password;
private String address;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
再定义mapper:
@Mapper
public interface PersonMapper {
}
启动类中开启Mapper扫描:
再定义Service层:
public interface PersonService {
}
@Service
public class PersonServiceImpl implements PersonService{
}
添加几个接口:
public interface PersonService {
// 添加
int addPerson(Person person);
// 修改
int updatePerson(Person person);
// 删除
int delPerson(int id);
// 查询所有
List<Person> findAll();
}
@Service
public class PersonServiceImpl implements PersonService{
@Autowired
PersonMapper personMapper;
@Override
public int addPerson(Person person) {
return personMapper.addPerson(person);
}
@Override
public int updatePerson(Person person) {
return personMapper.updatePerson(person);
}
@Override
public int delPerson(int id) {
return personMapper.delPerson(id);
}
@Override
public List<Person> findAll() {
return personMapper.findAll();
}
}
完善mapper:
@Mapper
public interface PersonMapper {
int addPerson(Person person);
int updatePerson(Person person);
@Delete("delete from person where id=#{ids}")
int delPerson(int id);
@Select("select * from person")
List<Person> findAll();
}
定义PersonMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycz.demo.dao.PersonMapper">
<!-- 添加记录 -->
<insert id="addPerson" parameterType="com.ycz.demo.pojo.Person">
insert into person(name,password,address,age) values
(#{name},#{password},#{address},#{age})
</insert>
<!-- 修改记录 -->
<update id="updatePerson" parameterType="com.ycz.demo.pojo.Person">
update person set
<if test="name!=null">
name=#{name},
</if>
<if test="password!=null">
password=#{password}
</if>
<if test="address!=null">
address=#{address}
</if>
<if test="age">
age=#{age}
</if>
where id=#{id}
</update>
</mapper>
定义一个控制器:
@RestController
@RequestMapping("/person")
public class PersonController {
@Autowired
PersonService personService;
@RequestMapping("/add")
public JSONResult add() {
// 这里直接用静态数据了
Person p = new Person();
p.setName("测试添加");
p.setPassword("123456");
p.setAddress("测试demo");
p.setAge(26);
int res = personService.addPerson(p);
if(res>0) {
return new JSONResult("添加成功!");
}
return new JSONResult("添加失败!");
}
@RequestMapping("/update")
public JSONResult update(int id) {
Person p = new Person();
p.setId(id);
p.setName("测试修改");
p.setAddress("修改11111");
int res = personService.updatePerson(p);
if(res>0) {
return new JSONResult("修改成功!");
}
return new JSONResult("修改失败!");
}
@RequestMapping("/del")
public JSONResult del(int id) {
int res = personService.delPerson(id);
if(res>0) {
return new JSONResult("删除成功!");
}
return new JSONResult("删除失败!");
}
@RequestMapping("/findAll")
public JSONResult findAll() {
List<Person> persons = personService.findAll();
if(persons!=null && persons.size()>0) {
return new JSONResult(persons);
}
return new JSONResult(null);
}
}
测试添加:
查看表:
测试修改:
查看表:
测试删除:
查看表:
测试查询所有:
以上接口测试完毕,没问题。
添加几个新的接口:
// 按照id查询
Person findById(int id);
// 按照年龄范围查询
List<Person> findByAge(int age);
// 多个参数查询
List<Person> findByArgs(Map<String,Object> map);
@Override
public Person findById(int id) {
return personMapper.findById(id);
}
@Override
public List<Person> findByAge(int age) {
return personMapper.findByAge(age);
}
@Override
public List<Person> findByArgs(Map<String,Object> map) {
return personMapper.findByArgs(map);
}
@Select("select * from person where id=#{id}")
Person findById(int id);
@Select("select * from person where age>#{age}")
List<Person> findByAge(int age);
List<Person> findByArgs(Map<String,Object> map);
<!-- 多参数查询 -->
<select id="findByArgs" resultType="com.ycz.demo.pojo.Person" parameterType="map">
select * from person where address like CONCAT(#{address},'%')
and age>#{age}
</select>
控制器添加新的方法:
@RequestMapping("/findById")
public JSONResult findById(int id) {
Person person = personService.findById(id);
if(person!=null) {
return new JSONResult(person);
}
return new JSONResult(null);
}
@RequestMapping("/findByAge")
public JSONResult findByAge(int age) {
List<Person> persons = personService.findByAge(age);
if(persons!=null && persons.size()>0) {
return new JSONResult(persons);
}
return new JSONResult(null);
}
@RequestMapping("/findByArgs")
public JSONResult findByArgs(String address,int age) {
Map<String,Object> map = new HashMap<>();
map.put("address",address);
map.put("age", age);
List<Person> persons = personService.findByArgs(map);
if(persons!=null && persons.size()>0) {
return new JSONResult(persons);
}
return new JSONResult(null);
}
测试:
接口测试没问题。
4.13.1、分页查询
以下添加两个分页查询的接口:
// 手动拼写分页参数
List<Person> findByPersonPaged(int page,int pageSize);
// 使用pageHelper分页
List<Person> findByPersonPage2(int page,int pageSize);
@Override
public List<Person> findByPersonPaged(int page, int pageSize) {
Map<String, Object> map = new HashMap<>();
map.put("offset", (page - 1) * pageSize);
map.put("pageSize", pageSize);
return personMapper.findByPersonPaged(map);
}
@Override
public List<Person> findByPersonPage2(int page, int pageSize) {
if (page < 0) {
page = 1;
}
if (pageSize < 5) {
pageSize = 5;
}
// 开始分页
PageHelper.startPage(page, pageSize);
// 获取分页对象
Page<Person> p = personMapper.findByPersonPage2();
// 从分页对象中获取查询列表
List<Person> persons = p.getResult();
return persons;
}
List<Person> findByPersonPaged(Map<String,Object> map);
Page<Person> findByPersonPage2();
<!-- 分页查询,手动拼接分页参数 -->
<select id="findByPersonPaged" resultType="com.ycz.demo.pojo.Person" parameterType="map">
select * from person limit #{offset},#{pageSize}
</select>
<!-- 分页查询,自动拼接分页参数 -->
<select id="findByPersonPage2" resultType="com.ycz.demo.pojo.Person">
select * from person
</select>
控制器中添加两个方法:
@RequestMapping("/queryPage")
public JSONResult queryPage(int page,int size) {
List<Person> persons = personService.findByPersonPaged(page, size);
if(persons!=null && persons.size()>0) {
return new JSONResult(persons);
}
return new JSONResult(null);
}
@RequestMapping("/queryPage2")
public JSONResult queryPage2(int page,int size) {
List<Person> persons = personService.findByPersonPage2(page, size);
if(persons!=null && persons.size()>0) {
return new JSONResult(persons);
}
return new JSONResult(null);
}
先测试第一个分页方法:
一共14条记录,每页5条,只有3页,没问题。
测试第二个分页方法:
每页10条,只有2页,没问题。
推荐使用PageHelper进行分页查询,这个插件能帮我们在查询时自动拼接分页参数,无需手动在SQL中拼接分页参数,原理是使用分页拦截器。
4.13.2、整合持久层事务
事务的隔离级别使用isolation属性来设置,隔离级别有如下5种:
- DEFAULT :默认。使用底层数据库的默认隔离级别。大部分数据库为READ_COMMITTED。
- READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
- READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
- REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
- SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
事务的传播行为使用propagation属性来设置,传播行为有如下7种:
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED 。
注意:执行insert、update、delete操作时使用REQUIRED,执行select时使用SUPPORTS
启动类上开启事务:
在Service层上设置事务的隔离级别和传播行为,增删改使用REQUIRED行为,查询使用SUPPORTS行为:
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
PersonMapper personMapper;
@Override
// 设置事务的隔离级别和传播行为
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.REQUIRED)
public int addPerson(Person person) {
return personMapper.addPerson(person);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.REQUIRED)
public int updatePerson(Person person) {
return personMapper.updatePerson(person);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.REQUIRED)
public int delPerson(int id) {
return personMapper.delPerson(id);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public List<Person> findAll() {
return personMapper.findAll();
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public Person findById(int id) {
return personMapper.findById(id);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public List<Person> findByAge(int age) {
return personMapper.findByAge(age);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public List<Person> findByArgs(Map<String, Object> map) {
return personMapper.findByArgs(map);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public List<Person> findByPersonPaged(int page, int pageSize) {
Map<String, Object> map = new HashMap<>();
map.put("offset", (page - 1) * pageSize);
map.put("pageSize", pageSize);
return personMapper.findByPersonPaged(map);
}
@Override
@Transactional(isolation = Isolation.DEFAULT,
propagation = Propagation.SUPPORTS)
public List<Person> findByPersonPage2(int page, int pageSize) {
if (page < 0) {
page = 1;
}
if (pageSize < 5) {
pageSize = 5;
}
// 开始分页
PageHelper.startPage(page, pageSize);
// 获取分页对象
Page<Person> p = personMapper.findByPersonPage2();
// 从分页对象中获取查询列表
List<Person> persons = p.getResult();
return persons;
}
}
4.14、整合data-jpa
这个是和hibernate框架相关的,比较简单的SQL我们直接可以使用data jpa来自动生成,没必要每个接口都自己写SQL语句,这样会很麻烦,data jpa为我们提供了可以快速进行增删改查的方法,直接用就行了。
先在pom中引入data jpa依赖:
<!-- 引入data jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
在application.yml中进行jpa的配置:
## 配置data jpa
jpa:
hibernate:
## 如果启动时表格式不一致则更新表,原有数据保留
ddl-auto: update
## 输出sql语句日志
show-sql: true
修改实体类,添加上一些注解,修改后的Person如下:
package com.ycz.demo.pojo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity//实体类标注
@Table(name = "person")//指定表
public class Person {
@Id//主键标注
@GeneratedValue(strategy = GenerationType.IDENTITY)//主键生成策略
//对应表中字段名,字段名一样时可以省略,如果不一样,一定要指定
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "password")
private String password;
@Column(name = "address")
private String address;
@Column(name = "age")
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "id:" + this.id + ",\n"
+ "name:" + this.name + ",\n"
+ "password:" + this.password + ",\n"
+ "address:" + this.address + ",\n"
+ "age:" + this.age;
}
}
然后创建一个接口,继承JpaRepository,如下:
/*
* 这个接口直接继承JpaRepository
*/
// 第一个参数是实体类型,第二个参数是实体类中的主键类型
public interface PersonDao extends JpaRepository<Person, Integer>{
// 按照地址精确查询
// 像findBy + 字段名这种驼峰形式,会帮我们自动生成SQL语句,所以无需写
// 多个字段用findBy + 字段名 + And + 字段名 + And + ...连接
// 但是这种只限于精确查询,模糊查询还是要自己写
List<Person> findByAddress(String address);
// 按照地址模糊查询
// 自己写SQL语句,参数用?占位符替代
@Query(value= "select * from person where address like ?%"
,nativeQuery = true)
List<Person> findByAddressLike(String address);
@Query(value = "select * from person where address like ?% and age>?"
,nativeQuery = true)
List<Person> findByAddressLikeAndAge(String address,int age);
}
JpaRepository接口为我们提供了简单增删改查的方法,所以连接口方法都不用写,SQL语句也自动帮我们生成了,对于没有提供的方法,那么就要自己在接口中添加,简单方法可以不用写SQL,稍微复杂的方法需要自己写SQL语句,注意即可。
然后在测试类中进行测试:
@SpringBootTest
// 配置Spring测试环境
@RunWith(SpringRunner.class)
public class Test1 {
@Autowired
private PersonDao personDao;
@Autowired
private PersonMapper personMapper;
// 测试添加
@Test
public void add() {
Person p = new Person();
p.setName("大地");
p.setPassword("11111");
p.setAddress("湖北武汉");
p.setAge(31);
Person person = personDao.save(p);
if(person!=null) {
System.out.println(person.toString());
}else {
System.out.println("添加失败!");
}
}
// 测试修改
@Test
public void update() {
Person p = new Person();
p.setId(32);
p.setName("大地修改");
p.setPassword("22222");
p.setAddress("湖北武汉");
p.setAge(31);
Person person = personDao.save(p);
if(person!=null) {
System.out.println(person.toString());
}else {
System.out.println("修改失败!");
}
}
// 测试删除
@Test
public void del() {
try {
personDao.deleteById(32);
System.out.println("删除成功!");
} catch (Exception e) {
System.err.println("删除失败!");
}
}
// 测试查询全部
@Test
public void findAll() {
List<Person> persons = personDao.findAll();
if(persons!=null && persons.size()>0) {
System.out.println("一共查询到" + persons.size() + "条记录!");
} else {
System.out.println("未找到任何记录!");
}
}
// 测试分页查询
// 分页查询最好使用mybatis
// 简单的SQL使用jpa提供的
@Test
public void findByPaged() {
int page = 1;
int size = 5;
// 分页
PageHelper.startPage(page,size);
// 获取分页对象
Page<Person> pg = personMapper.findByPersonPage2();
// 分页对象中获取数据列表
List<Person> persons = pg.getResult();
if(persons!=null && persons.size()>0) {
System.out.println("总记录:" + pg.getTotal() + "条!");
System.out.println("一共查询到" + persons.size() + "条记录!");
for(Person p:persons) {
System.out.println(p.toString());
}
} else {
System.out.println("未找到任何记录!");
}
}
// 测试按照地址查询
@Test
public void findByAddress() {
List<Person> persons = personDao.findByAddress("湖北武汉");
if(persons!=null && persons.size()>0) {
System.out.println("一共查询到" + persons.size() + "条记录!");
for(Person p:persons) {
System.out.println(p.toString());
}
} else {
System.out.println("未找到任何记录!");
}
}
// 按照地址模糊查询
@Test
public void findByAddressLike() {
List<Person> persons = personDao.findByAddressLike("湖北");
if(persons!=null && persons.size()>0) {
System.out.println("一共查询到" + persons.size() + "条记录!");
for(Person p:persons) {
System.out.println(p.toString());
}
} else {
System.out.println("未找到任何记录!");
}
}
// 按照地址和年龄范围查询
@Test
public void findByAddressAndAgeLike() {
List<Person> persons = personDao.findByAddressLikeAndAge("湖北",25);
if(persons!=null && persons.size()>0) {
System.out.println("一共查询到" + persons.size() + "条记录!");
for(Person p:persons) {
System.out.println(p.toString());
}
} else {
System.out.println("未找到任何记录!");
}
}
}
测试第一个添加方法:
可以看到控制台输出了Hibernate自动生成的SQL语句,并且添加成功了,查看表:
OK。没问题。
测试修改方法:
注意的是添加修改用的都是save方法,查看表:
修改成功,没问题。
测试删除方法:
查看表:
记录删除成功。
测试查询全部的方法:
没问题。
测试分页查询:
第一页数据,没问题。
测试按照地址精确查询:
精确查询到2条数据,没问题。
测试按照地址模糊查询:
模糊查询,湖北的查询到6条,没问题。
测试多参数查询:
查到2条数据,没问题。
小结:对于比较简单的增删改查,我们完全可以用data jpa来实现,对于比较复杂的SQL语句,使用MyBatis来实现,结合使用,可以提高开发效率。
4.15、整合redis
先在pom中引入依赖:
<!-- 引入redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中配置:
## 配置redis
redis:
host: localhost ## 主机地址
port: 6379 ## 端口号
password: 123456 ## 连接密码,设置了密码需要配置,没设置不需要
database: 1 ## 连接的数据库
jedis:
pool:
max-active: 1000 ## 设置最大连接池数
max-wait: -1 ## 设置连接池最大阻塞等待时间,负数表示没有限制
max-idle: 10 ## 连接池中的最大空闲连接
min-idle: 2 ## 连接池中的最小空闲连接
timeout: 1000 ## 连接超时时间
创建一个工具类JsonUtils处理json数据:
/**
* 工具类,处理json格式数据
*/
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
*/
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
创建控制器进行测试:
@RestController
@RequestMapping("/redis")
public class RedisController {
// 注入StringRedisTemplate模板
@Autowired
StringRedisTemplate stringRedisTemplate;
// 存取普通字符串
@RequestMapping("/t1")
public JSONResult t1() {
// 存字符串
stringRedisTemplate.opsForValue().set("name", "yanchengzhi");
// 取字符串
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println("取出的字符串:" + name);
return new JSONResult(name);
}
// 存取对象
@RequestMapping("/t2")
public JSONResult t2() {
Person p = new Person();
p.setName("yanchengzhi");
p.setPassword("ycz123456");
p.setAddress("湖北武汉");
p.setAge(25);
// 对象转json串
String jsonStr = JsonUtils.objectToJson(p);
// 存值
stringRedisTemplate.opsForValue().set("user", jsonStr);
// 取值
String userStr = stringRedisTemplate.opsForValue().get("user");
// json串转为对象
Person u = JsonUtils.jsonToPojo(userStr, Person.class);
System.out.println("取出内容:" + u.toString());
return new JSONResult(u);
}
}
测试:
查看数据库:
数据库:
现在这个2号数据库有两个key/value串。
将Redis的常用操作做一个封装,以配置的形式托管给Spring容器,如下:
/*
* 封装Redis常用操作
* 以配置的形式托管Spring
*/
@Configuration
public class RedisUtils {
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间
*
*/
public long ttl(String key) {
return stringRedisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*/
public void expire(String key, long timeout) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:INCR key,增加key一次
*/
public long incr(String key, long delta) {
return stringRedisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set<String> keys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*/
public void del(String key) {
stringRedisTemplate.delete(key);
}
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*/
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*/
public void set(String key, String value, long timeout) {
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*/
public String get(String key) {
return (String) stringRedisTemplate.opsForValue().get(key);
}
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*/
public void hset(String key, String field, Object value) {
stringRedisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*/
public String hget(String key, String field) {
return (String) stringRedisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*/
public void hdel(String key, Object... fields) {
stringRedisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*/
public Map<Object, Object> hgetall(String key) {
return stringRedisTemplate.opsForHash().entries(key);
}
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*/
public long lpush(String key, String value) {
return stringRedisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*/
public String lpop(String key) {
return (String) stringRedisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*/
public long rpush(String key, String value) {
return stringRedisTemplate.opsForList().rightPush(key, value);
}
}
修改控制器:
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
RedisUtils redisUtils;
// 使用封装好的方法操作redis
@RequestMapping("/t1")
public JSONResult t1() {
List<Person> persons = new ArrayList<>();
Person p0 = new Person();
p0.setName("yanchengzhi");
p0.setAddress("湖北武汉");
p0.setAge(25);
Person p2 = new Person();
p2.setName("云过梦无痕");
p2.setAddress("湖北黄冈");
p2.setAge(24);
Person p3 = new Person();
p3.setName("老云");
p3.setAddress("重庆渝北");
p3.setAge(28);
persons.add(p0);
persons.add(p2);
persons.add(p3);
// list转json串
String listJson = JsonUtils.objectToJson(persons);
// 存值,并设置过期时间
redisUtils.set("persons", listJson, 1200);
// 取值
String jsonStr = redisUtils.get("persons");
// json串转为list
List<Person> list = JsonUtils.jsonToList(jsonStr, Person.class);
if(list!=null && list.size()>0) {
for(Person p:persons){
System.out.println(p.toString());
}
}
return new JSONResult(list);
}
}
测试:
查看数据库:
右上角的TTL是剩余的过期时间,秒为单位。那么SpringBoot整合Redis就完成了,如果不想封装Redis通用操作的话,可以直接使用data-redis提供的StringRedisTemplate模板,只要注入就可以直接使用,可以满足大部分操作,也可以自己封装一个工具类,这样所有项目都可以用的上,比较方便。
4.16、整合Task定时任务
先在启动类上通过@EnableScheduling开启定时任务,默认是关闭的,需要手动来开启。也可以在任何能够被Spring扫描到的类上标这个注解,一般是在启动类上,这样好管理。
然后定义一个类,这个类需要托管到Spring容器,所以以配置的形式托管就行了,在这个类中定义需要执行定时任务的方法。如下:
/*
* 定时任务配置类
*/
@Configuration
public class Task {
// 指定一个日期格式化的文本
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new
SimpleDateFormat("HH:mm:ss");
// 定义需要定时执行的方法
@Scheduled(fixedRate = 5000)//定义执行规则,每5秒执行一次
public void reportCurrentTime() {
String time = SIMPLE_DATE_FORMAT.format(new Date());
System.out.println("现在时间:" + time);
}
}
启动微服务测试:
确实是5秒执行一次。修改规则:
测试:
在每分钟的10到30秒之间才会执行该方法,0-10秒和31-60秒之间不执行。需要注意的是,这里其实都是单线程执行任务的,所以一定会按照顺序执行,实际开发中不可能是单线程的,一般都是多线程,那么就需要用到线程池,只需要在线程池中创建多个线程就可以了,这里不说。
4.17、整合异步任务
先在启动类上通过@EnableAsync注解开启异步任务,它默认是关闭的,必须通过手动开启。如下:
其实同步和异步在本质上来说,是针对于方法而言的。定义一个异步任务配置类,在该类中定义异步方法。如下:
/*
* 异步任务配置类
*/
@Configuration
public class AsyncTask {
// 标了这个注解的是异步方法,没标的是同步方法
@Async
public Future<Boolean> doTask0() throws InterruptedException {
long t1 = System.currentTimeMillis();
// 当前线程休眠2秒
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
System.out.println("任务1耗时:" + (t2 - t1) + "ms");
return new AsyncResult<Boolean>(true);
}
@Async
public Future<Boolean> doTask2() throws InterruptedException {
long t1 = System.currentTimeMillis();
// 当前线程休眠1秒
Thread.sleep(1000);
long t2 = System.currentTimeMillis();
System.out.println("任务2耗时:" + (t2 - t1) + "ms");
return new AsyncResult<Boolean>(true);
}
@Async
public Future<Boolean> doTask3() throws InterruptedException {
long t1 = System.currentTimeMillis();
// 当前线程休眠0.8秒
Thread.sleep(800);
long t2 = System.currentTimeMillis();
System.out.println("任务3耗时:" + (t2 - t1) + "ms");
return new AsyncResult<Boolean>(true);
}
}
定义一个控制器:
@RestController
@RequestMapping("/task")
public class TaskController {
// 注入异步任务的配置类
@Autowired
AsyncTask asyncTask;
@RequestMapping("/t1")
public String t1() throws InterruptedException {
long t1 = System.currentTimeMillis();
// 执行异步方法
Future<Boolean> a = asyncTask.doTask0();
Future<Boolean> b = asyncTask.doTask2();
Future<Boolean> c = asyncTask.doTask3();
while(!a.isDone() || !b.isDone() || !c.isDone()) {
// 3个任务同时完成时,退出
if(a.isDone() && b.isDone() && c.isDone()) {
break;
}
}
long t2 = System.currentTimeMillis();
String times = "任务全部完成,总耗时为:" + (t2- t1) + "ms!";
System.out.println(times);
return times;
}
}
测试:
控制台:
可以看到,3个任务执行的总时间等于所需时间最长的方法的执行时间加上while循环的执行时间。异步任务的执行并不是按照先后顺序的,这3个方法可以同时执行,即不必等到一个方法执行结束后再执行另一个方法,3个方法同步进行,这就是异步调用。
修改一下,将异步注解全部去掉:
按照理论讲,现在3个方法是同步方法,必须按照先后顺序执行,即一个方法必须要等到另一个方法执行完毕后才会执行,重启服务测试:
可以看到,执行3个方法的总时间刚好等于分别执行每个方法所需时间的总和,必须按照顺序调用,这就是同步。
异步任务在实际开发中经常会用到,异步任务的常用场景:
- 发送短信、邮件。
- APP消息推送。
- 节省运维凌晨发布任务时间提供效率。
4.18、整合拦截器
先定义两个拦截器:
/*
* 定义拦截器
*/
public class OneInterceptor implements HandlerInterceptor {
// 请求处理之前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("被One拦截器拦截,放行....");
return true;
}
// 请求处理之后,视图渲染之前调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
// 请求结束之后,视图渲染完毕之后调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
// 自定义一个方法
public void returnErrorResponse(HttpServletResponse response, JSONResult jsonResult)
throws Exception {
// 设置响应对象的编码和类型
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
// 从响应对象中获取输出流
OutputStream outputStream = response.getOutputStream();
// 将对象转为json串
String jsonStr = JsonUtils.objectToJson(jsonResult);
// 写到浏览器
outputStream.write(jsonStr.getBytes("utf-8"));
// 刷新
outputStream.flush();
// 关闭流
outputStream.close();
}
}
/*
* 定义拦截器
*/
public class TwoInterceptor implements HandlerInterceptor {
// 请求处理之前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(true) {
returnErrorResponse(response, JSONResult.errorMsg("被Two拦截..."));
}
System.out.println("被Two拦截...");
return false;
}
// 请求处理之后,视图渲染之前调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
// 请求结束之后,视图渲染完毕之后调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
// 自定义一个方法
public void returnErrorResponse(HttpServletResponse response, JSONResult jsonResult)
throws Exception {
// 设置响应对象的编码和类型
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
// 从响应对象中获取输出流
OutputStream outputStream = response.getOutputStream();
// 将对象转为json串
String jsonStr = JsonUtils.objectToJson(jsonResult);
// 写到浏览器
outputStream.write(jsonStr.getBytes("utf-8"));
// 刷新
outputStream.flush();
// 关闭流
outputStream.close();
}
}
定义两个控制器:
@RestController
@RequestMapping("/one")
public class OneController {
@RequestMapping("/t1")
public String test() {
return "one控制器里面的test方法!";
}
}
@RestController
@RequestMapping("/two")
public class TwoController {
@RequestMapping("/t2")
public String test() {
return "two控制器里面的test方法!";
}
}
定义一个类继承WebMvcConfigurerAdapter类,重写addInterceptors方法,在该方法里面配置拦截器:
/*
* 拦截器配置类
*/
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter{
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置拦截器,拦截器将按照配置顺序执行
registry.addInterceptor(new OneInterceptor()).addPathPatterns("/one/**");
super.addInterceptors(registry);
}
}
测试:
这个请求被拦截器拦截到了,但是放行了,所以还是可以进入one控制器里面的test方法。修改一下:
测试:
因为Two拦截器的preHandle方法返回了false,不会继续往下执行了,请求也就到不了two控制器了。
再修改:
测试:
one控制器和two控制器里的方法全都被拦截了,因为第一个拦截器two的preHandle方法返回了false,所以请求阻塞在了这里,所以就算后面第二个拦截器one是通的,放行所有请求,请求也到达不了这里,阻塞在了前面。