SpringBoot学习笔记 学习时间:2022年1月20日
视频来源:黑马
1 基础篇 主要内容:
SpringBoot快速入门
SpringBoot基础配置
基于SpringBoot整合SSMP
1.1 SpringBoot快速上手 SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化 Spring应用的初始搭建以及开发过程。
1.1.1 IDEA联网版 下面使用SpringBoot技术快速构建一个SpringMVC的程序,通过这个过程体会简化二字的含义
① 构建步骤
新建一个空工程,然后创建新模块,选择Spring Initializr
,并配置模块相关基础信息
选择当前模块需要使用的技术集。因为是构建一个Spring MVC的程序,因此选择Spring Web。
开发控制类
1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/books") public class BookController { @GetMapping public String getById () { System.out.println("SpringBoot is running..." ); return "SpringBoot is running..." ; } }
入门案例制作的SpringMVC的控制器基于Rest风格 开发,当然此处使用原始格式制作SpringMVC的程序也是没有问题的,上例中的@RestController与@GetMapping注解是基于Restful开发的典型注解。
关注:做到这里SpringBoot程序的最基础的开发已经做完了,现在就可以正常的运行Spring程序了。可能有些小伙伴会有疑惑,Tomcat服务器没有配置,Spring也没有配置,什么都没有配置这就能用吗?这就是SpringBoot技术的强大之处。
运行自动生成的Application类
使用带main方法的java程序的运行形式来运行程序,运行完毕后,控制台输出上述信息。
不难看出,运行的信息中包含了8080的端口,Tomcat这种熟悉的字样,难道这里启动了Tomcat服务器?是的,这里已经启动了。那服务器没有配置,哪里来的呢?后面再说。现在你就可以通过浏览器访问请求的路径,测试功能是否工作正常了。
访问路径:http://localhost:8080/books
② 最简SpringBoot程序所包含的基础文件
pom文件:这是maven的配置文件,描述了当前工程构建时相应的配置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?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 > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.6.2</version > <relativePath /> </parent > <groupId > com.hongyi</groupId > <artifactId > demo</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > demo</name > <description > demo</description > <properties > <java.version > 11</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
配置中有两个信息需要关注,一个是parent,也就是当前工程继承了另外一个工程,干什么用的后面再说,还有依赖坐标,干什么用的后面再说。
1 2 3 4 5 6 7 8 @SpringBootApplication public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } }
这个类功能很简单,就一句代码,前面运行程序就是运行的这个类。
到这里我们可以大胆推测一下,如果上面这两个文件没有的话,SpringBoot肯定没法玩,看来核心就是这两个文件了。由于是制作第一个SpringBoot程序,先不要关注这两个文件的功能,后面详细讲解内部工作流程。
③ Spring程序与SpringBoot程序对比 通过上面的制作,我们不难发现,SpringBoot程序简直太好写了,几乎什么都没写,功能就有了,这也是SpringBoot技术为什么现在这么火的原因,和Spring程序相比,SpringBoot程序在开发的过程中各个层面均具有优势。
类配置文件
Spring
SpringBoot
pom文件中的坐标
手工添加
勾选添加
web3.0配置类
手工制作
无
Spring/SpringMVC配置类
手工制作
无
控制器
手工制作
手工制作
一句话总结一下就是能少写就少写,能不写就不写,这就是SpringBoot技术给我们带来的好处,行了,现在你就可以动手做一做SpringBoot程序了,看看效果如何,是否真的帮助你简化开发了。
④ 小结
开发SpringBoot程序可以根据向导进行联网 快速制作
SpringBoot程序需要基于JDK8以上版本进行制作
SpringBoot程序中需要使用何种功能通过勾选选择技术,也可以手工添加对应的要使用的技术(后期讲解)
运行SpringBoot程序通过运行Application程序入口进行
1.1.2 官网创建版 如果Idea不能正常联网,这个SpringBoot程序就无法制作了吗?开什么玩笑,世上IDE工具千千万,难道SpringBoot技术还必须基于Idea来做了?这是不可能的。开发SpringBoot程序,可以不基于任意的IDE工具进行,其实在SpringBoot的官网里面就可以直接创建SpringBoot程序。
① 步骤
进入页面https://start.spring.io/,下面是输入信息的过程,和前面的一样,只是界面变了而已,根据自己的要求,在左侧选择对应信息和输入对应的信息即可
将生成的工程文件解压缩后放入刚才的工作空间中,并导入该模块
新建控制器类,并运行
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("/books") public class BookController { @GetMapping public String getById () { System.out.println("SpringBoot is running...2" ); return "SpringBoot is running...2" ; } }
② 小结 官网创建版和idea联网版效果是一样的。
1.1.3 阿里云版 前面提到网站如果被限制访问了,该怎么办?开动脑筋想一想,不管是方式一还是方式二其实都是走的同一个地方,也就是SpringBoot的官网创建的SpringBoot工程,那如果我们国内有这么一个网站能提供这样的功能,是不是就解决了呢?必然的嘛,新的问题又来了,这个国内的网站有吗?还真有,阿里提供了一个,下面问题就简单了,网址告诉我们就OK了。
① 步骤
创建工程时,切换选择starter服务路径,然后手工输入阿里云提供给我们的使用地址即可。地址:http://start.aliyun.com或https://start.aliyun.com
其余步骤同1,2小节。此外,阿里为了便于自己开发使用,因此在依赖坐标中添加了一些阿里相关的技术,也是为了推广自己的技术吧,所以在依赖选择列表中,你有了更多的选择。不过有一点需要说清楚,阿里云地址默认创建的SpringBoot工程版本是2.4.1,所以如果你想更换其他的版本,创建项目后手工修改 即可,别忘了刷新一下,加载新版本信息。
注意:阿里云提供的工程创建地址初始化完毕后和使用SpringBoot官网创建出来的工程略有区别。主要是在配置文件pom.xml 的形式上有区别。这个信息在后面讲解Boot程序的执行流程时给大家揭晓。
运行结果:
② 小结
选择start来源为自定义URL
输入阿里云start地址
创建项目
1.1.4 手工创建版 略
1.1.5 隐藏不必要的文件 创建SpringBoot工程时,使用SpringBoot向导也好,阿里云也罢,其实都是为了一个目的,得到一个标准的SpringBoot工程文件结构。这个时候就有新的问题出现了,标准的工程结构中包含了一些未知的文件夹,在开发的时候看起来特别别扭,这一节就来说说这些文件怎么处理。
① 步骤
打开设置,【Files】→【Settings】
打开文件类型设置界面,【Editor】→【File Types】→【Ignored Files and Folders】,忽略文件或文件夹显示。
添加你要隐藏的文件名称或文件夹名称,可以使用*号通配符,表示任意,设置完毕即可到这里就做完了,其实就是Idea的一个小功能。
执行结果:
② 小结
【Files】→【Settings】
【Editor】→【File Types】→【Ignored Files and Folders】
输入要隐藏的名称,支持*号通配符
回车确认添加
1.2 SpringBoot简介 1.2.1 概述 SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。都简化了了哪些东西呢?其实就是针对原始的Spring程序制作的两个方面进行了简化。
Spring程序缺点
依赖设置繁琐
以前写Spring程序,使用的技术都要自己一个一个的写,现在不需要了,如果做过原始SpringMVC程序的小伙伴应该知道,写SpringMVC程序,最基础的spring-web和spring-webmvc这两个坐标时必须的。
配置繁琐
以前写配置类或者配置文件,然后用什么东西就要自己写加载bean这些东西,现在呢?什么都没写,照样能用
SpringBoot程序的核心功能及优点
起步依赖(简化依赖配置)
自动配置(简化常用工程相关配置)
配置过于繁琐,使用自动配置就可以做响应的简化,但是内部还是很复杂的,后面具体展开说。
辅助功能(内置服务器,……)
除了上面的功能,其实SpringBoot程序还有其他的一些优势,比如我们没有配置Tomcat服务器,但是能正常运行,这是SpringBoot程序的一个可以感知到的功能,也是SpringBoot的辅助功能之一。
下面结合入门程序来说说这些简化操作都在哪些方面进行体现的,一共分为4个方面
parent
starter
引导类
内嵌tomcat
1.2.2 parent SpringBoot关注到开发者在进行开发时,往往对依赖版本的选择具有固定的搭配格式,并且这些依赖版本的选择还不能乱搭配。比如A技术的2.0版与B技术的3.5版可以合作在一起,但是和B技术的3.7版合并使用时就有冲突。其实很多开发者都一直想做一件事情,就是将各种各样的技术配合使用的常见依赖版本进行收集整理,制作出了最合理的依赖版本配置方案,这样使用起来就方便多了。
① 版本依赖的管理 例如两个工程project-a
和project-b
的pom配置文件都是一样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.16</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency >
此时可以将版本号抽取出来,形成一个父工程project-dependencies
,父工程的pom文件内容如下,即和上面的内容相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.16</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency >
对于a和b两个工程pom文件内容,则可以改写如下:
1 2 3 4 5 <dependency > <groupId > itheima</groupId > <artifactId > project-dependencies</artifactId > <version > 1.1.10</version > </dependency >
此时,父工程对版本的依赖性还是很强,于是又将各个依赖的版本抽取出来,再次形成一个父工程project-parent
。
project-dependencies
的pom内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > ${druid.version}</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > ${mybatis.version}</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > ${mysql.version}</version > </dependency >
整个工程的父工程project-parent
的pom内容如下:
1 2 3 4 5 <properties > <druid.version > 1.1.16</druid.version > <mybatis.version > 3.5.6</mybatis.version > <mysql.version > 5.1.47</mysql.version > </properties >
② SpringBoot中的版本依赖管理 SpringBoot将所有的技术版本的常见使用方案都给开发者整理了出来,以后开发者使用时直接用它提供的版本方案,就不用担心冲突问题了,相当于SpringBoot做了无数个技术版本搭配的列表,这个技术搭配列表的名字叫做parent。
parent自身具有很多个版本,每个parent版本中包含有几百个其他技术的版本号 ,不同的parent间使用的各种技术的版本号有可能会发生变化。当开发者使用某些技术时,直接使用SpringBoot提供的parent就行了,由parent帮助开发者统一的进行各种技术的版本管理。
比如你现在要使用Spring配合MyBatis开发,没有parent之前怎么做呢?选个Spring的版本,再选个MyBatis的版本,再把这些技术使用时关联的其他技术的版本逐一确定下来。当你Spring的版本发生变化需要切换时,你的MyBatis版本有可能也要跟着切换,关联技术呢?可能都要切换,而且切换后还可能出现问题。现在这一切工作都可以交给parent来做了。你无需关注这些技术间的版本冲突问题,你只需要关注你用什么技术就行了,冲突问题由parent负责处理。
有人可能会提出来,万一parent给我导入了一些我不想使用的依赖怎么办?记清楚,这一点很关键,parent仅仅帮我们进行版本管理,它不负责帮你导入坐标,说白了用什么还是你自己定,只不过版本不需要你管理了。整体上来说,使用parent可以帮助开发者进行版本的统一管理
关注:parent定义出来以后,并不是直接使用的,仅仅给了开发者一个说明书,但是并没有实际使用,这个一定要确认清楚。
那SpringBoot又是如何做到这一点的呢?可以查阅SpringBoot的配置源码,看到这些定义
1 2 3 4 5 6 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.5.4</version > </parent >
1 2 3 4 5 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 2.5.4</version > </parent >
这个坐标(spring-boot-dependencies
)中定义了两组信息,第一组是各式各样的依赖版本号属性,下面列出依赖版本属性的局部,可以看的出来,定义了若干个技术的依赖版本号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <properties > <activemq.version > 5.16.3</activemq.version > <aspectj.version > 1.9.7</aspectj.version > <assertj.version > 3.19.0</assertj.version > <commons-codec.version > 1.15</commons-codec.version > <commons-dbcp2.version > 2.8.0</commons-dbcp2.version > <commons-lang3.version > 3.12.0</commons-lang3.version > <commons-pool.version > 1.6</commons-pool.version > <commons-pool2.version > 2.9.0</commons-pool2.version > <h2.version > 1.4.200</h2.version > <hibernate.version > 5.4.32.Final</hibernate.version > <hibernate-validator.version > 6.2.0.Final</hibernate-validator.version > <httpclient.version > 4.5.13</httpclient.version > <jackson-bom.version > 2.12.4</jackson-bom.version > <javax-jms.version > 2.0.1</javax-jms.version > <javax-json.version > 1.1.4</javax-json.version > <javax-websocket.version > 1.1</javax-websocket.version > <jetty-el.version > 9.0.48</jetty-el.version > <junit.version > 4.13.2</junit.version > </properties >
第二组是各式各样的的依赖坐标信息,可以看出依赖坐标定义中没有具体的依赖版本号,而是引用了第一组信息中定义的依赖版本属性值。同样只列出了部分内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependencyManagement > <dependencies > <dependency > <groupId > org.hibernate</groupId > <artifactId > hibernate-core</artifactId > <version > ${hibernate.version}</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > ${junit.version}</version > </dependency > </dependencies > </dependencyManagement >
关注:上面的依赖坐标定义是出现在<dependencyManagement>
标签中的,其实是对引用坐标的依赖管理,并不是实际使用的坐标。因此当你的项目中继承了这组parent信息后,在不使用对应坐标的情况下,前面的这组定义是不会具体导入某个依赖的。
关注:因为在maven中继承机会只有一次,上述继承的格式还可以切换成导入的形式 进行,并且在阿里云的starter创建工程时就使用了此种形式。
1 2 3 4 5 6 7 8 9 10 11 <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement >
1.2.3 starter ① 概述 SpringBoot关注到开发者在实际开发时,对于依赖坐标的使用往往都有一些固定的组合方式,比如使用spring-webmvc就一定要使用spring-web。每次都要固定搭配着写,非常繁琐,而且格式固定,没有任何技术含量。
SpringBoot把所有的技术使用的固定搭配格式都给开发出来,以后用某个技术,就不用一次写一堆依赖了,开发者使用的时候,直接用SpringBoot做好的这个东西就好了,对于这样的固定技术搭配 ,SpringBoot给它起了个名字叫做starter。
starter定义了使用某种技术时对于依赖的固定搭配格式,也是一种最佳解决方案,使用starter可以帮助开发者减少依赖配置
项目中的pom.xml定义了使用SpringMVC技术,但是并没有写SpringMVC的坐标,而是添加了一个名字中包含starter的依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
在spring-boot-starter-web中又定义了若干个具体依赖的坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-json</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-web</artifactId > <version > 5.3.9</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.3.9</version > <scope > compile</scope > </dependency > </dependencies >
我们可以发现,starter中包含了若干个坐标,其实就是使用SpringMVC开发通常都会使用到Json,使用json又离不开这里面定义的这些坐标,看来还真是方便,SpringBoot把我们开发中使用的东西能用到的都给提前做好了。你仔细看完会发现,里面有一些你没用过的。的确会出现这种过量导入的可能性,没关系,可以通过maven中的排除依赖 剔除掉一部分。
到这里基本上得到了一个信息,使用starter可以帮开发者快速配置依赖关系 。以前写依赖3个坐标的,现在写导入一个starter就搞定了,它就是加速依赖配置的。
② starter与parent的区别
③ 实际开发应用方式
1 2 3 4 5 6 7 8 9 10 11 <properties > <activemq.version > 5.16.3</activemq.version > <aspectj.version > 1.9.7</aspectj.version > <assertj.version > 3.19.0</assertj.version > <commons-codec.version > 1.15</commons-codec.version > <commons-dbcp2.version > 2.8.0</commons-dbcp2.version > <commons-lang3.version > 3.12.0</commons-lang3.version > <commons-pool.version > 1.6</commons-pool.version > <commons-pool2.version > 2.9.0</commons-pool2.version > </properties >
starter格式
SpringBoot官方给出了好多个starter的定义,方便我们使用,而且名称都是如下格式:
1 命名规则:spring-boot-starter-技术名称
1.2.4 引导类 下面说一下程序是如何运行的。目前程序运行的入口就是SpringBoot工程创建时自带的那个类了,带有main方法的那个类,运行这个类就可以启动SpringBoot工程的运行。
1 2 3 4 5 6 7 @SpringBootApplication public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } }
SpringBoot本身是为了加速Spring程序的开发的,而Spring程序运行的基础是需要创建自己的Spring容器对象(IoC容器)并将所有的对象交给Spring的容器管理,也就是一个一个的Bean。SpringBoot加速开发Spring程序,这个容器还在吗?这个疑问不用说,一定在。当前这个类运行后就会产生一个Spring容器对象 ,并且可以将这个对象保存起来,通过容器对象直接操作Bean。
1 2 3 4 5 6 7 8 9 @SpringBootApplication public class DemoApplication { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args); BookController bean = context.getBean(BookController.class); System.out.println(bean); } }
运行结果:
通过上述操作不难看出,其实SpringBoot程序启动还是创建了一个Spring容器对象。这个类在SpringBoot程序中是所有功能的入口 ,称这个类为引导类 。
作为一个引导类最典型的特征就是当前类上方声明了一个注解@SpringBootApplication
1.2.5 内嵌Tomcat 当前我们做的SpringBoot入门案例勾选了Spirng-web的功能,并且导入了对应的starter。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
SpringBoot发现,既然你要做web程序,肯定离不开使用web服务器,这样吧,帮人帮到底,送佛送到西。我帮你搞一个web服务器,你要愿意用的,直接使用就好了,干脆我再多给你几种选择,你随便切换。万一你不想用我给你提供的,也行,你可以自己搞。
由于这个功能不属于程序的主体功能 ,可用可不用,于是乎SpringBoot将其定位成辅助功能,别小看这么一个辅助功能,它可是帮我们开发者又减少了好多的设置性工作。
① 内嵌Tomcat定义位置 打开查看web的starter导入了哪些东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-json</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-web</artifactId > <version > 5.3.9</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.3.9</version > <scope > compile</scope > </dependency > </dependencies >
第三个依赖就是这个tomcat对应的东西了,居然也是一个starter,再打开看看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <dependencies > <dependency > <groupId > jakarta.annotation</groupId > <artifactId > jakarta.annotation-api</artifactId > <version > 1.3.5</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-core</artifactId > <version > 9.0.52</version > <scope > compile</scope > <exclusions > <exclusion > <artifactId > tomcat-annotations-api</artifactId > <groupId > org.apache.tomcat</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-el</artifactId > <version > 9.0.52</version > <scope > compile</scope > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-websocket</artifactId > <version > 9.0.52</version > <scope > compile</scope > <exclusions > <exclusion > <artifactId > tomcat-annotations-api</artifactId > <groupId > org.apache.tomcat</groupId > </exclusion > </exclusions > </dependency > </dependencies >
这里面有一个核心的坐标,tomcat-embed-core
,叫做tomcat内嵌核心。就是这个东西把tomcat功能引入到了我们的程序中。目前解决了第一个问题,找到根儿了,谁把tomcat引入到程序中的?是spring-boot-starter-web中的spring-boot-starter-tomcat做的。之所以你感觉很奇妙的原因就是,这个东西是默认加入到程序中了,所以感觉很神奇,居然什么都不做,就有了web服务器对应的功能,再来说第二个问题,这个服务器是怎么运行的。
② 内嵌Tomcat运行原理 Tomcat服务器是一款软件,而且是一款使用java语言开发的软件,熟悉的小伙伴可能有印象,tomcat安装目录中保存有jar,好多个jar。
下面的问题来了,既然是使用java语言开发的,运行的时候肯定符合java程序运行的原理,java程序运行靠的是什么?对象呀,一切皆对象,万物皆对象。那tomcat运行起来呢?也是对象。
如果是对象,那Spring容器是用来管理对象的,这个对象能不能交给Spring容器管理呢?把吗去掉,是个对象都可以交给Spring容器管理,行了,这下通了。tomcat服务器运行其实是以对象的形式在Spring容器中运行的 ,怪不得我们没有安装这个tomcat,而且还能用。闹了白天这东西最后是以一个对象的形式存在,保存在Spring容器中悄悄运行的。具体运行的是什么呢?其实就是上前面提到的那个tomcat内嵌核心 。
那既然是个对象,如果把这个对象从Spring容器中去掉是不是就没有web服务器的功能呢?是这样的,通过依赖排除 可以去掉这个web服务器功能。
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency >
运行结果:
发现并没有输出服务器运行的信息,并且SpringBoot程序还停止了。
③ 更换内嵌Tomcat 根据SpringBoot的工作机制,用什么技术,加入什么依赖就行了。SpringBoot提供了3款内置的服务器。
例如替换为jetty服务器:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jetty</artifactId > </dependency >
运行结果:
1.2.6 补充:REST开发 ① 简介
REST:Representational State Transfer
,表现形式状态转换
传统风格资源描述形式
http://localhost/user/getById?id=1
http://localhost/user/saveUser
REST风格描述形式
http://localhost/user/1
http://localhost/user
优点
隐藏资源的访问行为,无法通过地址得知对资源是何种操作
书写简化
行为动作
按照REST风格访问资源时,使用行为动作 区分对资源进行了何种操作
请求地址
操作
行为动作
http://localhost/users
查询全部用户信息
GET(查询)
http://localhost/users/1
查询指定用户信息
GET(查询)
http://localhost/users
添加用户信息
POST(新增/保存)
http://localhost/users
修改用户信息
PUT(修改/更新)
http://localhost/users/1
删除用户信息
DELETE(删除)
根据REST风格对资源进行访问称为RESTful
注意:
② 入门案例 暂略
1.3 SpringBoot基础配置 1.3.1 准备——复制工程
步骤
准备一个工程模板,例如demo2
,复制一份,并修改工程名称demo-template
删除demo-template
下与IDEA相关配置文件,只保留src和pom.xml
另外复制一份demo-template
,取名为demo_base_config
修改demo_base_config
的pom.xml中的artifactId
与工程名相同(即demo_base_config
)(重要!),然后删除其中的name
标签
保留备份工程demo-template
,日后使用
将工程导入demo_base_config
模块并测试功能
测试功能:
1.3.2 属性配置 ① 服务器端口配置 SpringBoot通过配置文件application.properties
就可以修改默认的配置(通过键值对配置对应属性),那咱们就先找个简单的配置下手,当前访问tomcat的默认端口是8080,我们改成80,通过这个操作来熟悉一下SpringBoot的配置格式是什么样的。
修改application.properties
:
注意:
SpringBoot程序可以在application.properties文件中进行属性配置
application.properties文件中只要输入要配置的属性关键字就可以根据提示进行设置
SpringBoot将配置信息集中在一个文件中写 ,不管你是服务器的配置,还是数据库的配置,总之都写在一起,逃离一个项目十几种配置文件格式的尴尬局面
② 其他配置
1 spring.main.banner-mode =off
1 spring.banner.image.location =takao.png
1 logging.level.root =debug
SpringBoot内置属性查询
文档网址:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties
温馨提示
所有的starter中都会依赖下面这个starter,叫做spring-boot-starter
。这个starter是所有的SpringBoot的starter的基础依赖 ,里面定义了SpringBoot相关的基础配置,关于这个starter我们到开发应用篇和原理篇中再深入讲解。
1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > <version > 2.5.4</version > <scope > compile</scope > </dependency >
1.3.3 配置文件分类 ① 三种配置格式 现在已经能够进行SpringBoot相关的配置了,但是properties格式的配置写起来总是觉得看着不舒服,所以就期望存在一种书写起来更简便的配置格式提供给开发者使用。SpringBoot除了支持properties格式的配置文件,还支持另外两种格式的配置文件。分别如下:
properties
格式(传统和默认格式)
yml
格式(主流)
yaml
格式
格式示例
application.properties
(properties格式)
仔细看会发现yml格式和yaml格式除了文件名后缀不一样,格式完全一样,是这样的,yml和yaml文件格式就是一模一样的,只是文件后缀不同,所以可以合并成一种格式来看。那对于这三种格式来说,以后用哪一种比较多呢?记清楚,以后基本上都是用yml格式的 。
② 配置文件的优先级
配置文件间的加载优先级 properties(最高)> yml > yaml(最低)
不同配置文件中相同配置按照加载优先级相互覆盖,不同配置文件中不同配置全部保留
以后的开发都使用yml
格式
③ 自动提示功能消失的解决方案 下图中,application.yml
和application.yaml
并没有没IDEA识别为SpringBoot的配置文件(没有绿叶的标志),并且编辑文件时也没有代码提示功能。
这个自动提示功能消失的原因还是蛮多的,如果想解决这个问题,就要知道为什么会消失,大体原因有如下2种:
Idea认为你现在写配置的文件不是个配置文件,所以拒绝给你提供提示功能
Idea认定你是合理的配置文件,但是Idea加载不到对应的提示信息
这里我们主要解决第一个现象,第二种现象到原理篇再讲解。第一种现象的解决方式如下:
步骤
打开设置,【Files】→【Project Structure…】
在弹出窗口中左侧选择【Facets】,右侧选中Spring路径下对应的模块名称,也就是你自动提示功能消失的那个模块。
点击Customize Spring Boot按钮,此时可以看到当前模块对应的配置文件是哪些了。如果没有你想要称为配置文件的文件格式,就有可能无法弹出提示。选择添加配置文件,然后选中要作为配置文件的具体文件就OK了。
执行结果:
发现已有绿叶的标志,并且有代码提示功能
1.3.4 yaml文件 ① 概述 SpringBoot的配置以后主要使用yml结尾的这种文件格式,并且在书写时可以通过提示的形式加载正确的格式。但是这种文件还是有严格的书写格式要求的。下面就来说一下具体的语法格式。
YAML(YAML Ain't Markup Language
),一种数据序列化格式。具有容易阅读、容易与脚本语言交互、以数据为核心,重数据轻格式的特点。常见的文件扩展名 有两种:
对于文件自身在书写时,具有严格的语法格式要求,具体如下:
大小写敏感
属性层级关系使用多行描述,每行结尾使用冒号结束
使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
属性值前面添加空格 (属性名与属性值之间使用冒号+空格
作为分隔)
#
号表示注释
示例
1 2 3 4 5 6 7 8 boolean: TRUE float: 3.14 int: 123 null: ~ string: HelloWorld string2: "Hello World" date: 2018-02-17 datetime: 2018-02-17T15:02:31+08:00
此外,yaml格式中也可以表示数组,在属性名书写位置的下方使用减号
作为数据开始符号,每行书写一个数据,减号与数据间空格分隔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 subject: - Java - 前端 - 大数据 subject: [Java , 前端 , 大数据 ]enterprise: name: itcast age: 16 subject: - Java - 前端 - 大数据 users: - name: Tom age: 4 - name: Jerry age: 5 users: - name: Tom age: 4 - name: Jerry age: 5 users2: [ { name:Tom , age:4 } , { name:Jerry , age:5 } ]
② 读取单一数据 对于yaml文件中的数据,其实你就可以想象成这就是一个小型的数据库,里面保存有若干数据,每个数据都有一个独立的名字,如果你想读取里面的数据,肯定是支持的,下面就介绍3种读取数据的方式。
yaml中保存的单个数据,可以使用Spring中的注解直接读取,使用@Value
可以读取单个数据,属性名引用方式:${一级属性名.二级属性名……}
示例
1 2 3 4 5 6 7 8 9 country: china province: city: Chengdu likes: - game - music - sleep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping("/books") public class BookController { @Value("${country}") private String country; @Value("${province.city}") private String city; @Value("${likes[1]}") private String likes1; @GetMapping public String getById () { System.out.println("SpringBoot is running..." ); System.out.println("country: " + country); System.out.println("city: " + city); System.out.println("likes[1]: " + likes1); return "SpringBoot is running..." ; } }
执行结果:
③ 数据引用 如果你在书写yaml数据时,经常出现如下现象,比如很多个文件都具有相同的目录前缀:
1 2 3 4 5 center: dataDir: /usr/local/fire/data tmpDir: /usr/local/fire/tmp logDir: /usr/local/fire/log msgDir: /usr/local/fire/msgDir
在配置文件中可以使用属性名引用方式引用属性 :
1 2 3 4 5 6 baseDir: /usr/local/fire center: dataDir: ${baseDir}/data tmpDir: ${baseDir}/tmp logDir: ${baseDir}/log msgDir: ${baseDir}/msgDir
还有一个注意事项,在书写字符串时,如果需要使用转义字符 ,需要将数据字符串使用双引号包裹起来:
1 lesson: "Spring\tboot\nlesson"
示例
1 2 3 baseDir: c:\windows tempDir: ${baseDir}\temp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/books") public class BookController { @Value("${tempDir}") private String tempDir; @GetMapping public String getById () { System.out.println("SpringBoot is running..." ); System.out.println("tempDir: " + tempDir); return "SpringBoot is running..." ; } }
执行结果:
④ 读取全部数据 读取单一数据可以解决读取数据的问题,但是如果定义的数据量过大,这么一个一个书写肯定会累死人的,SpringBoot提供了一个对象,能够把所有的数据都封装到这一个对象中,这个对象叫做Environment
,使用自动装配注解 可以将所有的yaml数据封装到这个对象中。
数据封装到了Environment对象中,获取属性时,通过Environment的接口操作进行,具体方法是getProperties(String),参数填写属性名即可。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RestController @RequestMapping("/books") public class BookController { @Autowired private Environment env; @GetMapping public String getById () { System.out.println("SpringBoot is running..." ); System.out.println("country: " + env.getProperty("country" )); System.out.println("city: " + env.getProperty("province.city" )); return "SpringBoot is running..." ; } }
执行结果:
⑤ 读取对象数据 单一数据读取书写比较繁琐,全数据封装又封装的太厉害了,每次拿数据还要一个一个的getProperties
,总之用起来都不是很舒服。由于Java是一个面向对象的语言,很多情况下,我们会将一组数据封装成一个对象。SpringBoot也提供了可以将一组yaml对象数据封装一个Java对象的操作。
首先定义一个对象,并将该对象纳入Spring管控的范围,也就是定义成一个bean,然后使用注解@ConfigurationProperties
指定该对象加载哪一组yaml中配置的信息。
示例
1 2 3 4 5 datasource: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost/springboot_db username: root password: 12345678
新建一个类MyDataSource:
1 2 3 4 5 6 7 8 9 10 @Data @ToString @Component @ConfigurationProperties(prefix = "datasource") public class MyDataSource { private String driver; private String url; private String username; private String password; }
控制层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestController @RequestMapping("/books") public class BookController { @Autowired private MyDataSource myDataSource; @GetMapping public String getById () { System.out.println("SpringBoot is running..." ); System.out.println(myDataSource); System.out.println("driver: " + myDataSource.getDriver()); return "SpringBoot is running..." ; } }
执行结果:
1.4 第三方技术整合 1.4.1 Junit整合 首先新建一个模块demo_junit
,不添加任何外部依赖。新建接口和实现类BookDao
和BookDaoImpl
。
1 2 3 public interface BookDao { public void save () ; }
1 2 3 4 5 6 7 @Repository public class BookDaoImpl implements BookDao { @Override public void save () { System.out.println("book is running..." ); } }
Spring整合Junit的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTestCase { @Autowired private AccountService accountService; @Test public void testGetById () { System.out.println(accountService.findById(2 )); } }
Springboot整合Junit的方式
在pom文件中,已经默认导入了junit相关的依赖:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
因此直接在测试类中书写代码即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootTest(classes = DemoJunitApplication.class) class DemoJunitApplicationTests { @Autowired private BookDao bookDao; @Test void contextLoads () { bookDao.save(); } }
测试结果:
@SpringBootTest
注解:
类型:测试类注解
位置:测试类定义的上方
作用:设置Junit加载的springboot启动类
属性:class
设置springboot启动类
注意:如果测试类在SpringBoot启动类的包或子包 中,可以省略 启动类的设置,也就是省略classes的设定。如果不在包或子包中,却省略了启动类设置,会导致测试过程报错。
1 2 Test ignored. java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
1.4.2 整合MyBatis 新建一个模块demo_mybatis
,注意添加mybatis框架
和mysql驱动
的依赖:
① Spring中整合MyBatis
导入坐标,MyBatis坐标不能少,Spring整合MyBatis还有自己专用的坐标,此外Spring进行数据库操作的jdbc坐标是必须的,剩下还有mysql驱动坐标,本例中使用了Druid数据源,这个倒是可以不要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <dependencies > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.16</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 1.3.0</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.10.RELEASE</version > </dependency > </dependencies >
1 2 3 4 5 6 @Configuration @ComponentScan("com.itheima") @PropertySource("jdbc.properties") public class SpringConfig { }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Configuration public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory (DataSource dataSource) { SqlSessionFactoryBean sfb = new SqlSessionFactoryBean (); sfb.setDataSource(dataSource); sfb.setTypeAliasesPackage("com.itheima.domain" ); return sfb; } @Bean public MapperScannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer msc = new MapperScannerConfigurer (); msc.setBasePackage("com.itheima.dao" ); return msc; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean("dataSource") public DataSource dataSource () { DruidDataSource ds = new DruidDataSource (); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }
1 2 3 4 jdbc.driver =com.mysql.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/spring_db?useSSL=false jdbc.username =root jdbc.password =root
② SpringBoot整合MyBatis 步骤① :查看默认导入的相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.2.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
发现是mybatis-spring-boot-starter
,与SpringBoot官方的命名方式刚好相反。此外测试的依赖starter也导入了进来。
注:starter命名规范
starter所属
命名规则
示例
官方提供
spring-boot-starter-技术名称
spring-boot-starter-test
第三方提供
第三方技术名称-spring-boot-starter
druid-spring-boot-starter
第三方提供
第三方技术名称-boot-starter(第三方技术名称过长,简化命名)
mybatis-plus-boot-starter
步骤② :配置数据源相关信息,没有这个信息你连接哪个数据库都不知道
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: 12345678
步骤③ :编写实体类、映射接口和测试类代码
1 2 3 4 5 6 7 8 @Data @ToString public class Book { private Integer id; private String type; private String name; private String description; }
1 2 3 4 5 @Mapper public interface BookDao { @Select("select * from book where id = #{id}") public Book getById (Integer id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest class DemoMybatisApplicationTests { @Autowired private BookDao bookDao; @Test void contextLoads () { System.out.println(bookDao.getById(1 )); } }
测试结果:
1.4.3 整合MyBatisPlus 做完了两种技术的整合了,各位小伙伴要学会总结,我们做这个整合究竟哪些是核心?总结下来就两句话:
导入对应技术的starter坐标
根据对应技术的要求做配置
整合步骤
步骤① :导入对应的starter:
1 2 3 4 5 6 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.0</version > </dependency >
截止目前,SpringBoot官网还未收录此坐标,而我们Idea创建模块时读取的是SpringBoot官网的Spring Initializr,所以也没有。如果换用阿里云的url创建项目可以找到对应的坐标。也可以在maven中央仓库中查找该坐标。
步骤② :配置数据源信息
步骤③ :编写映射接口、测试类代码
核心在于Dao接口继承了一个BaseMapper
的接口,这个接口中帮助开发者预定了若干个常用的API接口,简化了通用API接口的开发工作。
1 2 3 4 @Mapper public interface BookDao extends BaseMapper <Book> {}
测试类:
1 2 3 4 5 6 7 8 @Test void testGetAll () { List<Book> books = bookDao.selectList(null ); for (Book book : books) { System.out.println(book); } }
1.4.4 整合Druid 前面整合MyBatis和MP的时候,使用的数据源对象都是SpringBoot默认的数据源对象,下面我们手工控制一下,自己指定了一个数据源对象,Druid
。
在没有指定数据源时,我们的配置如下:
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai username: root password: root
此时虽然没有指定数据源,但是根据SpringBoot的德行,肯定帮我们选了一个它认为最好的数据源对象,这就是HiKari
。通过启动日志可以查看到对应的身影。
整合步骤
步骤① :导入对应的starter
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.8</version > </dependency >
步骤② :修改配置,在数据源配置中有一个type属性,专用于指定数据源类型
1 2 3 4 5 6 7 8 9 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: 12345678 type: com.alibaba.druid.pool.DruidDataSource
注意,这种形式不推荐,下面这种形式更推荐 :
1 2 3 4 5 6 7 8 spring: datasource: druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: 12345678
步骤③ :测试
1 2 3 4 @Test void contextLoads () { System.out.println(bookDao.getById(3 )); }
这是我们做的第4个技术的整合方案,还是那两句话:导入对应starter,使用对应配置 。没了,SpringBoot整合其他技术就这么简单粗暴。
1.5 SpringBoot整合综合案例 1.5.1 技术选型
实体类开发————使用Lombok快速制作实体类
Dao开发————整合MyBatisPlus,制作数据层测试
Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类
Controller开发————基于Restful开发,使用PostMan测试接口功能
Controller开发————前后端开发协议制作
页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理
项目异常处理
按条件查询————页面功能调整、Controller修正功能、Service修正功能
可以看的出来,东西还是很多的,希望通过这个案例,各位小伙伴能够完成基础开发的技能训练。整体开发过程采用做一层测一层的形式进行,过程完整,战线较长,希望各位能跟进进度,完成这个小案例的制作。
1.5.2 模块创建 新建一个模块ssmp
,在初始化模块时,添加mysql驱动,SpringWeb,Lombok技术。
之后手动导入mybatisplus,druid技术的依赖
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.0</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.8</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
application.yml
1.5.3 实体类开发 本案例对应的模块表结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 DROP TABLE IF EXISTS `tbl_book`;CREATE TABLE `tbl_book` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `type` varchar (20 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `name` varchar (50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `description` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ; INSERT INTO `tbl_book` VALUES (1 , '计算机理论' , 'Spring实战 第5版' , 'Spring入门经典教程,深入理解Spring原理技术内幕' );INSERT INTO `tbl_book` VALUES (2 , '计算机理论' , 'Spring 5核心原理与30个类手写实战' , '十年沉淀之作,手写Spring精华思想' );INSERT INTO `tbl_book` VALUES (3 , '计算机理论' , 'Spring 5 设计模式' , '深入Spring源码剖析Spring源码中蕴含的10大设计模式' );INSERT INTO `tbl_book` VALUES (4 , '计算机理论' , 'Spring MVC+MyBatis开发从入门到项目实战' , '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手' );INSERT INTO `tbl_book` VALUES (5 , '计算机理论' , '轻量级Java Web企业应用实战' , '源码级剖析Spring框架,适合已掌握Java基础的读者' );INSERT INTO `tbl_book` VALUES (6 , '计算机理论' , 'Java核心技术 卷I 基础知识(原书第11版)' , 'Core Java 第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新' );INSERT INTO `tbl_book` VALUES (7 , '计算机理论' , '深入理解Java虚拟机' , '5个维度全面剖析JVM,大厂面试知识点全覆盖' );INSERT INTO `tbl_book` VALUES (8 , '计算机理论' , 'Java编程思想(第4版)' , 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉' );INSERT INTO `tbl_book` VALUES (9 , '计算机理论' , '零基础学Java(全彩版)' , '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术' );INSERT INTO `tbl_book` VALUES (10 , '市场营销' , '直播就该这么做:主播高效沟通实战指南' , '李子柒、李佳琦、薇娅成长为网红的秘密都在书中' );INSERT INTO `tbl_book` VALUES (11 , '市场营销' , '直播销讲实战一本通' , '和秋叶一起学系列网络营销书籍' );INSERT INTO `tbl_book` VALUES (12 , '市场营销' , '直播带货:淘宝、天猫直播从新手到高手' , '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+' );
实体类
1 2 3 4 5 6 7 @Data public class Book { private Integer id; private String type; private String name; private String description; }
Lombok
,一个Java类库,提供了一组注解,简化POJO实体类开发,SpringBoot目前默认集成了lombok技术,并提供了对应的版本控制,所以只需要提供对应的坐标即可,在pom.xml中添加lombok的坐标。
使用lombok可以通过一个注解@Data完成一个实体类对应的getter,setter,toString,equals,hashCode 等操作的快速添加。对于构造器可以使用注解:
1 2 @NoArgsConstructor @AllArgsConstructor
1.5.4 数据层开发 ① 基础CRUD 数据层开发本次使用MyBatisPlus技术,数据源使用前面学习的Druid,学都学了都用上。
步骤① :导入MyBatisPlus与Druid对应的starter,当然mysql的驱动不能少
步骤② :配置数据库连接相关的数据源配置。注意MP技术默认的主键生成策略为雪花算法 ,生成的主键ID长度较大,和目前的数据库设定规则不相符,需要配置一下使MP使用数据库的主键生成策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: datasource: druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: 12345678 mybatis-plus: global-config: db-config: table-prefix: tbl_ id-type: auto
步骤③ :使用MP的标准通用接口BaseMapper
加速开发,别忘了@Mapper和泛型的指定
1 2 3 4 @Mapper public interface BookDao extends BaseMapper <Book> {}
步骤④ :制作测试类测试结果,这个测试类制作是个好习惯,不过在企业开发中往往都为加速开发跳过此步,且行且珍惜吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @SpringBootTest public class BookDaoTest { @Autowired private BookDao bookDao; @Test void testGetById () { System.out.println(bookDao.selectById(2 )); } @Test void testSave () { Book book = new Book (); book.setType("test" ); book.setName("test123" ); book.setDescription("123test" ); bookDao.insert(book); } @Test void testUpdate () { Book book = new Book (); book.setId(51 ); book.setType("test123" ); book.setName("test123" ); book.setDescription("123test" ); bookDao.updateById(book); } }
注意这个测试类的位置:
② 查看MP运行日志 SpringBoot整合MP的时候充分考虑到了这点,通过配置的形式就可以查阅执行期SQL语句,配置如下:
1 2 3 4 5 6 7 8 9 10 11 mybatis-plus: global-config: db-config: table-prefix: tbl_ id-type: auto configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
再来看运行结果,此时就显示了运行期执行SQL的情况。
③ 分页功能 步骤① :定义MP拦截器,设置具体的分页拦截器,并将其设置为Spring管控的bean
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class MPConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return interceptor; } }
上述代码第一行是创建MP的拦截器栈,这个时候拦截器栈中没有具体的拦截器,第二行是初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦截器,按照第二行的格式继续add进去新的拦截器就可以了。
步骤② :编写测试类
1 2 3 4 5 6 7 8 9 10 @Test void testPage () { IPage<Book> page = new Page <Book>(1 , 5 ); page = bookDao.selectPage(page, null ); System.out.println(page.getCurrent()); System.out.println(page.getSize()); System.out.println(page.getTotal()); System.out.println(page.getPages()); System.out.println(page.getRecords()); }
其中selectPage方法需要传入一个封装分页数据的对象,可以通过new的形式创建这个对象,当然这个对象也是MP提供的。创建此对象时就需要指定分页的两个基本数据:
可以通过创建Page对象时利用构造方法初始化这两个数据:
1 IPage page = new Page (2 ,5 );
将该对象传入到查询方法selectPage后,可以得到查询结果,但是我们会发现当前操作查询结果返回值仍然是一个IPage对象,这又是怎么回事?
1 IPage page = bookDao.selectPage(page, null );
原来这个IPage对象中封装了若干个数据,而查询的结果作为IPage对象封装的一个数据存在的,可以理解为查询结果得到后,又塞到了这个IPage对象中 ,其实还是为了高度的封装,一个IPage描述了分页所有的信息。
执行结果
④ 条件查询功能 除了分页功能,MP还提供有强大的条件查询功能。以往我们写条件查询要自己动态拼写复杂的SQL语句,现在简单了,MP将这些操作都制作成API接口,调用一个又一个的方法就可以实现各种套件的拼装。这里给大家普及一下基本格式,详细的操作还是到MP的课程中查阅吧。
下面的操作就是执行一个模糊匹配对应的操作,由like条件书写变为了like方法的调用:
1 2 3 4 5 6 7 8 @Test void testGetBy () { QueryWrapper<Book> queryWrapper = new QueryWrapper <Book>(); queryWrapper.like("name" , "Spring" ); bookDao.selectList(queryWrapper); }
其中QueryWrapper
对象是一个用于封装查询条件的对象 ,该对象可以动态使用API调用的方法添加条件,最终转化成对应的SQL语句。第二句就是一个条件了,需要什么条件,使用QueryWapper对象直接调用对应操作即可。比如做大于小于关系,就可以使用lt
或gt
方法,等于使用eq
方法,等等。
执行结果
这组API使用还是比较简单的,但是关于属性字段名的书写存在着安全隐患,比如查询字段name,当前是以字符串的形态书写的,万一写错,编译器还没有办法发现,只能将问题抛到运行器通过异常堆栈告诉开发者,不太友好。
MP针对字段检查进行了功能升级,全面支持Lambda
表达式,就有了下面这组API。由QueryWrapper对象升级为LambdaQueryWrapper
对象(推荐使用)。
1 2 3 4 5 6 7 8 @Test void testGetBy2 () { String name = "1" ; LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper <Book>(); lqw.like(Book::getName,name); bookDao.selectList(lqw); }
此外,为了便于开发者动态拼写SQL,防止将null数据作为条件使用,MP还提供了动态拼装SQL的快捷书写方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test void testGetBy2 () { String name = null ; LambdaQueryWrapper<Book> queryWrapper = new LambdaQueryWrapper <Book>(); queryWrapper.like(name != null ,Book::getName, name); bookDao.selectList(queryWrapper); }
1.5.5 业务层开发 ① 基本步骤 数据层开发告一段落,下面进行业务层开发,其实标准业务层开发很多初学者认为就是调用数据层,怎么说呢?这个理解是没有大问题的,更精准的说法应该是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用 。有什么差别呢?目标是为了组织出符合需求的业务逻辑功能,至于调不调用数据层还真不好说,有需求就调用,没有需求就不调用。
一个常识性的知识普及一下,业务层的方法名定义一定要与业务有关 ,例如登录操作
1 login(String username,String password);
而数据层的方法名定义一定与业务无关 ,是一定,不是可能,也不是有可能,例如根据用户名密码查询:
1 selectByUserNameAndPassword(String username,String password);
我们在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。比如一个哥们制作数据层,他就可以不知道业务是什么样子,拿到的需求文档要求可能是这样的:
1 2 3 接口:传入用户名与密码字段,查询出对应结果,结果是单条数据 接口:传入ID字段,查询出对应结果,结果是单条数据 接口:传入离职字段,查询出对应结果,结果是多条数据
但是进行业务功能开发的哥们,拿到的需求文档要求差别就很大:
1 接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,8到24位,对XXX字段做特殊字符校验,不允许存在空格,查询结果为对象。如果为null,返回BusinessException,封装消息码INFO_LOGON_USERNAME_PASSWORD_ERROR
1 2 3 4 5 6 7 8 public interface BookService { Boolean save (Book book) ; Boolean update (Book book) ; Boolean delete (Integer id) ; Book getById (Integer id) ; List<Book> getAll () ; IPage<Book> getPage (int currentPage, int pageSize) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override public Boolean save (Book book) { return bookDao.insert(book) > 0 ; } @Override public Boolean update (Book book) { return bookDao.updateById(book) > 0 ; } @Override public Boolean delete (Integer id) { return bookDao.deleteById(id) > 0 ; } @Override public Book getById (Integer id) { return bookDao.selectById(id); } @Override public List<Book> getAll () { return bookDao.selectList(null ); } @Override public IPage<Book> getPage (int currentPage, int pageSize) { IPage<Book> page = new Page <>(currentPage, pageSize); return bookDao.selectPage(page, null ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @SpringBootTest public class BookServiceTest { @Autowired private BookService bookService; @Test void testGetById () { System.out.println(bookService.getById(4 )); } @Test void testGetPage () { IPage<Book> page = bookService.getPage(2 , 5 ); System.out.println(page.getCurrent()); System.out.println(page.getSize()); System.out.println(page.getTotal()); System.out.println(page.getPages()); System.out.println(page.getRecords()); } }
② 业务层快速开发 其实MP技术不仅提供了数据层快速开发方案,业务层MP也给了一个通用接口,个人观点不推荐使用,凑合能用吧,其实就是一个封装+继承的思想,代码给出,实际开发慎用。
1 2 3 public interface IBookService extends IService <Book> { }
业务层接口实现类快速开发,关注继承的类需要传入两个泛型,一个是数据层接口,另一个是实体类
1 2 3 4 5 6 @Service public class BookServiceImpl extends ServiceImpl <BookDao, Book> implements IBookService { @Autowired private BookDao bookDao; }
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest public class BookServiceTest { @Autowired private IBookService bookService; @Test void testGetById () { System.out.println(bookService.getById(5 )); } }
总结
使用通用接口(ISerivce<T>
)快速开发Service
使用通用实现类(ServiceImpl<M,T>
)快速开发ServiceImpl
可以在通用接口基础上做功能重载或功能追加
注意重载时不要覆盖原始操作,避免原始提供的功能丢失
1.5.6 表现层开发 ① 基本开发和测试 表现层开发使用Rest风格,利用Postman进行接口测试。
表现层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @RestController @RequestMapping("/books") public class BookController { @Autowired private IBookService bookService; @GetMapping public List<Book> getAll () { return bookService.list(); } @PostMapping public Boolean save (@RequestBody Book book) { return bookService.save(book); } @PutMapping public Boolean update (@RequestBody Book book) { return bookService.modify(book); } @DeleteMapping("{id}") public Boolean delete (@PathVariable Integer id) { return bookService.removeById(id); } @GetMapping("{id}") public Book getById (@PathVariable Integer id) { return bookService.getById(id); } @GetMapping("{currentPage}/{pageSize}") public IPage<Book> getPage (@PathVariable int currentPage,@PathVariable int pageSize) { return bookService.getPage(currentPage, pageSize); } }
业务层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class BookServiceImpl extends ServiceImpl <BookDao, Book> implements IBookService { @Autowired private BookDao bookDao; @Override public Boolean modify (Book book) { return bookDao.updateById(book) > 0 ; } @Override public IPage<Book> getPage (int currentPage, int pageSize) { IPage<Book> page = new Page <>(currentPage, pageSize); bookDao.selectPage(page, null ); return page; } }
测试
只测试了分页功能:
返回消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { "records" : [ { "id" : 5 , "type" : "计算机理论" , "name" : "轻量级Java Web企业应用实战" , "description" : "源码级剖析Spring框架,适合已掌握Java基础的读者" } , { "id" : 6 , "type" : "计算机理论" , "name" : "Java核心技术 卷I 基础知识(原书第11版)" , "description" : "Core Java 第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新" } , { "id" : 7 , "type" : "计算机理论" , "name" : "深入理解Java虚拟机" , "description" : "5个维度全面剖析JVM,大厂面试知识点全覆盖" } , { "id" : 8 , "type" : "计算机理论" , "name" : "Java编程思想(第4版)" , "description" : "Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉" } ] , "total" : 14 , "size" : 4 , "current" : 2 , "orders" : [ ] , "optimizeCountSql" : true , "searchCount" : true , "countId" : null , "maxLimit" : null , "pages" : 4 }
小结
基于Restful制作表现层接口
新增:POST
删除:DELETE
修改:PUT
查询:GET
接收参数
实体数据:@RequestBody
路径变量:@PathVariable
② 消息一致性处理 目前我们通过Postman测试后业务层接口功能时通的,但是这样的结果给到前端开发者会出现一个小问题。不同的操作结果所展示的数据格式差异化严重:
增删改操作结果
查询单个数据操作结果
1 2 3 4 5 6 { "id" : 1 , "type" : "计算机理论" , "name" : "Spring实战 第5版" , "description" : "Spring入门经典教程" }
查询全部数据操作结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [ { "id" : 1 , "type" : "计算机理论" , "name" : "Spring实战 第5版" , "description" : "Spring入门经典教程" } , { "id" : 2 , "type" : "计算机理论" , "name" : "Spring 5核心原理与30个类手写实战" , "description" : "十年沉淀之作" } ]
每种不同操作返回的数据格式都不一样,而且还不知道以后还会有什么格式,这样的结果让前端人员看了是很容易让人崩溃的,必须将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一 ,也称为前后端数据协议
表现层代码改造
在controller包下新建一个包utils
,并新建类R
,用于封装后端向前端返回的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data public class R { private Boolean flag; private Object data; public R () { } public R (Boolean flag) { this .flag = flag; } public R (Boolean flag, Object data) { this .flag = flag; this .data = data; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @RestController @RequestMapping("/books") public class BookController { @Autowired private IBookService bookService; @GetMapping public R getAll () { return new R (true , bookService.list()); } @PostMapping public R save (@RequestBody Book book) { return new R (bookService.save(book)); } @PutMapping public R update (@RequestBody Book book) { return new R (bookService.modify(book)); } @DeleteMapping("{id}") public R delete (@PathVariable Integer id) { return new R (bookService.removeById(id)); } @GetMapping("{id}") public R getById (@PathVariable Integer id) { return new R (true , bookService.getById(id)); } @GetMapping("{currentPage}/{pageSize}") public R getPage (@PathVariable int currentPage,@PathVariable int pageSize) { return new R (true , bookService.getPage(currentPage, pageSize)); } }
小结
设计统一的返回值结果类型便于前端开发读取数据
返回值结果类型可以根据需求自行设定,没有固定格式
返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议
1.5.7 前后端联通性测试 单体应用中,前端页面及其配置被放在resources
下的static
中:
在进行具体的功能开发之前,先做联通性的测试,通过页面发送异步提交(axios),这一步调试通过后再进行进一步的功能开发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 created ( ) { this .getAll (); }, methods : { getAll ( ) { axios.get ("/books" ).then ((res ) => { console .log (res.data ); }); }, }
如果出现跨域请求错误,在controller类上添加注解:
1 @CrossOrigin(origins = "*", maxAge = 3600)
执行结果
总结
单体项目中页面放置在resources/static目录下
created钩子函数用于初始化页面时发起调用
页面使用axios发送异步请求获取数据后确认前后端是否联通
1.5.8 页面基础功能开发 ① 列表功能(除分页) 列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可。
页面数据模型定义
1 2 3 4 data:{ dataList: [],//当前页要展示的列表数据 ... },
异步请求获取数据
1 2 3 4 5 6 7 8 getAll ( ) { axios.get ("/books" ).then ((res ) => { console .log (res.data ); this .dataList = res.data .data ; }); }
这样在页面加载时就可以获取到数据,并且由VUE将数据展示到页面上了:
② 添加功能 添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可。
默认状态
1 2 3 4 data :{ dialogFormVisible : false , ... },
切换为显示状态
1 2 3 4 5 6 7 handleCreate ( ) { this .dialogFormVisible = true ; this .resetForm (); },
由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹。
定义清理数据操作
1 2 3 4 5 resetForm ( ) { this .formData = {}; },
添加操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 handleAdd () { axios.post ("/books" , this .formData ).then ((res ) => { if (res.data .flag ) { this .dialogFormVisible = false ; this .$message .success ("添加成功" ); } else { this .$message .error ("添加失败" ); } }).finally (() => { this .getAll (); }); },
将要保存的数据传递到后台,通过post
请求的第二个参数传递json数据到后台
根据返回的操作结果决定下一步操作
如何是true就关闭添加窗口,显示添加成功的消息
如果是false保留添加窗口,显示添加失败的消息
无论添加是否成功,页面均进行刷新,动态加载数据(对getAll操作发起调用)
取消添加操作
1 2 3 4 5 cancel ( ){ this .dialogFormVisible = false ; this .$message .info ("当前操作取消" ); },
总结
请求方式使用POST调用后台对应操作
添加操作结束后动态刷新页面加载数据
根据操作结果不同,显示对应的提示信息
弹出添加Div时清除表单数据
③ 删除功能 模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据id到后台即可。
注意删除操作要添加提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 handleDelete (row ) { this .$confirm("是否继续删除?" , "提示" , {type : "info" }).then (() => { axios.delete ("/books/" + row.id ).then ((res ) => { if (res.data .flag ) { this .$message .success ("删除成功" ); } else { this .$message .error ("删除失败" ); } }).finally (() => { this .getAll (); }); }).catch (() => { this .$message .info ("取消操作" ); }); },
④ 修改功能 修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:
页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据
查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据
修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据
所以整体上来看,修改功能就是前面几个功能的大合体
查询并展示数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 handleUpdate (row ) { axios.get ("/books/" + row.id ).then ((res ) => { if (res.data .flag && res.data .data != null ) { this .dialogFormVisible4Edit = true ; this .formData = res.data .data ; } else { this .$message .error ("数据已不存在" ); } }).finally (() => { this .getAll (); }); },
修改操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 handleEdit ( ) { axios.put ("/books" , this .formData ).then ((res ) => { if (res.data .flag ) { this .dialogFormVisible4Edit = false ; this .$message .success ("修改成功" ); } else { this .$message .error ("修改失败" ); } }).finally (() => { this .getAll (); }); },
取消操作的提示
1 2 3 4 5 6 cancel ( ){ this .dialogFormVisible = false ; this .dialogFormVisible4Edit = false ; this .$message .info ("当前操作取消" ); },
1.5.9 业务消息一致性处理 目前的功能制作基本上达成了正常使用的情况,什么叫正常使用呢?也就是这个程序不出BUG,如果我们搞一个BUG出来,你会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子:
1 2 3 4 5 6 7 @GetMapping("{id}") public R getById (@PathVariable Integer id) throws IOException { if (true ) { throw new IOException (); } return new R (true , bookService.getById(id)); }
利用postman测试该接口,获取到的结果:
1 2 3 4 5 6 { "timestamp" : "2021-09-15T03:27:31.038+00:00" , "status" : 500 , "error" : "Internal Server Error" , "path" : "/books/1" }
面对这种情况,前端的同学又不会了,这又是什么格式?怎么和之前的格式不一样?
1 2 3 4 5 6 7 8 9 { "flag" : true , "data" : { "id" : 1 , "type" : "计算机理论" , "name" : "Spring实战 第5版" , "description" : "Spring入门经典教程" } }
看来不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理
首先在当前的数据结果中添加消息字段,用来兼容后台出现的操作消息
1 2 3 4 5 6 7 8 @Data public class R { private Boolean flag; private Object data; private String msg; }
然后在表现层做统一的异常处理,使用SpringMVC提供的异常处理器做统一的异常处理。在utils包下新建类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler public R doException (Exception e) { e.printStackTrace(); return new R ("服务器故障,请稍后再试!" ); } }
页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果false,就读取后台传递的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 handleAdd () { axios.post ("/books" , this .formData ).then ((res ) => { if (res.data .flag ) { this .dialogFormVisible = false ; this .$message .success ("添加成功" ); } else { this .$message .error (res.data .msg ); } }).finally (() => { this .getAll (); }); },
测试结果:
1 2 3 4 5 { "flag" : false , "data" : null , "msg" : "服务器故障,请稍后再试!" }
1.5.10 页面其他功能开发 ① 分页功能 分页功能的制作用于替换前面的查询全部,其中要使用到elementUI提供的分页组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!--分页组件--> <div class ="pagination-container" > <el-pagination class ="pagiantion" <!--处理分页的方法-- > @current-change="handleCurrentChange" :current-page="pagination.currentPage" :page-size="pagination.pageSize" layout="total, prev, pager, next, jumper" :total="pagination.total"> </el-pagination > </div >
为了配合分页组件,封装分页对应的数据模型:
1 2 3 4 5 6 7 8 data :{ pagination : { currentPage : 1 , pageSize :10 , total :0 , } },
步骤① :修改查询全部功能为分页查询,通过路径变量传递页码信息参数,页面根据分页操作结果读取对应数据,并进行数据模型绑定:
1 2 3 4 5 6 7 8 9 10 getAll ( ) { axios.get ("/books/" + this .pagination .currentPage + "/" + this .pagination .pageSize ).then ((res ) => { this .pagination .total = res.data .data .total ; this .dataList = res.data .data .records ; }); },
步骤② :对切换页码操作设置调用当前分页操作
1 2 3 4 5 6 7 handleCurrentChange (currentPage ) { this .pagination .currentPage = currentPage; this .getAll (); },
删除功能的维护
由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询 。其实这个问题解决方案很多,这里给出比较简单的一种处理方案。
1 2 3 4 5 6 7 8 9 @GetMapping("{currentPage}/{pageSize}") public R getPage (@PathVariable int currentPage,@PathVariable int pageSize) { IPage<Book> page = bookService.getPage(currentPage, pageSize); if (currentPage > page.getPages()) { page = bookService.getPage((int ) page.getPages(), pageSize); } return new R (true , page); }
② 条件查询 条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了:
页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递2个分页相关的数据转换成2个分页数据加若干个条件
后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是null,现在换成具体条件,差别不大
查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略
经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别。
页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装url参数的形式传递。
页面封装查询条件字段
1 2 3 4 5 6 7 8 pagination : { currentPage : 1 , pageSize :10 , total :0 , type : "" , name : "" , description : "" }
页面添加查询条件字段对应的数据模型绑定名称:
1 2 3 4 5 6 7 <div class ="filter-container" > <el-input placeholder ="图书类别" v-model ="pagination.type" style ="width: 200px;" class ="filter-item" > </el-input > <el-input placeholder ="图书名称" v-model ="pagination.name" style ="width: 200px;" class ="filter-item" > </el-input > <el-input placeholder ="图书描述" v-model ="pagination.description" style ="width: 200px;" class ="filter-item" > </el-input > <el-button @click ="getAll()" class ="dalfBut" > 查询</el-button > <el-button type ="primary" class ="butT" @click ="handleCreate()" > 新建</el-button > </div >
将查询条件组织成url参数,添加到请求url地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 getAll ( ) { param = "?type=" + this .pagination .type ; param += "&name=" + this .pagination .name ; param += "&description=" + this .pagination .description ; axios.get ("/books/" + this .pagination .currentPage + "/" + this .pagination .pageSize + param).then ((res ) => { this .pagination .total = res.data .data .total ; this .dataList = res.data .data .records ; }); },
后台代码中定义实体类封查询条件:
1 2 3 4 5 6 7 8 9 @GetMapping("{currentPage}/{pageSize}") public R getPage (@PathVariable int currentPage, @PathVariable int pageSize, Book book) { IPage<Book> page = bookService.getPage(currentPage, pageSize, book); if (currentPage > page.getPages()) { page = bookService.getPage((int ) page.getPages(), pageSize, book); } return new R (true , page); }
对应业务层接口与实现类进行修正:
1 IPage<Book> getPage (int currentPage, int pageSize, Book book) ;
1 2 3 4 5 6 7 8 9 10 @Override public IPage<Book> getPage (int currentPage, int pageSize, Book book) { LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper <Book>(); lqw.like(Strings.isNotEmpty(book.getType()), Book::getType, book.getType()); lqw.like(Strings.isNotEmpty(book.getName()), Book::getName, book.getName()); lqw.like(Strings.isNotEmpty(book.getDescription()), Book::getDescription, book.getDescription()); IPage<Book> page = new Page <>(currentPage, pageSize); bookDao.selectPage(page, lqw); return page; }
2 运维篇 2.1 SpringBoot程序打包与运行 2.1.1 打包和运行指令 企业项目上线为了保障环境适配性会采用下面流程发布项目,这里不讨论此过程。
开发部门使用Git、SVN等版本控制工具上传工程到版本服务器
服务器使用版本控制工具下载工程
服务器上使用Maven工具在当前真机环境下重新构建项目
启动服务
继续说我们的打包和运行过程。所谓打包指将程序转换成一个可执行的文件(例如jar文件),所谓运行指不依赖开发环境执行打包产生的文件。上述两个操作都有对应的命令可以快速执行。
程序打包
SpringBoot程序是基于Maven创建的,在Maven中提供有打包的指令,叫做package。本操作可以在Idea环境下执行。
打包后会产生一个与工程名类似的jar文件,其名称是由模块名+版本号+.jar组成的。
打包好的文件:注意jar文件大小为30Mb 左右
程序运行
程序包打好以后,就可以直接执行了。在程序包所在路径下,执行指令。
执行程序打包指令后,程序正常运行,与在Idea下执行程序没有区别。
注意:在使用向导创建SpringBoot工程时,pom.xml文件中会有如下配置(maven的插件),这一段配置千万不能删除,否则打包后无法正常执行程序。这组配置决定了打包出来的程序包是否可以执行。
1 2 3 4 5 6 7 8 <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build >
Linux下的运行命令
1 nohup java -jar XXX.jar > XXX.log 2>&1 &
1 ps -ef | grep "java -jar"
2.1.2 打包失败的处理 当注释掉上一节的maven插件再进行打包并运行时,发现报错:
查看打包好的jar文件,发现大小只有1Mb左右:
此外,观察两次打包后的程序包的差别,共有3处比较明显的特征:
打包后文件的大小不同
打包后所包含的内容不同
打包程序中个别文件内容不同
内容也完全不一样,仅有一个目录是一样的,叫做META-INF。打开容量大的程序包中的BOOT-INF目录下的classes目录,我们发现其中的内容居然和容量小的程序包中的内容完全一样。
原来大的程序包中除了包含小的程序包中的所有内容,还有别的东西。都有什么呢?回到BOOT-INF目录下,打开lib
目录,里面显示了很多个jar文件。
仔细翻阅不难发现,这些jar文件都是我们制作这个工程时导入的坐标对应的文件。大概可以想明白了,SpringBoot程序为了让自己打包生成的程序可以独立运行,不仅将项目中自己开发的内容进行了打包,还把当前工程运行需要使用的jar包全部打包进来了 。为什么这样做呢?就是为了可以独立运行 。不依赖程序包外部的任何资源可以独立运行当前程序。这也是为什么大的程序包容量是小的程序包容量的30倍的主要原因。
再看看大程序包还有什么不同之处,在最外层目录包含一个org目录,进入此目录,目录名是org\springframework\boot\loader
,在里面可以找到一个JarLauncher.class
的文件,先记得这个文件。再看这套目录名,明显是一个Spring的目录名,为什么要把Spring框架的东西打包到这个程序包中呢?不清楚。
回到两个程序包的最外层目录,查看名称相同的文件夹META-INF下都有一个叫做MANIFEST.MF
的文件,但是大小不同,打开文件,比较内容区别:
1 2 3 4 5 Manifest-Version: 1.0 Implementation-Title: springboot_08_ssmp Implementation-Version: 0.0.1-SNAPSHOT Build-Jdk-Spec: 1.8 Created-By: Maven Jar Plugin 3.2.0
1 2 3 4 5 6 7 8 9 10 11 12 Manifest-Version: 1.0 Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Implementation-Title: springboot_08_ssmp Implementation-Version: 0.0.1-SNAPSHOT Spring-Boot-Layers-Index: BOOT-INF/layers.idx Start-Class: com.itheima.SSMPApplication # 引导类 Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.5.4 Created-By: Maven Jar Plugin 3.2.0 Main-Class: org.springframework.boot.loader.JarLauncher # jar启动器
大文件中明显比小文件中多了几行信息,其中最后一行信息是Main-Class: org.springframework.boot.loader.JarLauncher
。这句话什么意思呢?如果使用java -jar
执行此程序包,将执行Main-Class属性配置的类,这个类恰巧就是前面看到的那个文件。原来SpringBoot打包程序中出现Spring框架的东西是为这里服务的。而这个org.springframework.boot.loader.JarLauncher
类内部要查找Start-Class属性中配置的类,并执行对应的类。这个属性在当前配置中也存在,对应的就是我们的引导类类名。
现在这组设定的作用就搞清楚了——要能是SpringBoot程序打包后能够独立运行
SpringBoot程序添加配置后会打出一个特殊的包,包含Spring框架部分功能,原始工程内容,原始工程依赖的jar包
首先读取MANIFEST.MF文件中的Main-Class属性,用来标记执行java -jar命令后运行的类
JarLauncher类执行时会找到Start-Class属性,也就是启动类类名
运行启动类时会运行当前工程的内容
运行当前工程时会使用依赖的jar包,从lib目录中查找
总之一句话:spring-boot-maven-plugin插件用于将当前程序打包成一个可以独立运行的程序包 。
2.1.3 端口占用及解决 在DOS环境下启动SpringBoot工程时,可能会遇到端口占用的问题。以下命令用于杀死占用某个端口的进程。
1 2 3 4 5 6 7 8 9 10 # 查询端口 netstat -ano # 查询指定端口 推荐 netstat -ano | findstr ":端口号" # 根据进程PID查询进程名称 tasklist |findstr "进程PID号" # 根据PID杀死任务 推荐 taskkill /F /PID "进程PID号" # 根据进程名称杀死任务 taskkill -f -t -im "进程名称"
示例——杀死运行在80端口的java进程
2.2 高级配置 2.2.1 临时属性配置 SpringBoot提供了灵活的配置方式,如果你发现你的项目中有个别属性需要重新配置,可以使用临时属性的方式快速修改某些配置。方法也特别简单,在启动的时候添加上对应参数就可以了。
例如,要求项目临时在80端口启动,则:
1 java –jar springboot.jar --server.port=80
如果要修改的属性不止一个,可以按照上述格式继续写,属性与属性之间使用空格分隔。
1 java –jar springboot.jar --server.port=80 --logging.level.root=debug
① 属性加载优先级 现在我们的程序配置受两个地方控制了,第一配置文件,第二临时属性。并且我们发现临时属性的加载优先级要高于配置文件的。那是否还有其他的配置方式呢?其实是有的,而且还不少,打开官方文档中对应的内容,就可以查看配置读取的优先顺序。
官网地址: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
② 开发环境中使用临时属性 临时使用目前是有了,但是上线的时候通过命令行输入的临时属性必须是正确的,那这些属性配置值我们必须在开发环境中测试好才行。下面说一下开发环境中如何使用临时属性,其实就是Idea界面下如何操作了。
打开SpringBoot引导类的运行界面,在里面找到配置项。其中Program arguments
对应的位置就是添加临时属性的,可以加几个试试效果。
注意这里的Program arguments
是传递给了主启动类的main方法的String[] args
形参,可以打印出来看一下:
1 2 3 4 5 6 7 8 9 @SpringBootApplication public class SsmpApplication { public static void main (String[] args) { System.out.println("args: " + Arrays.toString(args)); SpringApplication.run(SsmpApplication.class, args); } }
所以,这里如果不用这个args是不是就断开了外部传递临时属性的入口呢?是这样的,我们可以使用下面的调用方式,这样外部临时属性就无法进入到SpringBoot程序中了。
1 2 3 4 5 6 7 8 9 @SpringBootApplication public class SsmpApplication { public static void main (String[] args) { SpringApplication.run(SsmpApplication.class); } }
此外,还可以这样玩:(本质上args就是字符串数组)
1 2 3 4 5 6 public static void main (String[] args) { String[] arg = new String [1 ]; arg[0 ] = "--server.port=8082" ; SpringApplication.run(SSMPApplication.class, arg); }
2.2.2 配置文件分类 SpringBoot提供了配置文件和临时属性的方式来对程序进行配置。前面一直说的是临时属性,这一节要说说配置文件了。其实这个配置文件我们一直在使用,只不过我们用的是SpringBoot提供的4级配置文件中的其中一个级别。
4个级别分别是:
类路径下配置文件(一直使用的是这个,也就是resources目录中的application.yml文件)classpath:application.yml
。
类路径下config目录下配置文件classpath:config/application.yml
程序打包后所在目录中配置文件file:application.yml
程序打包后所在目录中config目录下配置文件file:config/application.yml
其优先级从低到高。后者与前者相同的内容,后者覆盖前者。即:多层级配置文件间的属性采用叠加并覆盖的形式作用于程序
示例
在类路径下新建文件夹config
,并新建配置文件application.yml
:
resource下的配置文件内容为:
启动项目:
可见前者覆盖了后者
2.2.3 自定义配置文件 之前做配置使用的配置文件都是application.yml,其实这个文件也是可以改名字的,这样方便维护。比如我2020年4月1日搞活动,走了一组配置,2020年5月1日活动取消,恢复原始配置,这个时候只需要重新更换一下配置文件就可以了。但是你总不能在原始配置文件上修改吧,不然搞完活动以后,活动的配置就留不下来了,不利于维护。
在resource下新建配置文件ebank.properties
或ebank.yml
。
自定义配置文件方式有如下两种:
方式① :使用临时属性设置Program arguments
配置文件名,注意仅仅是名称,不要带扩展名
1 --spring.congih.name=ebank
方式② :使用临时属性设置配置文件路径,这个是全路径名,此时要加扩展名
1 --spring.config.location=classpath:/ebank.properties
也可以设置加载多个配置文件
1 --spring.config.location=classpath:/ebank.properties,classpath:/ebank.yml
注意
我们现在研究的都是SpringBoot单体项目,就是单服务器版本。其实企业开发现在更多的是使用基于SpringCloud技术的多服务器项目。这种配置方式和我们现在学习的完全不一样,所有的服务器将不再设置自己的配置文件,而是通过配置中心 获取配置,动态加载配置信息 。为什么这样做?集中管理,便于维护。
2.3 多环境开发 什么是多环境?其实就是说你的电脑上写的程序最终要放到别人的服务器上去运行。每个计算机环境不一样,这就是多环境。常见的多环境开发主要兼顾3种环境设置,开发环境——自己用的,测试环境——自己公司用的,生产环境——甲方爸爸用的 。因为这是绝对不同的三台电脑,所以环境肯定有所不同,比如连接的数据库不一样,设置的访问端口不一样等等。
2.3.1 yml单一文件版 新建模块demo_profile
,并向配置文件写入内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spring: profiles: active: pro --- spring: profiles: pro server: port: 80 --- spring: profiles: dev server: port: 81 --- spring: config: activate: on-profile: test server: port: 82
其中测试的配置格式是推荐的,前两个的格式已经过时。
总结:
多环境开发需要设置若干种常用环境,例如开发、生产、测试环境
yaml格式中设置多环境使用---
区分环境设置边界
每种环境的区别在于加载的配置属性不同
启用某种环境时需要指定启动时使用该环境
2.3.2 yml多文件版 将所有的配置都放在一个配置文件中,尤其是每一个配置应用场景都不一样,这显然不合理,于是就有了将一个配置文件拆分成多个配置文件的想法。拆分后,每个配置文件中写自己的配置,主配置文件中写清楚用哪一个配置文件就好了。
将原来的配置文件备份,并新建四个配置文件,分别为主配置文件和三个不同环境的配置文件:
1 2 3 spring: profiles: active: pro
环境配置文件,例如application-dev.yml
环境配置文件因为每一个都是配置自己的项,所以连名字都不用写里面了。那问题是如何区分这是哪一组配置呢?使用文件名区分 。
文件的命名规则为:application-环境名.yml
总结:
主配置文件中设置公共配置(全局)
环境分类配置文件中常用于设置冲突属性(局部)
可以使用独立配置文件定义环境属性
独立配置文件便于线上系统维护更新并保障系统安全性
2.3.3 多环境开发独立配置文件书写技巧 将所有的配置根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下。
application-devDB.yml
application-devRedis.yml
application-devMVC.yml
使用
在主配置文件中,使用include
属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔。
1 2 3 4 spring: profiles: active: dev include: decMVC, devDB
注意:当主环境dev与其他环境有相同属性时,主环境属性生效;其他环境中有相同属性时,最后加载的环境属性生效 ,例如devDB会覆盖decMVC中相同的配置。
改进
但是上面的设置也有一个问题,比如我要切换dev环境为pro时,include也要修改。因为include属性只能使用一次,这就比较麻烦了。SpringBoot从2.4版开始使用group属性替代include属性,降低了配置书写量。
1 2 3 4 5 6 spring: profiles: active: dev group: "dev": devDB, devMVC "pro": proDB, proMVC
此时,只需修改dev为pro即可。
2.3.4 多环境开发控制 最后说一个冲突问题,就是maven和SpringBoot同时设置多环境的话怎么搞。
要想处理这个冲突问题,要先理清一个关系,究竟谁在多环境开发中其主导地位。也就是说如果现在都设置了多环境,谁的应该是保留下来的,另一个应该遵从相同的设置。
maven是做什么的?项目构建管理的,最终生成代码包的,SpringBoot是干什么的?简化开发的。简化,又不是其主导作用。最终还是要靠maven来管理整个工程,所以SpringBoot应该听maven的。整个确认后下面就好做了。大体思想如下:
先在maven环境中设置用什么具体的环境
在SpringBoot中读取maven设置的环境即可
maven中设置多环境(使用属性方式区分环境)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <profiles > <profile > <id > env_dev</id > <properties > <profile.active > dev</profile.active > </properties > <activation > <activeByDefault > true</activeByDefault > </activation > </profile > <profile > <id > env_pro</id > <properties > <profile.active > pro</profile.active > </properties > </profile > </profiles >
SpringBoot中读取maven设置值,指定环境
1 2 3 4 5 spring: profiles: active: @profile.active@
总结:
当Maven与SpringBoot同时对多环境进行控制时,以Mavn为主,SpringBoot使用@..@
占位符读取Maven对应的配置属性值
基于SpringBoot读取Maven配置属性的前提下,如果在Idea下测试工程时pom.xml每次更新需要手动compile方可生效
2.4 日志 2.4.1 日志框架
Log4j 2
:Apache Log4j 2 是一个功能强大的日志框架,支持异步日志和各种配置格式。
Logback
:这是 Spring Boot 默认的日志框架,由 Log4j 的作者开发,性能优越且配置灵活。
SLF4J (Simple Logging Facade for Java)
:这是一个日志门面框架,它本身并不负责日志记录,而是将日志调用转发给具体的日志实现。SLF4J 常用于将不同的日志框架(如 Logback
或 Log4j 2
)结合在一起使用。
2.4.2 SLF4J SLF4J
日志的使用格式非常固定,直接上操作步骤:
步骤① :添加日志记录操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/books") public class BookController { private static final Logger log = LoggerFactory.getLogger(BookController.class); @GetMapping public String getById () { System.out.println("SpringBoot is running...2" ); log.debug("debug..." ); log.info("info..." ); log.warn("warn..." ); log.error("error..." ); return "SpringBoot is running...2" ; } }
上述代码中log对象就是用来记录日志的对象,下面的log.debug
,log.info
这些操作就是写日志的API了。
步骤② :设置日志输出级别
日志设置好以后可以根据设置选择哪些参与记录。这里是根据日志的级别来设置的。日志的级别分为6种,分别是:
TRACE
:运行堆栈信息,使用率低
DEBUG
:程序员调试代码使用
INFO
:记录运维过程数据
WARN
:记录运维过程报警数据
ERROR
:记录错误堆栈信息
FATAL
:灾难信息,合并计入ERROR
一般情况下,开发时候使用DEBUG,上线后使用INFO,运维信息记录使用WARN即可。下面就设置一下日志级别:
这么设置太简单粗暴了,日志系统通常都提供了细粒度的控制
1 2 3 4 5 6 7 debug: true logging: level: root: debug
还可以再设置更细粒度的控制
步骤③ :设置日志组,控制指定包对应的日志输出级别,也可以直接控制指定包对应的日志输出级别
1 2 3 4 5 6 7 8 9 10 11 logging: group: ebank: com.itheima.controller level: root: warn ebank: debug com.itheima.controller: debug
优化
写代码的时候每个类都要写创建日志记录对象,这个可以优化一下,使用前面用过的lombok
技术给我们提供的工具类即可。
1 2 3 4 5 6 @Slf4j @RestController @RequestMapping("/books") public class BookController extends BaseClass { }
2.4.2 日志输出格式控制
PID:进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值可用于协助程序员调试程序
所属类/接口名:当前显示信息为SpringBoot重写后的信息,名称过长时,简化包名书写为首字母,甚至直接删除
对于单条日志信息来说,日期,触发位置,记录信息是最核心的信息。级别用于做筛选过滤,PID与线程名用于做精准分析。了解这些信息后就可以DIY日志格式了。
下面给出课程中模拟的官方日志模板的书写格式,便于大家学习。
设置日志输出格式
1 2 3 logging: pattern: console: "%d - %m%n"
其中:
官方日志格式
1 2 3 logging: pattern: console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"
%p
: 日志级别
%clr(...){...}
: 彩色标注,前面是标注的内容,后面是颜色类型,例如cyan青色
%t
:线程名
%c
:类名
2.4.3 日志文件——与Logback结合使用 对于日志文件的使用存在各种各样的策略,例如每日记录,分类记录,报警后记录等。这里主要研究日志文件如何记录。
① yml格式 记录日志到文件中格式非常简单,设置日志文件名即可。
1 2 3 logging: file: name: server.log
生成的日志文件注意是在项目上一层文件夹中:
虽然使用上述格式可以将日志记录下来了,但是面对线上的复杂情况,一个文件记录肯定是不能够满足运维要求的,通常会每天记录日志文件,同时为了便于维护,还要限制每个日志文件的大小。下面给出日志文件的常用配置方式:
1 2 3 4 5 logging: logback: rollingpolicy: max-file-size: 3KB file-name-pattern: server.%d{yyyy-MM-dd}.%i.log
以上格式是基于logback
日志技术设置每日日志文件的设置格式,要求容量到达3KB以后就转存信息到第二个文件中。
文件命名规则中的%d
标识日期,%i
是一个递增变量,用于区分日志文件。
② xml设置 在根目录下新建logback.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <property name ="log.path" value ="/home/airport/logs" /> <property name ="log.pattern" value ="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" /> <appender name ="console" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > ${log.pattern}</pattern > </encoder > </appender > <appender name ="file_info" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${log.path}/sys-info.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > ${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern > <maxHistory > 60</maxHistory > </rollingPolicy > <encoder > <pattern > ${log.pattern}</pattern > </encoder > <filter class ="ch.qos.logback.classic.filter.LevelFilter" > <level > INFO</level > <onMatch > ACCEPT</onMatch > <onMismatch > DENY</onMismatch > </filter > </appender > <appender name ="file_error" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${log.path}/sys-error.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > ${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern > <maxHistory > 60</maxHistory > </rollingPolicy > <encoder > <pattern > ${log.pattern}</pattern > </encoder > <filter class ="ch.qos.logback.classic.filter.LevelFilter" > <level > ERROR</level > <onMatch > ACCEPT</onMatch > <onMismatch > DENY</onMismatch > </filter > </appender > <appender name ="sys-user" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${log.path}/sys-user.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > ${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern > <maxHistory > 60</maxHistory > </rollingPolicy > <encoder > <pattern > ${log.pattern}</pattern > </encoder > </appender > <logger name ="com.airport" level ="info" /> <logger name ="org.springframework" level ="warn" /> <root level ="info" > <appender-ref ref ="console" /> </root > <root level ="info" > <appender-ref ref ="file_info" /> <appender-ref ref ="file_error" /> </root > <logger name ="sys-user" level ="info" > <appender-ref ref ="sys-user" /> </logger > </configuration >
3 开发篇 3.1 热部署 3.1.1 手动启动热部署
步骤
步骤①:在pom文件中引入开发者工具的坐标:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > </dependency >
步骤②:激活热部署ctrl + F9
关于热部署
重启restart
:自定义开发代码,包含类、页面、配置文件等,加载位置restart类加载器
重载reload
:jar包,加载位置base类加载器,例如maven的jar包
热部署仅包含重启过程,仅仅加载当前开发者自定义开发的资源,不加载maven的jar资源。
3.1.2 自动启动热部署
步骤
激活方式:Idea失去焦点5s后启动热部署
3.1.3 热部署范围配置 默认不触发重启的目录列表:
/META-INF/maven
/META-INF/resources
/resources
/static
/public
/templates
自定义不参与重启排除项,在application配置文件中:
1 2 3 4 5 6 7 8 9 10 11 12 spring: datasource: druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: 12345678 devtools: restart: exclude: static/**, public/**, config/application.yml
3.1.4 关闭热部署
1 2 3 4 5 6 spring: devtools: restart: exclude: static/**, public/**, config/application.yml enabled: false
参加下图,方式一是图中第三个方法来进行配置,方式二是图中第六个方法来进行配置,优先级高于方式一。
配置代码在主启动类中:
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication public class SsmpApplication { public static void main (String[] args) { System.setProperty("spring.devtools.restart.enabled" , "false" ); SpringApplication.run(SsmpApplication.class, args); } }
3.2 配置高级 3.2.1 @ConfigurationProperties
回顾1.3.4.(5)小节
自定义属性注入配置。
新建模块demo_config
,并新建配置类ServerConfig.java
:
1 2 3 4 5 6 7 8 @Data @Component @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; }
1 2 3 4 servers: ipAddress: 192.168 .0 .1 port: 2345 timeout: -1
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication public class DemoConfigApplication { public static void main (String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(DemoConfigApplication.class, args); ServerConfig bean = ctx.getBean(ServerConfig.class); System.out.println(bean); } }
执行结果:
1 ServerConfig(ipAddress=192.168.0.1, port=2345, timeout=-1)
接下来使用@ConfigurationProperties
为第三方bean绑定属性
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.16</version > </dependency >
1 2 datasource: driverClassName: com.mysql.jdbc.Driver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication public class DemoConfigApplication { @Bean @ConfigurationProperties(prefix = "datasource") public DruidDataSource dataSource () { DruidDataSource ds = new DruidDataSource (); return ds; } public static void main (String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(DemoConfigApplication.class, args); DruidDataSource ds = ctx.getBean(DruidDataSource.class); System.out.println(ds); System.out.println(ds.getDriverClassName()); } }
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 { CreateTime:"2022-02-22 17:10:29", ActiveCount:0, PoolingCount:0, CreateCount:0, DestroyCount:0, CloseCount:0, ConnectCount:0, Connections:[ ] } com.mysql.jdbc.Driver
@EnableConfigurationProperties
注解
@EnableConfigurationProperties
注解可以将使用@ConfigurationProperties
注解对应的类加入到Spring容器中。在启动类上书写。
注意:@ConfigurationProperties
和@Component
不能同时使用,否则容器会报存在两个相同bean的错误。
例如:
1 2 3 4 5 6 7 8 @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; }
1 2 3 4 5 @SpringBootApplication @EnableConfigurationProperties(ServerConfig.class) public class DemoConfigApplication { }
解除使用@ConfigurationProperties
注释警告
方法:添加依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > </dependency >
3.2.2 宽松绑定和松散绑定
绑定前缀名 命名规范:仅能使用纯小写字母、数字、下划线作为合法的字符
1 2 3 4 5 datasource: driverClassName: com.mysql.jdbc.Driver dataSource: driverClassName: com.mysql.jdbc.Driver
于是在读取配置类中,会报错:
@ConfigurationProperties
绑定属性支持属性名 宽松绑定,即配置类中的属性和配置文件中的属性名不一定要一一对应,但有多种推荐的命名模式
1 2 3 4 5 6 7 8 9 servers: ip-address: 192.168 .0 .1 port: 2345 timeout: -1
官方推荐中划线模式 书写属性名。
在配置类中也可以任意使用各种模式书写属性名,但是推荐使用驼峰模式。
1 2 3 4 5 6 7 8 @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; }
1 2 servers: ip-address: 192.168 .0 .1
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest class DemoConfigApplicationTests { @Value("${servers.ipaddress}") private String s; @Test void contextLoads () { System.out.println(s); } }
3.2.3 常用计量单位绑定 SpringBoot支持JDK8提供的时间与空间计量单位。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; @DurationUnit(ChronoUnit.HOURS) private Duration serverTimeOut; @DataSizeUnit(DataUnit.MEGABYTES) private DataSize dataSize; }
1 2 3 4 5 6 7 servers: ip-address: 192.168 .0 .1 port: 2345 timeout: -1 server-time-out: 3 data-size: 10
执行结果:
1 ServerConfig(ipAddress=192.168.0.1, port=2345, timeout=-1, serverTimeOut=PT3H, dataSize=10485760B)
其中10485760B = 1024 * 1024 * 10B
3.2.4 数据校验 开启数据校验有助于系统安全性,J2EE规范中JSR303规范定义了一组关于数据校验的API。
1 2 3 4 5 <dependency > <groupId > javax.validation</groupId > <artifactId > validation-api</artifactId > <version > 2.0.1.Final</version > </dependency >
开启bean的校验功能,对配置类使用@Validated
注解
在字段上设置校验规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data @ConfigurationProperties(prefix = "servers") @Validated public class ServerConfig { private String ipAddress; @Max(value = 8888, message = "端口最大值不能大于8888") @Min(value = 202, message = "端口最小值不能小于222") @NotEmpty(message = "端口不能为空") private int port; private long timeout; @DurationUnit(ChronoUnit.HOURS) private Duration serverTimeOut; @DataSizeUnit(DataUnit.MEGABYTES) private DataSize dataSize; }
执行报错:
1 2 3 4 5 6 7 8 9 10 11 *************************** APPLICATION FAILED TO START *************************** Description: The Bean Validation API is on the classpath but no implementation could be found Action: Add an implementation, such as Hibernate Validator, to the classpath
发现需要一个接口Hibernate Validator
的实现,于是添加依赖:
1 2 3 4 5 6 <dependency > <groupId > org.hibernate.validator</groupId > <artifactId > hibernate-validator</artifactId > <version > 7.0.1.Final</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 *************************** APPLICATION FAILED TO START *************************** Description: Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'servers' to com.hongyi.config.ServerConfig failed: Property: servers.port Value: 1 Origin: class path resource [application.yml] - 3:9 Reason: 端口最小值不能小于222 Action: Update your application's configuration Process finished with exit code 1
发现校验不通过,并打印了提示信息
3.3 测试 3.3.1 加载测试专用属性 新建模块demo_test
,删除原来的测试类,新增测试类PropertiesAndArgsTest.java
启动测试环境时,可以通过properties
或args
属性为当前测试用例添加临时的属性配置,且都可以覆盖配置文件中的属性
优点:比多环境开发中的测试环境影响范围更小,仅对当前测试类有效
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SpringBootTest(args = {"--test.prop=testValue2"}) public class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void testProperties () { System.out.println(msg); } }
3.3.2 加载测试专用配置
在测试范围内新建一个专用的配置类config.MsgConfig.java
和测试类ConfigTest.java
1 2 3 4 5 6 7 8 9 @Configuration public class MsgConfig { @Bean public String msg () { return "msg bean" ; } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SpringBootTest @Import(MsgConfig.class) public class ConfigTest { @Autowired private String msg; @Test void testConfig () { System.out.println(msg); } }
3.3.3 Web环境模拟测试 需要达到的效果:代替 Postman
测试接口功能
① 测试端口启动 使用webEnvironment
属性可以在某个端口启动Web服务器。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
1 2 3 4 5 6 7 8 @SpringBootTest public class WebTest { @Test void test () { } }
运行后发现服务器并没有启动
1 2 3 Starting WebTest using Java 11.0.10 on MSI with PID 29112 (started by Hongyi in E:\develop\study\project-study\springboot\demo1\demo_test) No active profile set, falling back to default profiles: default Started WebTest in 1.513 seconds (JVM running for 2.382)
添加属性webEnvironment
,指定服务器启动端口,共有四种:
MOCK
:自定义端口
DEFINED_PORT
:默认端口8080
RANDOM_PORT
:随机端口
NONE
:无
1 2 3 4 5 6 7 8 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) public class WebTest { @Test void test () { } }
随机端口的情况:
② 发送虚拟请求
注解:
@AutoConfigureMockMvc
:开启虚拟MVC调用
创建控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/books") public class BookController { @GetMapping public String getById () { System.out.println("getById running..." ); return "springboot" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class WebTest { @Test void testWeb (@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books" ); mvc.perform(builder); } }
执行结果:
③ 匹配响应执行状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test void testStatus (@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1" ); ResultActions action = mvc.perform(builder); StatusResultMatchers status = MockMvcResultMatchers.status(); ResultMatcher ok = status.isOk(); action.andExpect(ok); }
打印后的重要信息:
④ 匹配响应体 1 2 3 4 5 6 7 8 9 10 11 @Test void testBody (@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books" ); ResultActions action = mvc.perform(builder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.string("springboot1" ); action.andExpect(result); }
⑤ 匹配响应体json
1 2 3 4 5 6 7 @Data public class Book { private int id; private String name; private String type; private String description; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test void testJson (@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books" ); ResultActions action = mvc.perform(builder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.json("{\"id\": 2,\n" + " \"name\": \"Hongyi\",\n" + " \"type\": \"Book\",\n" + " \"description\": \"Hello World\"}" ); action.andExpect(result); }
⑥匹配响应头 1 2 3 4 5 6 7 8 9 10 11 @Test void testContentType (@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books" ); ResultActions action = mvc.perform(builder); HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher result = header.string("Content-Type" , "application/json" ); action.andExpect(result); }
3.3.4 业务层测试回滚 在进行测试时,会涉及到数据库的增删改查,不可避免地会对数据库中的数据进行操作,这样导致了测试代码对原来数据库的污染,例如删除或更新了原始数据或者增加了仅测试用的数据(实际开发中开发有开发数据库,生产环境有自己的数据库,这样也可以避免污染生产环境使用的数据库)。
这里介绍如何让测试不污染数据库。
为测试用例添加事务 @Transactional
,SpringBoot会对测试用例对应的事务提交操作进行回滚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest @Transactional public class ServiceTest { @Autowired private BookService bookService; @Test void testSave () { Book book = new Book (); book.setName("Hongyi2" ); book.setType("Book2" ); book.setDescription("Hello World2" ); bookService.save(book); } }
发现并没有提交的数据
如果想在测试用例中提交事务,可以通过@Rollback
注解设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest @Transactional @Rollback(value = false) public class ServiceTest { @Autowired private BookService bookService; @Test void testSave () { Book book = new Book (); book.setName("Hongyi2" ); book.setType("Book2" ); book.setDescription("Hello World2" ); bookService.save(book); } }
3.3.5 测试用例数据设定 测试用例数据通常采用随机值 进行测试,使用SpringBoot提供的随机数为其赋值。
1 2 3 4 5 testcase: book: name: ${random.value} type: ${random.value} description: ${random.value}
其他的例子:
1 2 3 4 5 6 7 8 testcast: book: id: ${random.int} id2: ${random.int(10)} type: ${random.int(10,20)} uuid: ${random.uuid} name: ${random.value} publishTime: ${random.long}
{random.int}
表示随机整数
{random.int(10)}
表示10以内的随机数
{random.int(10,20)}
表示10到20的随机数
其中()
可以是任意字符,例如[]
,!!
均可
在test
范围内新建实体类BookCase.java
1 2 3 4 5 6 7 8 @Component @Data @ConfigurationProperties(prefix = "testcase.book") public class BookCase { private String name; private String type; private String description; }
3.4 统一接口封装 3.4.1 统一必要性 以查询某个用户接口而言,如果没有封装,返回结果如下:
1 2 3 4 { "userId" : 1 , "userName" : "赵一" }
如果封装了,则返回正常的结果:
1 2 3 4 5 6 7 8 9 { "timestamp" : 11111111111 , "status" : 200 , "message" : "success" , "data" : { "userId" : 1 , "userName" : "赵一" } }
异常返回结果:
1 2 3 4 5 6 { "timestamp" : 11111111111 , "status" : 10001 , "message" : "User not exist" , "data" : null }
3.4.2 实现案例 ① 状态码封装 这里以常见的状态码为例,包含responseCode 和 description两个属性。
如果还有其它业务状态码,也可以放到这个类中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Getter @AllArgsConstructor public enum ResponseStatus { SUCCESS("200" , "success" ), FAIL("500" , "failed" ), HTTP_STATUS_200("200" , "ok" ), HTTP_STATUS_400("400" , "request error" ), HTTP_STATUS_401("401" , "no authentication" ), HTTP_STATUS_403("403" , "no authorities" ), HTTP_STATUS_500("500" , "server error" ); public static final List<ResponseStatus> HTTP_STATUS_ALL = Collections.unmodifiableList( Arrays.asList(HTTP_STATUS_200, HTTP_STATUS_400, HTTP_STATUS_401, HTTP_STATUS_403, HTTP_STATUS_500 )); private final String responseCode; private final String description; }
② 返回内容封装 包含公共的接口返回时间,状态status,消息message, 以及数据data。
考虑到数据的序列化(比如在网络上传输),这里data有时候还会extends Serializable。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Data @Builder public class ResponseResult <T> { private long timestamp; private String status; private String message; private T data; public static <T> ResponseResult<T> success () { return success(null ); } public static <T> ResponseResult<T> success (T data) { return ResponseResult.<T>builder().data(data) .message(ResponseStatus.SUCCESS.getDescription()) .status(ResponseStatus.SUCCESS.getResponseCode()) .timestamp(System.currentTimeMillis()) .build(); } public static <T extends Serializable > ResponseResult<T> fail (String message) { return fail(null , message); } public static <T> ResponseResult<T> fail (T data, String message) { return ResponseResult.<T>builder().data(data) .message(message) .status(ResponseStatus.FAIL.getResponseCode()) .timestamp(System.currentTimeMillis()) .build(); } }
③ 接口返回时调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @ApiOperation("Add/Edit User") @PostMapping("add") public ResponseResult<User> add (User user) { if (user.getId()==null || !userService.exists(user.getId())) { user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); userService.save(user); } else { user.setUpdateTime(LocalDateTime.now()); userService.update(user); } return ResponseResult.success(userService.find(user.getId())); } @ApiOperation("Query User One") @GetMapping("edit/{userId}") public ResponseResult<User> edit (@PathVariable("userId") Long userId) { return ResponseResult.success(userService.find(userId)); } }
1 2 3 4 5 6 7 8 9 { "timestamp" : 1698896257569 , "status" : "200" , "message" : "success" , "data" : { "name" : "Hongyi" , "age" : 20 } }
3.5 参数校验 3.5.1 必要性 后端对前端传过来的参数也是需要进行校验的,如果在controller中直接校验需要用大量的if else
做判断
以添加用户的接口为例,需要对前端传过来的参数进行校验, 如下的校验就是不优雅的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/user") public class UserController { @PostMapping("add") public ResponseEntity<String> add (User user) { if (user.getName()==null ) { return ResponseResult.fail("user name should not be empty" ); } else if (user.getName().length()<5 || user.getName().length()>50 ){ return ResponseResult.fail("user name length should between 5-50" ); } if (user.getAge()< 1 || user.getAge()> 150 ) { return ResponseResult.fail("invalid age" ); } return ResponseEntity.ok("success" ); } }
Spring Validation是对hibernate validation的二次封装 ,用于支持spring mvc参数自动校验。
3.5.2 实现案例 ① POM依赖 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
② 请求参数封装 单一职责,所以将查询用户的参数封装到UserParam
中, 而不是User(数据库实体)本身。
对每个参数字段添加validation注解约束和message。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Data @Getter public class UserParam { @NotEmpty(message = "could not be empty") private String userId; @NotEmpty(message = "could not be empty") @Email(message = "invalid email") private String email; @NotEmpty(message = "could not be empty") @Pattern(regexp = "^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "invalid ID") private String cardNo; @NotEmpty(message = "could not be empty") @Length(min = 1, max = 10, message = "nick name should be 1-10") private String nickName; @NotNull(message = "could not be empty") @Range(min = 0, max = 1, message = "sex should be 0-1") private int sex; @Max(value = 100, message = "Please input valid age") private int age; }
③ Controller中获取参数绑定结果 使用@Valid
或者@Validated
注解,参数校验的值放在BindingResult中:
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("add") public ResponseResult<String> add (@Valid @RequestBody UserParam userParam, BindingResult bindingResult) { if (bindingResult.hasErrors()) { List<ObjectError> errors = bindingResult.getAllErrors(); errors.forEach(p -> { FieldError fieldError = (FieldError) p; log.error("Invalid Parameter : object - {},field - {},errorMessage - {}" , fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage()); }); return ResponseResult.fail("invalid parameter" ); } return ResponseResult.success("success" ); }
3.6 统一异常处理 3.6.1 必要性 如果我们不统一的处理异常,经常会在controller层有大量的异常处理的代码, 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/user") public class UserController { @PostMapping("add") public ResponseResult<String> add (@Valid @RequestBody UserParam userParam) { try { } catch (Exception e) { return ResponseResult.fail("error" ); } return ResponseResult.success("success" ); } }
3.6.2 实现案例 通过@RestControllerAdvice
进行统一异常处理。
全局异常处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @RestControllerAdvice public class HandleException { @ExceptionHandler(BindException.class) public ResultList handleException (BindException e) { return new ResultList (2004 , e.getBindingResult().getFieldError().getDefaultMessage(), null ); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResultList handleException (MethodArgumentTypeMismatchException e) { return new ResultList (2004 , "参数`" + e.getName() + "`类型转换错误-->" + e.getCause(), null ); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResultList handleException (MethodArgumentNotValidException e) { return new ResultList (2004 , e.getBindingResult().getFieldError().getDefaultMessage(), null ); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResultList handleException (HttpRequestMethodNotSupportedException e) { return new ResultList (2004 , e.getMessage(), null ); } @ExceptionHandler(CustomException.class) public ResultList handleException (CustomException e) { return new ResultList (e.getCode(), e.getMsg(), null ); } @ExceptionHandler(SQLSyntaxErrorException.class) public ResultList handleException (SQLSyntaxErrorException e) { return new ResultList (Code.EXECUTION_ERROR.getCode(), e.getMessage(), null ); } @ExceptionHandler(ReflectionException.class) public ResultList handleException (ReflectionException e) { return new ResultList (Code.EXECUTION_ERROR.getCode(), e.getMessage(), null ); } @ExceptionHandler(InvalidFormatException.class) public ResultList handleException (InvalidFormatException e) { return new ResultList (Code.EXECUTION_ERROR.getCode(), e.getOriginalMessage(), null ); } }
自定义异常,继承RuntimeException
运行时异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ToString @Getter @Setter public class CustomException extends RuntimeException { public CustomException (int code, String msg) { this .code = code; this .msg = msg; } public CustomException (Code code) { this .code = code.getCode(); this .msg = code.getMsg(); } private int code; private String msg; }
异常代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @ToString @Getter public enum Code { PARAM_FORMAT_ERROR(2004 , "参数异常" ), DELETE_NULL(4002 , "数据删除为空" ), DELETE_FAIL(4001 , "数据删除失败" ), DELETE_SUCCESS(4000 , "数据删除成功" ), UPDATE_NULL(5001 , "更新数据为空" ), UPDATE_SUCCESS(5000 , "更新成功" ), ADD_FAIL(5002 , "数据添加失败" ), ADD_SUCCESS(5000 , "数据添加成功" ), QUERY_NULL(2008 , "数据查询为空" ), QUERY_FAIL(2009 , "数据查询失败" ), CHECK_FAIL(6001 , "接口检验失败" ), EXECUTION_ERROR(6002 , "接口执行失败" ), QUERY_SUCCESS(2000 , "数据查询成功" ), CHANGE_LOCK(8000 , "改变设备锁状态" ), CONNECTION_ERROR(8004 , "服务器连接异常" ), SUCC(9000 , "定时任务执行成功" ), FAIL(9001 , "定时任务执行失败" ); private final int code; private final String msg; Code(int code, String msg) { this .code = code; this .msg = msg; } }
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static List<String> getRecentTriggerTime (String cron) { List<String> list = new ArrayList <>(); try { CronTriggerImpl cronTriggerImpl = new CronTriggerImpl (); cronTriggerImpl.setCronExpression(cron); List<Date> dateList = TriggerUtils.computeFireTimes(cronTriggerImpl, null , 5 ); SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); dateList.forEach(date -> list.add(dateFormat.format(date))); } catch (ParseException e) { throw new CustomException (Code.EXECUTION_ERROR); } return list; }
全局异常处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BaseException.class) public AjaxResult baseException (BaseException e) { return AjaxResult.error(e.getMessage()); } @ExceptionHandler(CustomException.class) public AjaxResult businessException (CustomException e) { if (StringUtils.isNull(e.getCode())){ return AjaxResult.error(e.getMessage()); } return AjaxResult.error(e.getCode(), e.getMessage()); } @ExceptionHandler(NoHandlerFoundException.class) public AjaxResult handlerNoFoundException (Exception e) { log.error(e.getMessage(), e); return AjaxResult.error(HttpStatus.NOT_FOUND, "路径不存在,请检查路径是否正确" ); } @ExceptionHandler(AccessDeniedException.class) public AjaxResult handleAuthorizationException (AccessDeniedException e) { log.error(e.getMessage()); return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权" ); } @ExceptionHandler(AccountExpiredException.class) public AjaxResult handleAccountExpiredException (AccountExpiredException e) { log.error(e.getMessage(), e); return AjaxResult.error(e.getMessage()); } @ExceptionHandler(UsernameNotFoundException.class) public AjaxResult handleUsernameNotFoundException (UsernameNotFoundException e) { log.error(e.getMessage(), e); return AjaxResult.error(e.getMessage()); } @ExceptionHandler(Exception.class) public AjaxResult handleException (Exception e) { log.error(e.getMessage(), e); return AjaxResult.error(e.getMessage()); } @ExceptionHandler(BindException.class) public AjaxResult validatedBindException (BindException e) { log.error(e.getMessage(), e); String message = e.getAllErrors().get(0 ).getDefaultMessage(); return AjaxResult.error(message); } @ExceptionHandler(MethodArgumentNotValidException.class) public Object validExceptionHandler (MethodArgumentNotValidException e) { log.error(e.getMessage(), e); String message = e.getBindingResult().getFieldError().getDefaultMessage(); return AjaxResult.error(message); } @ExceptionHandler(DemoModeException.class) public AjaxResult demoModeException (DemoModeException e) { return AjaxResult.error("演示模式,不允许操作" ); } }
自定义异常:可以细分模块
1 2 3 4 5 6 7 8 public class CustomException extends RuntimeException { private static final long serialVersionUID = 1L ; private Integer code; private String message; }
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void validateEdit (GenTable genTable) { if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())){ String options = JSON.toJSONString(genTable.getParams()); JSONObject paramsObj = JSONObject.parseObject(options); if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))){ throw new CustomException ("树编码字段不能为空" ); } else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))){ throw new CustomException ("树父编码字段不能为空" ); } else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))){ throw new CustomException ("树名称字段不能为空" ); } else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())){ if (StringUtils.isEmpty(genTable.getSubTableName())){ throw new CustomException ("关联子表的表名不能为空" ); } else if (StringUtils.isEmpty(genTable.getSubTableFkName())){ throw new CustomException ("子表关联的外键名不能为空" ); } } } }
3.7 定时任务 实现定时任务的方法:
Timer
:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
ScheduledExecutorService
:也jdk自带的一个类;是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
Spring Task
:Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
Quartz
:这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。
3.7.1 Timer
timer.schedule(TimerTask, delay)
:延迟任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Slf4j public class TimeTest { public static void main (String[] args) { timerTest(); } public static void timerTest () { Timer timer = new Timer (); timer.schedule(new TimerTask () { public void run () { log.info("timer-task @{}" , LocalDateTime.now()); } }, 1000 ); try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } timer.cancel(); } }
timer.schedule(TimerTask, delay, period)
:周期任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static void timerScheduleTest () { Timer timer = new Timer (); timer.schedule(new TimerTask () { @SneakyThrows public void run () { log.info("timer-period-task @{}" , LocalDateTime.now()); Thread.sleep(100 ); } }, 500 , 1000 ); try { Thread.sleep(10000 ); } catch (InterruptedException e) { e.printStackTrace(); } timer.cancel(); }
测试结果:
1 2 3 4 5 6 7 8 9 10 11 12 10:36:49.096 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:49.094084400 10:36:50.090 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:50.090673400 10:36:51.090 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:51.090960200 10:36:52.091 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:52.091758300 10:36:53.091 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:53.091636700 10:36:54.091 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:54.091817800 10:36:55.092 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:55.092264300 10:36:56.095 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:56.095661300 10:36:57.095 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:57.095840900 10:36:58.096 [Timer-0] INFO com.hongyi.apiintegration.time.TimeTest - timer-period-task @2024-07-15T10:36:58.096486300 Process finished with exit code 0
总结:每次执行完当前任务后,然后间隔一个period的时间再执行下一个任务; 当某个任务执行周期大于时间间隔时,依然按照间隔时间执行下个任务,即它保证了任务之间执行的间隔 。
Timer底层是使用一个单线程来实现多个Timer任务处理的,所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。
3.7.2 ScheduledExecutorService 该方法跟Timer类似,而ScheduledExecutorService是基于线程池的,可以开启多个线程进行执行多个任务,每个任务开启一个线程; 这样任务的延迟和未处理异常就不会影响其它任务的执行了。
1 2 3 4 5 6 7 8 9 10 11 12 private static void ScheduledExecutorServiceTest () { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1 ); executor.schedule(() -> log.info("run schedule @ {}" , LocalDateTime.now()), 1000 , TimeUnit.MILLISECONDS); try { Thread.sleep(10000 ); } catch (InterruptedException e) { e.printStackTrace(); } executor.shutdown(); }
3.7.3 Spring Task ① cron表达式 Cron表达式是一个具有时间含义的字符串,字符串以5个空格隔开,分为6个域,格式为X X X X X X
。其中X是一个域的占位符。单个域有多个取值时,使用半角逗号,
隔开取值。每个域可以是确定的取值,也可以是具有逻辑意义的特殊字符。秒 分 时 日 月 星 年
。
下表为Cron表达式中六个域能够取的值以及支持的特殊字符。
域
是否必需
取值范围
特殊字符
秒 Seconds
是
[0, 59]
* , - /
分钟 Minutes
是
[0, 59]
* , - /
小时 Hours
是
[0, 23]
* , - /
日期 DayofMonth
是
[1, 31]
* , - / ? L W
月份 Month
是
[1, 12]或[JAN, DEC]
* , - /
星期 DayofWeek
是
[1, 7]或[MON, SUN]。若使用[1, 7]表达方式,1代表星期一,7代表星期日。
* , - / ? L #
年 Year
否
1970+
- * /
其中:
*
:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。
?
:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用,如果使用 表示不管星期几都会触发,实际上并不是这样。
-
:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
/
:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
,
:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
L
:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
W
:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
LW
:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
#
:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
② 实现
cron
:cron表达式,指定任务在特定时间执行;
fixedDelay
:表示上一次任务执行完成后多久再次执行,参数类型为long,单位ms;
fixedDelayString
:与fixedDelay含义一样,只是参数类型变为String;
fixedRate
:表示按一定的频率执行任务,参数类型为long,单位ms;
fixedRateString
: 与fixedRate的含义一样,只是将参数类型变为String;
initialDelay
:表示延迟多久再第一次执行任务,参数类型为long,单位ms;
initialDelayString
:与initialDelay的含义一样,只是将参数类型变为String;
zone
:时区,默认为当前时区,一般没有用到。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Component @EnableScheduling public class ScheduleTest { @Scheduled(fixedRate = 1000 * 60) public void runScheduledFixedRate () { log.info("runScheduledFixedRate: current Datetime, {}" , LocalDateTime.now()); } }
问题:单线程串行执行,当某个任务超时或异常时,导致后面的任务阻塞。
3.7.4 Quartz Quartz[kwɔːrts]
,石英。
① 体系结构
Job 表示一个工作,要执行的具体内容,可以对应多个Trigger。
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
Trigger 代表一个调度参数的配置,什么时候去调,只能对应一个Job。
Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。Schedule维护着一个定时任务和触发器的注册表,当两者注册之后,如果触发器到达规定时间触发的时候,Schedule负责执行与触发器关联的定时任务。值得注意的是一个定时任务可以对应多个触发器,当每个触发器触发的时候都会执行其关联的定时任务,但是一个触发器只能对应一个定时任务。
② 实现
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-quartz</artifactId > </dependency >
继承QuartzJobBean
,并重载executeInternal
方法即可定义你自己的Job执行逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j public class HelloJob extends QuartzJobBean { @Override protected void executeInternal (JobExecutionContext context) { context.getJobDetail().getJobDataMap().forEach( (k, v) -> log.info("param, key:{}, value:{}" , k, v) ); log.info("Hello Job执行时间: " + new Date ()); } }
QuartzJobBean
实现了Job
接口:
1 2 3 public abstract class QuartzJobBean implements Job { }
配置Job:包含JobDetail,Trigger和Schedule
JobBuilder
用于实例化一个定时任务,即JobDetail
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Configuration public class QuartzConfig { @Bean("helloJob") public JobDetail helloJobDetail () { return JobBuilder.newJob(HelloJob.class) .withIdentity("DateTimeJob" ) .usingJobData("msg" , "Hello Quartz" ) .storeDurably() .build(); } @Bean public Trigger printTimeJobTrigger () { CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?" ); return TriggerBuilder.newTrigger() .forJob(helloJobDetail()) .withIdentity("quartzTaskService" ) .withSchedule(cronScheduleBuilder) .build(); } }
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 2024-07-15 11:33:50.691 INFO 4604 --- [eduler_Worker-1] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz 2024-07-15 11:33:50.691 INFO 4604 --- [eduler_Worker-1] com.hongyi.apiintegration.time.HelloJob : Hello Job执行时间: Mon Jul 15 11:33:50 CST 2024 2024-07-15 11:33:50.693 INFO 4604 --- [ main] com.hongyi.apiintegration.time.Main : Started Main in 4.386 seconds (JVM running for 4.764) 2024-07-15 11:33:51.001 INFO 4604 --- [eduler_Worker-2] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz 2024-07-15 11:33:51.001 INFO 4604 --- [eduler_Worker-2] com.hongyi.apiintegration.time.HelloJob : Hello Job执行时间: Mon Jul 15 11:33:51 CST 2024 2024-07-15 11:33:52.001 INFO 4604 --- [eduler_Worker-3] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz 2024-07-15 11:33:52.001 INFO 4604 --- [eduler_Worker-3] com.hongyi.apiintegration.time.HelloJob : Hello Job执行时间: Mon Jul 15 11:33:52 CST 2024 2024-07-15 11:33:53.000 INFO 4604 --- [eduler_Worker-4] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz 2024-07-15 11:33:53.000 INFO 4604 --- [eduler_Worker-4] com.hongyi.apiintegration.time.HelloJob : Hello Job执行时间: Mon Jul 15 11:33:53 CST 2024 2024-07-15 11:33:54.007 INFO 4604 --- [eduler_Worker-5] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz 2024-07-15 11:33:54.008 INFO 4604 --- [eduler_Worker-5] com.hongyi.apiintegration.time.HelloJob : Hello Job执行时间: Mon Jul 15 11:33:54 CST 2024 2024-07-15 11:33:55.000 INFO 4604 --- [eduler_Worker-6] com.hongyi.apiintegration.time.HelloJob : param, key:msg, value:Hello Quartz
③ 案例 项目地址:QuartzManager
项目结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 public class QuartzUtil { private static final SchedulerFactory schedulerFactory = new StdSchedulerFactory (); private static final Scheduler scheduler; private final static String groupName = "DEFAULT" ; static { try { scheduler = schedulerFactory.getScheduler(); } catch (SchedulerException e) { throw new CustomException (Code.EXECUTION_ERROR); } } public static <T extends Job > boolean addJob (String jobName, String triggerName, String description, Class<T> jobClass, String cron) { try { JobDetail job = JobBuilder.newJob(jobClass) .withIdentity(jobName, groupName) .withDescription(description) .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerName, groupName) .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); return scheduler.isStarted(); } catch (Exception e) { throw new CustomException (Code.EXECUTION_ERROR); } } public static Boolean rescheduleJob (String jobName, String triggerName, String cron) { try { TriggerKey triggerKey = TriggerKey.triggerKey(triggerName); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); if (trigger == null ) { return false ; } checkJobNameAndTriggerName(jobName, trigger); Date latestFireTime = new Date (); if (!trigger.getCronExpression().equalsIgnoreCase(cron)) { trigger = TriggerBuilder.newTrigger() .withIdentity(triggerName) .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); latestFireTime = scheduler.rescheduleJob(triggerKey, trigger); } return null != latestFireTime; } catch (SchedulerException e) { throw new CustomException (Code.EXECUTION_ERROR); } } public static boolean removeJob (String jobName, String triggerName) { boolean flag; try { Trigger trigger = scheduler.getTrigger(new TriggerKey (triggerName)); if (null == trigger) { return false ; } checkJobNameAndTriggerName(jobName, trigger); TriggerKey triggerKey = trigger.getKey(); scheduler.pauseTrigger(triggerKey); flag = scheduler.unscheduleJob(triggerKey); } catch (SchedulerException e) { throw new CustomException (Code.EXECUTION_ERROR); } return flag; } public static HashMap<String, String> getJob (String jobName, String triggerName) { try { JobDetail jobDetail = scheduler.getJobDetail(new JobKey (jobName)); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(new TriggerKey (triggerName)); if (null == jobDetail || null == trigger) { return new HashMap <>(); } checkJobNameAndTriggerName(jobName, trigger); return getJobInfo(jobDetail, trigger); } catch (SchedulerException e) { throw new CustomException (Code.EXECUTION_ERROR); } } public static List<HashMap<String, String>> getJobs () { List<HashMap<String, String>> list = new ArrayList <>(); try { List<String> triggerGroupNames = scheduler.getTriggerGroupNames(); for (String groupName : triggerGroupNames) { GroupMatcher<TriggerKey> groupMatcher = GroupMatcher.groupEquals(groupName); Set<TriggerKey> triggerKeySet = scheduler.getTriggerKeys(groupMatcher); for (TriggerKey triggerKey : triggerKeySet) { CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); JobDetail jobDetail = scheduler.getJobDetail(trigger.getJobKey()); list.add(getJobInfo(jobDetail, trigger)); } } } catch (SchedulerException e) { throw new CustomException (Code.EXECUTION_ERROR); } return list; } public static List<String> getRecentTriggerTime (String cron) { List<String> list = new ArrayList <>(); try { CronTriggerImpl cronTriggerImpl = new CronTriggerImpl (); cronTriggerImpl.setCronExpression(cron); List<Date> dateList = TriggerUtils.computeFireTimes(cronTriggerImpl, null , 5 ); SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); dateList.forEach(date -> list.add(dateFormat.format(date))); } catch (ParseException e) { throw new CustomException (Code.EXECUTION_ERROR); } return list; } @SuppressWarnings("all") public static boolean checkCronExpression (String cronExpression) { return CronExpression.isValidExpression(cronExpression); } private static HashMap<String, String> getJobInfo (JobDetail jobDetail, CronTrigger trigger) { HashMap<String, String> map = new HashMap <>(); map.put("jobName" , jobDetail.getKey().getName()); map.put("jobGroup" , jobDetail.getKey().getGroup()); map.put("corn" , trigger.getCronExpression()); map.put("triggerName" , trigger.getKey().getName()); map.put("description" , jobDetail.getDescription()); return map; } private static void checkJobNameAndTriggerName (String jobName, Trigger trigger) { String name = trigger.getJobKey().getName(); if (!name.equals(jobName)) { throw new CustomException (Code.PARAM_FORMAT_ERROR.getCode(), "jobName与triggerName不匹配" ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 public interface ITimedTaskService { ResultList queryTimedTask (String jobName, String triggerName) ; ResultList delTimedTask (String jobName, String triggerName) ; ResultList queryAllTask () ; ResultList getRecentTriggerTime (String cron) ; ResultList rescheduleJob (String jobName, String triggerName, String cron) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 @Service public class TimedTaskServiceImpl implements ITimedTaskService { @Override public ResultList queryTimedTask (String jobName, String triggerName) { HashMap<String, String> jobTask = QuartzUtil.getJob(jobName, triggerName); return ResultUtil.verifyQuery(jobTask); } @Override public ResultList delTimedTask (String jobName, String triggerName) { boolean isDelJob = QuartzUtil.removeJob(jobName, triggerName); return ResultUtil.verifyDelete(isDelJob); } @Override public ResultList rescheduleJob (String jobName, String triggerName,String cron) { if (!QuartzUtil.checkCronExpression(cron)) { return ResultUtil.execute(Code.PARAM_FORMAT_ERROR, "cron解析错误" ); } boolean isModifyJob = QuartzUtil.rescheduleJob(jobName, triggerName,cron); return ResultUtil.verifyUpdate(isModifyJob); } @Override public ResultList queryAllTask () { List<HashMap<String, String>> list = QuartzUtil.getJobs(); return ResultUtil.verifyQuery(list, list.size()); } @Override public ResultList getRecentTriggerTime (String cron) { if (!QuartzUtil.checkCronExpression(cron)) { return ResultUtil.execute(Code.PARAM_FORMAT_ERROR, "cron解析错误" ); } List<String> list = QuartzUtil.getRecentTriggerTime(cron); return new ResultList (Code.SUCC.getCode(), Code.SUCC.getMsg(), new Result <>(list.size(), list)); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Service public class MigratedUsedListImpl implements IMigrateUsedList { @Override public ResultList migrateUsedList (String jobName, String triggerName, String cron) { if (!QuartzUtil.checkCronExpression(cron)) { return ResultUtil.execute(Code.PARAM_FORMAT_ERROR, "cron解析错误" ); } String description = "将90天以前的历史订单迁移到xiaoJi_bak" ; boolean isStart = QuartzUtil.addJob(jobName, triggerName, description, MigrateUsedList.class, cron); if (isStart) { return ResultUtil.execute(Code.SUCC); } return ResultUtil.execute(Code.FAIL); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class HelloServiceImpl implements IHelloService { @Override public ResultList HelloTest (String jobName, String triggerName, String cron) { if (!QuartzUtil.checkCronExpression(cron)) { return ResultUtil.execute(Code.PARAM_FORMAT_ERROR, "cron解析错误" ); } String description = "HelloWorld测试" ; boolean isStart = QuartzUtil.addJob(jobName, triggerName, description, HelloJob.class, cron); if (isStart) { return ResultUtil.execute(Code.SUCC); } return ResultUtil.execute(Code.FAIL); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @RestController @Api(tags = "定时任务接口") @RequestMapping(value = "timed") public class TimedTaskController { @Resource ITimedTaskService timedTaskServiceImpl; @Resource IMigrateUsedList migrateUsedListImpl; @Resource IHelloService helloServiceImpl; @ApiOperation(value = "迁移历史订单数据定时任务", httpMethod = "POST") @PostMapping(value = "/migrateUsedList") public ResultList migrateUsedList (@RequestParam String jobName, @RequestParam String triggerName, @RequestParam String cron) { return migrateUsedListImpl.migrateUsedList(jobName, triggerName, cron); } @ApiOperation(value = "定时任务测试", httpMethod = "POST") @PostMapping(value = "/hello") public ResultList helloTest (@RequestParam String jobName, @RequestParam String triggerName, @RequestParam String cron) { return helloServiceImpl.HelloTest(jobName, triggerName, cron); } @ApiOperation(value = "查询所有定时任务", httpMethod = "GET") @GetMapping(value = "/job") public ResultList queryAllTask () { return timedTaskServiceImpl.queryAllTask(); } @ApiOperation(value = "查询定时任务", httpMethod = "GET") @GetMapping(value = "/job/{jobName}/{triggerName}") public ResultList queryTimedTask (@PathVariable String jobName, @PathVariable String triggerName) { return timedTaskServiceImpl.queryTimedTask(jobName, triggerName); } @ApiOperation(value = "查询最近5次执行时间", httpMethod = "GET") @GetMapping(value = "/job/{cron}") public ResultList queryAllTask (@PathVariable String cron) { return timedTaskServiceImpl.getRecentTriggerTime(cron); } @ApiOperation(value = "删除定时任务", httpMethod = "DELETE") @DeleteMapping(value = "/job/{jobName}/{triggerName}") public ResultList delTimedTask (@PathVariable String jobName, @PathVariable String triggerName) { return timedTaskServiceImpl.delTimedTask(jobName, triggerName); } @ApiOperation(value = "修改定时任务执行时间", httpMethod = "POST") @PostMapping(value = "/job/{jobName}/{triggerName}/{cron}") public ResultList rescheduleJob (@PathVariable String jobName, @PathVariable String triggerName, @PathVariable String cron) { return timedTaskServiceImpl.rescheduleJob(jobName, triggerName, cron); } }
执行结果:
1 2 3 4 5 6 2024-07-15 14:43:57.008 INFO 5776 --- [eduler_Worker-1] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:43:57.008757700 2024-07-15 14:43:59.000 INFO 5776 --- [eduler_Worker-2] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:43:59.000915500 2024-07-15 14:44:01.006 INFO 5776 --- [eduler_Worker-3] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:44:01.006727400 2024-07-15 14:44:03.001 INFO 5776 --- [eduler_Worker-4] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:44:03.001733900 2024-07-15 14:44:05.000 INFO 5776 --- [eduler_Worker-5] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:44:05.000841600 2024-07-15 14:44:07.002 INFO 5776 --- [eduler_Worker-6] com.frost2.quartz.common.job.HelloJob : Hello Job - time: 2024-07-15T14:44:07.002672700