课程说明

内容说明

  • SpringBoot这门技术是用来加速开发Spring程序的,因此学习这门技术有一定的门槛,即至少学完Spring,SpringMVC,Mybatis等相关技术,才能够学习这门技术。
  • 该课程主要分为基础篇、应用篇、原理篇
  • 每个课程单元内容设置不同,目标也不一样
课程单元学习目标
基础篇能够创建SpringBoot工程
基于SpringBoot实现ssm/ssmp整合
应用篇能够掌握SpringBoot程序多环境开发
能够基于Linux系统发布SpringBoot工程
能够解决线上灵活配置SpringBoot工程的需求
能够基于SpringBoot整合任意第三方技术
原理篇掌握SpringBoot内部工作流程
理解SpringBoot整合第三方技术的原理
实现自定义开发整合第三方技术的组件

前置知识说明

课程单元前置知识要求
基础篇Java基础语法面向对象,封装,继承,多态,类与接口,集合,IO,网络编程等
基础篇Spring与SpringMVC知道Spring是用来管理bean,能够基于Restful实现页面请求交互功能
基础篇Mybatis与Mybatis-Plus基于Mybatis和MybatisPlus能够开发出包含基础CRUD功能的标准Dao模块
基础篇数据库MySQL能够读懂基础CRUD功能的SQL语句
基础篇服务器知道服务器与web工程的关系,熟悉web服务器的基础配置
基础篇maven知道maven的依赖关系,知道什么是依赖范围,依赖传递,排除依赖,可选依赖,继承
基础篇web技术(含vue,ElementUI)知道vue如何发送ajax请求,如何获取响应数据,如何进行数据模型双向绑定
应用篇Linux(CenterOS7)熟悉常用的Linux基础指令,熟悉Linux系统目录结构
应用篇实用开发技术缓存:Redis、MongoDB、……
消息中间件:RocketMq、RabbitMq、……
原理篇Spring了解Spring加载bean的各种方式
知道Spring容器底层工作原理,能够阅读简单的Spring底层源码

SpringBoot基础篇

快速上手SpringBoot

SpringBoot技术由Pivotal团队研发制作,功能的话简单概括就是加速Spring程序的开发,这个加速要从如下两个方面来说

  • Spring程序初始搭建过程
    • 原始的Spring至少有一个配置文件或配置类,Web开发还需加载指定Spring配置。
  • Spring程序的开发过程
    • 开发过程无外乎关于导入依赖,将相关类交给Spring管理。

入门程序 (一)

课程版本:

  • IDEA 2020.3
  • Maven 3.6.1
  • JDK8

需求:使用SpringBoot快速构建SpringMVC程序

  • 步骤一:创建Spring工程
  • 步骤二:选择需要的技术集

根据需求,左侧选择Web然后中间选择SpringMVC即可。

  • 步骤三:开发控制器类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //Rest模式
    @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程序的运行形式来运行程序,运行完毕后,控制台输出上述信息。

​ 可以发现,SpringBoot启动成功,并且已经将8080端口映射到本机,这说明在内部SpringBoot容器为我们启动了一个Tomcat服务器,此时访问8080端口,就能测试功能是否正常工作了

访问路径: http://localhost:8080/books

是不是感觉很神奇?目前的效果其实依赖的底层逻辑还是很复杂的,但是从开发者角度来看,目前只有两个文件展现到了开发者面前


该配置文件中两个信息需要关注,一个是parent,一个是dependencies

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
<?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.5.4</version>
</parent>

<groupId>com.itheima</groupId>
<artifactId>springboot_01_01_quickstart</artifactId>
<version>0.0.1-SNAPSHOT</version>

<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>
</project>

  • 该类的执行的功能就一句代码,前面运行程序就是运行的这个类
    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }

SpringBoot的优势:

类配置文件SpringSpringBoot
pom文件中的坐标手工添加勾选添加
web3.0配置类手工制作
Spring/SpringMVC配置类手工制作
控制器手工制作手工制作

​ 一句话总结一下就是能少写就少写能不写就不写,这就是SpringBoot技术给我们带来的好处。

总结

  1. 开发SpringBoot程序可以根据向导进行联网快速制作
  2. SpringBoot程序需要基于JDK8以上版本进行制作
  3. SpringBoot程序中需要使用何种功能通过勾选来选择技术,也可以手工添加对应的要使用的技术(后期讲解)
  4. 运行SpringBoot程序通过运行Application程序入口进行

思考

​* 前面制作的时候说过,这个过程必须联网才可以进行,但是有些时候你会遇到一些莫名其妙的问题,比如基于Idea开发时,你会发现你配置了一些坐标,然后Maven下载对应东西的时候死慢死慢的,甚至还会失败。其实这和Idea这款IDE工具有关,万一Idea不能正常访问网络的话,我们是不是就无法制作SpringBoot程序了呢?下一节来解决这个问题

入门程序(二)

​ 开发SpringBoot也可以不基于任何IDE工具,直接在SprigBoot官网创建程序

​ SpringBoot官网和Spring的官网是在一起的,都是https://spring.io 。可以通过项目一级一级的找到SpringBoot技术的介绍页,然后在页面中间部位找到如下内容

步骤一::点击Spring Initializr后进入到创建SpringBoot程序的界面上,下面是输入信息的过程,和前面的一样,只是界面变了而已,根据自己的要求,在左侧选择对应信息和输入对应的信息即可

步骤②:右侧的ADD DEPENDENCIES选择需要使用的技术,和之前勾选的Spring WEB是在做同一件事,仅仅是界面不同,点击后打开网页版的技术选择界面

步骤③:所有信息设置完毕后,点击下面左侧按钮,生成一个文件包,将该文件包解压后就是创建的SpringBoot工程文件夹了,直接导入IDEA即可使用

其实在IDEA工具中创建的SpringBoot工程和这种方式创建的SpringBoot工程一样,只是IDEA将界面进行了整合展示。

思考

  • 这两种创建方式都是访问国外的Spring主站,但是互联网访问是可以控制的,如果一天这个网站你在国内都无法访问了,那前面这两种方式都无法创建SpringBoot工程了,这时候又该怎么解决这个问题呢?

入门程序(三)

前面两种方式都是通过SpringBoot的官网创建的SpringBoot工程,那如果我们国内有这么一个网站能提供这样的功能,是不是就解决了呢?答案是肯定的,阿里就为我们提供了这样一个网站

​ 创建工程时,切换选择starter服务路径,然后输入阿里云提供给我们的使用地址即可。地址:http://start.aliyun.com或https://start.aliyun.com

​ 阿里为了便于自己开发使用,因此在依赖坐标中添加了一些阿里相关的技术,也是为了推广自己的技术吧,所以在依赖选择列表中有了更多的选择。不过阿里云地址默认创建的SpringBoot工程版本是2.4.1,所以如果想更换其他的版本,创建项目后手工修改即可,别忘了刷新一下,加载新版本信息


注意:

  • 阿里云提供的工程创建地址初始化完毕后和使用SpringBoot官网创建出来的工程略有区别。主要是在配置文件的形式上有区别。这个信息在后面讲解Boot程序的执行流程时再次介绍。

思考

  • ​做到这里已经有了三种方式创建SpringBoot工程,但是每种方式都要求你必须能上网才能创建工程。假如有一天,你加入了一个保密级别比较高的项目组,整个项目组没有外网,整个事情是不是就不能做了呢?

入门程序(四)

​ 不能上网,还想创建SpringBoot工程,能不能做呢?能做,但是你要先问问自己联网和不联网到底差别是什么?这个信息找到以后,把联网要干的事情都提前准备好,就无需联网了。

​ 联网做什么呢?首先SpringBoot工程也是基于Maven构建的,而Maven工程当使用了一些自己需要使用又不存在的东西时,就要去下载。其实SpringBoot工程创建的时候就是去下载一些必要的组件的。提前将这些组件准备好就可以避免联网了。

步骤一:创建工程时,选择手工创建Maven工程

步骤二:参照标准SpringBoot工程的pom文件,书写自己的pom文件即可

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
<?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 http://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.5.4</version>
</parent>

<groupId>com.itheima</groupId>
<artifactId>springboot_01_04_quickstart</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

</project>

步骤三:之前运行SpringBoot工程需要一个类,这个缺不了,手写一个就行了,建议按照之前的目录结构来创建,先别玩花样,先学走后学跑。类名可以自定义,关联的名称一起修改即可

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(<Application.class);
}
}

关注:类上面的注解@SpringBootApplication是核心注解一定不能少

关注:类名可以自定义,只要保障下面代码中使用的类名和你自己定义的名称一样即可,也就是run方法中的那个class对应的名称

步骤四:下面就可以自己创建一个Controller测试一下是否能用了,和之前没有差别了

  • 其实实践一遍就明白了,前面集中方法和手写其实内容一样,只是帮我们直接生成了代码,更加方便一些

注意:

  • 如果你的计算机上从来没有创建成功过SpringBoot工程,自然也就没有下载过SpringBoot对应的坐标,那用手写创建的方式在不联网的情况下肯定是不能用的。所谓手写,就是自己写别人帮你生成的东西,但是引用的坐标对应的资源必须保障maven仓库里面有才行,如果没有,还是要去下载的

总结

  1. 创建普通Maven工程
  2. 继承spring-boot-starter-parent
  3. 添加依赖spring-boot-starter-web
  4. 制作引导类Application

小技巧:隐藏指定文件/文件夹

  • 无论哪种方式,创建SpringBoot工程时都会额外有一些用不到的文件夹,可能看起来就很臃肿别扭。
  • 如果我们足够了解这些文件/目录的作用,处理这些用不到的文件夹的方法无外乎两种:
    • 删除
    • 隐藏
  • 删除直接Delete就好,这里记录下隐藏方法

步骤一:打开设置,【Files】→【Settings】

步骤二:打开文件类型设置界面,【Editor】→【File Types】→【Ignored Files and Folders】,忽略文件或文件夹显示

步骤三:添加要隐藏的文件名称或文件夹名称,可以使用*号通配符,表示任意,设置完毕即可

小结

  • 这里简单介绍了SpringBoot的四种创建方案:
    • IDEA脚手架创建
    • Spring官网创建
    • 阿里脚手架创建
    • 手动创建

前面三种创建方法都需要联网创建,手动创建的方式不需要联网但前提是maven仓库中已经有了需要的SpringBoot依赖

SpringBoot简介

通过入门案例,可以清楚的感受到SpringBoot的功能——加速Spring程序开发

​SpringBoot在哪些方面进行了简化?

  • 起步依赖(简化依赖配置)
    • 依赖配置的书写简化就是靠这个起步依赖达成的,添加一个起步依赖即可引入许多相关坐标
  • 自动配置(简化常用工程相关配置)
    • 配置过于繁琐,使用自动配置就可以做响应的简化,但是内部还是很复杂的,后面具体展开说明
  • 辅助功能(内置服务器,……)
    • 除了上面的功能,其实SpringBoot程序还有其他的一些优势,比如我们没有配置Tomcat服务器,但是能正常运行,这是SpringBoot程序的一个可以感知到的功能,也是SpringBoot的辅助功能之一。

​ 下面结合入门程序来说说这些简化操作都在哪些方面进行体现的,一共分为4个方面

  • parent
  • starter
  • 引导类
  • 内嵌tomcat

parent

  • 开发者在引入依赖时,对于一些依赖往往具有固定的搭配格式,因为这些依赖在不同版本可能存在冲突比如A技术的2.0版与B技术的3.5版可以合作在一起,但是和B技术的3.7版合并使用时就有冲突。而SpringBoot就将各种技术的常见依赖版本搭配收集整理起来,相当于SpringBoot做了无数个技术版本搭配的列表,这个技术搭配列表的名字叫做parent

  • parent自身存在多个版本,每个parent版本都有几百个其他版本的版本号,开发者不用考虑其他问题,使用某些技术时,直接使用SpringBoot提供的parent即可

    比如现在要使用Spring配合MyBatis开发

    • 没有parent之前需要选个Spring的版本,再选个MyBatis的版本,再把这些技术使用时关联的其他技术的版本逐一确定下来。当Spring的版本发生变化需要切换时,MyBatis版本有可能也要跟着切换,连关联技术可能都要切换,而且切换后还可能出现问题。
    • 使用parent就不用关注不同技术间的版本冲突问题,只需要关注用到的技术即可


    ​parent会不会将一些我不想使用的依赖也导入进来?

  • 记清楚,这一点很关键,parent仅仅帮我们进行版本管理,它不负责导入坐标,说白了用什么还是你自己定,只不过版本不需要自己管理了。整体上来说,使用parent可以帮助开发者进行版本的统一管理

关注:parent定义出来以后,并不是直接使用的,仅仅给了开发者一个说明书,但是并没有实际使用,只有在项目中引入parent才会生效

  • 项目中的pom.xml中继承了一个坐标

    1
    2
    3
    4
    5
    <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>
  • 这个坐标中定义了两组信息,第一组是各式各样的依赖版本号属性,第二组是各式各样的依赖坐标信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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>

这些依赖坐标定义中没有具体的依赖版本号,而是引用了第一组信息中定义的依赖版本属性值

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>标签中,这是依赖管理,不是直接导入依赖,只有我们在<dependencies>引入了这些依赖,Maven才会真正声明并下载对应版本的依赖
  • 因为在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. 开发SpringBoot程序要继承spring-boot-starter-parent
  2. spring-boot-starter-parent中定义了若干个依赖管理
  3. 继承parent模块可以避免多个依赖使用相同技术时出现依赖版本冲突
  4. 继承parent的形式也可以采用引入依赖的形式实现效果

思考

​ parent中定义了若干个依赖版本管理,但是也没有使用,那这个设定也就不生效啊,究竟谁在使用这些定义呢?

starter

​SpringBoot关注到开发者在实际开发时,对于依赖坐标的使用往往都有一些固定的组合方式,比如使用spring-webmvc就一定要使用spring-web。每次都要固定搭配着写,非常繁琐,而且格式固定,没有任何技术含量。
于是SpringBoot将这些技术使用的固定搭配格式开发出来,以后开发者在使用某种技术时,就可以直接添加一个名字中包含starter的依赖,而不需要再添加其他的依赖了。

​ starter定义了使用某种技术时对于依赖的固定搭配格式,也是一种最佳解决方案,使用starter可以帮助开发者减少依赖配置

​ 入门案例中的web功能就是使用这种方式添加依赖的。可以查阅SpringBoot的配置源码,看到这些定义

  • 项目中的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
<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>

​ 之前提到过开发SpringMVC程序需要导入spring-webmvc的坐标和spring整合web开发的坐标,就是上面这组坐标中的最后两个了。

​ 但是我们发现除了这两个还有其他的,比如第二个,叫做spring-boot-starter-json。看名称就知道,这个是与json有关的坐标了,但是看名字发现和最后两个又不太一样,它的名字中也有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
<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</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.9</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.12.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.12.4</version>
<scope>compile</scope>
</dependency>
</dependencies>

​ 我们可以发现,这个starter中又包含了若干个坐标,其实就是使用SpringMVC开发通常都会使用到Json,使用json又离不开这里面定义的这些坐标,SpringBoot把我们开发中使用的东西能用到的都给提前做好了。仔细看完会发现,里面有一些你没用过的。的确会出现这种过量导入的可能性,没关系,可以通过maven中的排除依赖剔除掉一部分。不过你不管它也没事,大不了就是过量导入呗。

​ 到这里基本上得到了一个信息,使用starter可以帮开发者快速配置依赖关系。以前写依赖3个坐标的,现在导入一个就搞定了,就是加速依赖配置的。

starter与parent的区别

​ 朦朦胧胧中感觉starter与parent好像都是帮助我们简化配置的,但是功能又不一样,梳理一下。

  • starter是一个坐标中定了若干个坐标,以前写多个的,现在写一个,用来减少依赖配置的书写量

  • parent定义了几百个依赖版本号,以前写依赖需要自己手工控制版本,现在由SpringBoot统一管理,这样就不存在版本冲突了,用来减少依赖冲突

实际开发应用方式

  • 实际开发中如果需要用什么技术,先去找有没有这个技术对应的starter

    • 如果有对应的starter,直接写starter,而且无需指定版本,版本由parent提供
    • 如果没有对应的starter,手写坐标即可
  • 实际开发中如果发现坐标出现了冲突现象,确认你要使用的可行的版本号,使用手工书写的方式添加对应依赖,覆盖SpringBoot提供给我们的配置管理

    • 方式一:直接写坐标
    • 方式二:覆盖<properties>中定义的版本号,就是下面这堆东西了,哪个冲突了覆盖哪个就OK了
    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>

SpringBoot官方给出了好多个starter的定义,方便我们使用,而且名称都是如下格式

1
命名规则:spring-boot-starter-技术名称

总结

  1. 开发SpringBoot程序需要导入坐标时通常导入对应的starter
  2. 每个不同的starter根据功能不同,通常包含多个依赖坐标
  3. 使用starter可以实现快速配置的效果,达到简化配置的目的

引导类

通过前面学习的parentstarter帮助我们减少了很多的配置工作,下面说一下程序是如何运行的。目前程序运行的入口就是SpringBoot工程创建时自带的带有main方法的那个类,运行这个类就可以启动SpringBoot工程的运行

1
2
3
4
5
6
@SpringBootApplication
public class Springboot0101QuickstartApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot0101QuickstartApplication.class, args);
}
}

SpringBoot本身是为了加速Spring程序的开发的,而Spring程序运行的基础是需要创建自己的Spring容器对象(IoC容器)并将所有的对象交给Spring的容器管理。通过SpringBoot加速开发Spring程序,这个容器也一定在。当前这个类运行后就会产生一个Spring容器对象,并且可以将这个对象保存起来,通过容器对象直接操作Bean。

1
2
3
4
5
6
7
8
@SpringBootApplication
public class Springboot0101QuickstartApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(Springboot0101QuickstartApplication.class, args);
BookController bean = ctx.getBean(BookController.class);
System.out.println("bean======>" + bean);
}
}

​ 通过上述操作不难看出,其实SpringBoot程序启动还是创建了一个Spring容器对象。这个类在SpringBoot程序中是所有功能的入口,称这个类为引导类

​ 作为一个引导类最典型的特征就是当前类上方声明了一个注解@SpringBootApplication

总结

  1. SpringBoot工程提供引导类用来启动程序
  2. SpringBoot工程启动后创建并初始化Spring容器

思考
​ 程序现在已经运行了。但是运行java程序不应该是执行完就结束了吗?但是我们现在明显是启动了一个web服务器,不然网页怎么能正常访问呢?这个服务器是在哪里写的呢?

内嵌tomcat

  • 当前我们做的SpringBoot入门案例勾选了Spirng-web的功能,并且导入了对应的starter。
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

​如果我们要做Web程序,那么肯定离不开使用Web服务器,所以SpringBoot直接在程序中内嵌了一个tomcat服务器。由于这个功能不属于程序的主体功能,可用可不用,于是SpringBoot将其定位成辅助功能,这个辅助功能帮助我们开发者减少了许多设置性工作

围绕内置的tomcat服务器来研究几个问题

  1. 这个服务器在什么位置定义的
  2. 这个服务器是怎么运行的
  3. 这个服务器如果想换怎么换?(一般不换)

内嵌Tomcat定义位置

  • 当我们不开发Web程序时肯定用不到这个Tomcat服务器,而开发又要依赖这个服务器,所以SpringBoot将这个Tomcat服务器伴随着web的起步依赖导入进来

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  • 打开查看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
<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
    <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运行原理

​ Tomcat服务器是一款软件,而且是一款使用java语言开发的软件,既然是java开发的,运行的时候肯定符合java程序运行的原理。而java程序运行靠的是对象,所以在SpringBoot中,Tomcat服务器以对象的形式在Spring容器中运行,具体运行的就是前面提到的Tomcat内嵌核心

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.52</version>
<scope>compile</scope>
</dependency>
</dependencies>

  • 既然是对象,那么将这个对象从Spring容器中去掉就没有Web服务器的功能了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <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>
    </dependencies>

使用maven的排除依赖去掉了使用tomcat的starter。这下容器中就没有这个对象了,重新启动程序可以观察到程序运行了,但是并没有像之前那样运行后会等着用户发请求,而是直接停掉了,就是这个原因了。

更换内嵌Tomcat
SpringBoot提供了3款内置的服务器供我们更换

  • tomcat(默认):apache出品,粉丝多,应用面广,负载了若干较重的组件
  • jetty:更轻量级,负载性能远不及tomcat
  • undertow:负载性能勉强跑赢tomcat

想用哪个,加个坐标就OK。前提是把tomcat排除掉,因为tomcat是默认加载的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>

​ 现在就已经成功替换了web服务器,核心思想就是用什么加入对应坐标就可以了。如果有starter,优先使用starter。

总结

  1. 内嵌Tomcat服务器是SpringBoot辅助功能之一
  2. 内嵌Tomcat工作原理是将Tomcat服务器作为对象运行,并将该对象交给Spring容器管理
  3. 变更内嵌服务器思想是去除现有服务器,添加全新的服务器启动器

SpringBoot基础配置

​ 通过入门案例,我们能够感知到一个信息,SpringBoot没有具体的功能,它的作用是辅助加快Spring程序的开发效率。SpringBoot有各种默认配置,这些默认配置都是为了简化Spring程序的开发而设计的,接下来我们来解决不想使用SpringBoot提供的默认配置的情况。

属性配置

​ SpringBoot通过配置文件application.properties就可以修改默认的配置,当前访问tomcat的默认端口是8080,如果我们想修改为80端口,该如何操作呢?

  • 找到resource目录下的application.properties文件,尝试性的更改端口配置,当输入port时就发现IDEA给我们了许多提示

  • ​根据提示敲回车,输入80端口就完成了端口号的修改

    1
    server.port=80

从这个案例可以得到如下三个信息

  1. SpringBoot程序可以在application.properties文件中进行属性配置
  2. application.properties文件中只要输入要配置的属性关键字就可以根据提示进行设置
  3. SpringBoot将配置信息集中在一个文件中写,不管你是服务器的配置,还是数据库的配置,总之都写在一起,逃离一个项目十几种配置文件格式的尴尬局面
  • 做完了端口的配置,趁热打铁,再做几个配置,目前项目启动时会显示一些日志信息,就来改一改这里面的一些设置。

关闭运行日志图表(banner)

1
spring.main.banner-mode=off

设置运行日志的显示级别

1
logging.level.root=debug
  • 能够发现,这样搞配置比之前方便的多,之前不同的技术有专用的配置文件,文件的格式也不统一,而现在只需要在一个配置文件中统一格式进行配置。

该文件中能配置哪些属性?

  • 从SpringBoot官方文档中,打开查看附录中的Application Properties就可以查看所有的配置项。

这些配置项与什么技术有关?

  • 这些配置项与SpringBoot的starter有关,我们引入了哪些技术的starter,就可以配置哪些属性。

所有的Starter都会依赖spring-boot-starter,该起步器是SpringBoot的基础起步器,里面定义了SpringBoot相关的基础配置

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>

配置文件分类

现在能够在properties格式的文件下进行SpringBoot相关的配置了,但是有的人认为properties格式的配置在一些场景下会很繁琐,如一堆配置有相同的前缀,每次配置都要写一遍。

SpringBoot除了支持properties格式的配置文件,还支持另外两种格式的配置文件。分别如下:

  • properties格式
  • yml格式
  • yaml格式
1
server.port=80
1
2
server:
port: 81
1
2
server:
port: 82
  • yml格式和yaml格式是完全一样的,只是文件后缀不同,所以可以合并成一种格式
  • 以后yml格式使用较多,因为它的格式更简单,更易读

思考

现在我们已经知道使用三种格式都可以做配置了,那么问题来了,万一我三个都写了,他们三个谁说了算呢?

配置文件优先级

​三个文件如果共存的话,谁生效说的就是配置文件加载的优先级别。这种情况很少出现,但是这个知识还是可以学习一下的。我们就让三个配置文件书写同样的信息,比如都配置端口,然后我们让每个文件配置的端口号都不一样,最后启动程序后看启动端口是多少就知道谁的加载优先级比较高了。

1
server.port=80
1
2
server:
port: 81
1
2
server:
port: 82

​启动后发现目前的启动端口为80,把80对应的文件删除掉,然后再启动,现在端口又改成了81。现在我们就已经知道了3个文件的加载优先顺序是什么

application.properties > application.yml > application.yaml

最后我们把配置文件内容给修改一下

1
2
server.port=80
spring.main.banner-mode=off
1
2
3
4
5
server:
port: 81
logging:
level:
root: debug
1
2
server:
port: 82

​此时发现不仅端口生效了,最终显示80,同时其他两条配置也生效了,看来每个配置文件中的项都会生效,只不过如果多个配置文件中有相同类型的配置会优先级高的文件覆盖优先级的文件中的配置。如果配置项不同的话,那所有的配置项都会生效。

总结

  1. 配置文件间的加载优先级 properties(最高)> yml > yaml(最低)
  2. 不同配置文件中相同配置按照加载优先级相互覆盖,不同配置文件中不同配置全部保留

总结

  1. 指定SpringBoot配置文件

    • Setting → Project Structure → Facets
    • 选中对应项目/工程
    • Customize Spring Boot
    • 选择配置文件

yaml文件

SpringBoot的配置主要使用yml结尾的这种文件格式,并且在书写时可以通过提示的形式加载正确的格式。但是这种文件还是有严格的书写格式要求的。下面就来说一下具体的语法格式。

YAML(YAML Ain't Markup Language),一种数据序列化格式。具有容易阅读、容易与脚本语言交互、以数据为核心,重数据轻格式的特点。常见的文件扩展名有两种:

  • .yml格式(主流)
  • .yaml格式

对于文件自身在书写时,具有严格的语法格式要求,具体如下:

  1. 大小写敏感
  2. 属性层级关系使用多行描述,每行结尾使用冒号结束
  3. 使用缩进表示层级关系,同层级左侧对齐,不允许使用Tab键
  4. 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
  5. #号 表示注释

核心规则:数据前面要加空格与冒号隔开

​下面列出常见的数据书写格式,熟悉一下

1
2
3
4
5
6
7
8
boolean: TRUE  						#TRUE,true,True,FALSE,false,False均可
float: 3.14 #6.8523015e+5 #支持科学计数法
int: 123 #0b1010_0111_0100_1010_1110 #支持二进制、八进制、十六进制
null: ~ #使用~表示null
string: HelloWorld #字符串可以直接书写
string2: "Hello World" #可以使用双引号包裹特殊字符
date: 2018-02-17 #日期必须使用yyyy-MM-dd格式
datetime: 2018-02-17T15:02:31+08:00 #时间和日期之间使用T连接,最后使用+代表时区

此外,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
subject:
- Java
- 前端
- 大数据
enterprise:
name: itcast
age: 16
subject:
- Java
- 前端
- 大数据
likes: [王者荣耀,刺激战场] #数组书写缩略格式
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 } ] #对象数组缩略格式

总结

  1. yaml语法规则
    • 大小写敏感
    • 属性层级关系使用多行描述,每行结尾使用冒号结束
    • 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
    • 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
    • #号 表示注释
  2. 注意属性名冒号后面与数据之间有一个空格
  3. 字面值、对象数据格式、数组数据格式

思考
我们现在在文件中定义的数据都是给SpringBoot框架内部使用,都是内部定义好的,如果现在想配置一些数据在自己写的代码中使用,该如何操作呢?

yaml数据读取

对于yaml文件中的数据,可以想象成一个小型的数据库,里面保存着若干数据,每个数据有一个独立的名字,下面介绍3种读取数据的方法

读取单一数据

​ yaml中保存的单个数据,可以使用Spring中的注解直接读取,使用@Value可以读取单个数据,属性名引用方式:${一级属性名.二级属性名……}

使用@Value注解时,将该注解写在某一个指定的Spring管控的bean的属性名上方即可

总结

读取全部数据

可以使用value注解可以读取单一数据,但定义的数据量过大时,一个一个写非常麻烦,SpringBoot为我们提供了一个对象,可以将所有数据都封装到这一个对象,这个对象叫做Environment,使用自动装配注解可以将所有的yaml数据封装到这个对象中


数据封装到了Environment对象后,可以通过Environment接口提供的getProperties(String)方法获取属性值,参数填写属性名即可

读取对象数据

  • 读取对象数据时,使用单一数据读取书写比较繁琐,全数据封装又封装的太厉害了,每次拿数据还要一个一个的getProperties(String),总之用起来都不是很舒服。由于Java是一个面向对象的语言,很多情况下,我们会将一组数据封装成一个对象。SpringBoot也提供了可以将一组yaml对象数据封装一个Java对象的操作

  • 首先定义一个对象,并将该对象纳入Spring管控的范围,也就是定义成一个bean,然后使用注解@ConfigurationProperties指定该对象加载哪一组yaml中配置的信息。

​这个@ConfigurationProperties需要告诉他加载的数据前缀是什么,这样当前前缀下的所有属性就会封装到这个对象中。数据属性名要与对象的变量名一一对应,不然没法封装。其实以后如果你要定义一组数据自己使用,就可以先写一个对象,然后定义好属性,下面到配置中根据这个格式书写即可。

自定义的数据在yaml文件中书写时没有提示弹出,等到了原理篇再学习如何弹出提示

总结

  1. 使用@ConfigurationProperties注解绑定配置信息到封装类中
  2. 封装类需要定义为Spring管理的bean,否则无法进行属性注入

yaml文件中的数据引用

如果你在书写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
center:
dataDir: D:/usr/local/fire/data
tmpDir: D:/usr/local/fire/tmp
logDir: D:/usr/local/fire/log
msgDir: D:/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. 如果属性中出现特殊字符,可以使用双引号包裹起来作为字符解析

基于SpringBoot实现SSMP整合

SpringBoot之所以好用,就是它能方便快捷的整合其他技术,这一部分就来学习如何快捷方便的整合其他技术。这一章咱们学习如下技术的整合方式

  • 整合JUnit
  • 整合MyBatis
  • 整合MyBatis-Plus
  • 整合Druid

    整合JUnit

SpringBoot技术的定位用于简化开发,再具体点是简化Spring程序的开发。所以在整合任意技术的时候,如果你想直观感触到简化的效果,必须先知道使用非SpringBoot技术时对应的整合是如何做的,然后再看基于SpringBoot的整合是如何做的,才能比对出来简化在了哪里。

不使用SpringBoot技术时,Spring整合JUnit的制作方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//加载spring整合junit专用的类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//指定对应的配置信息
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
//注入你要测试的对象
@Autowired
private AccountService accountService;
// 添加测试注解
@Test
public void testGetById(){
//执行要测试的对象对应的方法
System.out.println(accountService.findById(2));
}
}

  • 核心代码是前两个注解,第一个注解@RunWith是设置Spring专用于测试的类运行器,简单说就是Spring程序执行程序有自己的一套独立的运行程序的方式,不能使用JUnit提供的类运行方式了,必须指定一下,但是格式是固定,第二个注解@ContextConfiguration是用来设置Spring核心配置文件或配置类的,简单说就是加载Spring的环境你要告诉Spring具体的环境配置是在哪里写的,虽然每次加载的文件都有可能不同,但是仔细想想,如果文件名是固定的,这个貌似也是一个固定格式。既然有可能是固定格式,那就有可能每次都写一样的东西,也是一个没有技术含量的内容书写

SpringBoot就将上述两条没有技术含量的内容书写进行开发简化,能走默认值的走默认值,能不写的就不写,具体格式如下

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class Springboot04JunitApplicationTests {
//注入你要测试的对象
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
//执行要测试的对象对应的方法
bookDao.save();
System.out.println("two...");
}
}

SpringBoot直接使用@SpringBootTest一个注解就解决了,而且没有参数。至于内部是怎么回事?和之前一样,只不过都走的默认值。通过默认值加载的配置类或配置文件是哪个?就是前面启动程序使用的引导类。也可以通过给@SpringBootTest注解添加classes属性来指定配置类。

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest(classes = Springboot04JunitApplication.class)
class Springboot04JunitApplicationTests {
//注入你要测试的对象
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
//执行要测试的对象对应的方法
bookDao.save();
System.out.println("two...");
}
}

温馨提示

使用SpringBoot整合JUnit需要保障导入test对应的starter,由于初始化项目时此项是默认导入的,所以此处没有提及,其实和之前学习的内容一样,用什么技术导入对应的starter即可。

​ **总结** 1. 导入测试对应的`starter` 2. 测试类使用`@SpringBootTest`修饰 3. 使用`自动装配`的形式添加要测试的对象 4. 测试类如果存在于引导类所在包或子包中无需指定引导类 5. 测试类如果不存在于引导类所在的包或子包中需要通过classes属性指定引导类 ### 整合MyBatis 接下来整合MyBatis。下面列举出原始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>
<!--1.导入mybatis与spring整合的jar包-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<!--导入spring操作数据库必选的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
  • Spring核心配置

添加核心配置类

1
2
3
4
5
@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")
public class SpringConfig {
}

  • MyBatis要交给Spring接管的bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //定义mybatis专用的配置类
    @Configuration
    public class MyBatisConfig {
    // 定义创建SqlSessionFactory对应的bean
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
    //SqlSessionFactoryBean是由mybatis-spring包提供的,专用于整合用的对象
    SqlSessionFactoryBean sfb = new SqlSessionFactoryBean();
    //设置数据源替代原始配置中的environments的配置
    sfb.setDataSource(dataSource);
    //设置类型别名替代原始配置中的typeAliases的配置
    sfb.setTypeAliasesPackage("com.itheima.domain");
    return sfb;
    }
    // 定义加载所有的映射配置
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    MapperScannerConfigurer msc = new MapperScannerConfigurer();
    msc.setBasePackage("com.itheima.dao");
    return msc;
    }

    }
  • 数据源对应的bean,此处使用Druid数据源

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;
}
}
  • 数据库连接信息(properties格式)

    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整合MyBaits格式

步骤一:创建模块时勾选要使用的技术,MyBatis,由于要操作数据库,还要勾选对应数据库

或者手工导入对应技术的starter,和对应数据库的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<!--1.导入对应的starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

步骤二:配置数据源相关信息

1
2
3
4
5
6
7
#2.配置相关信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db
username: root
password: root
  • 这就配置结束了,SpringBoot将可能出现的通用配置都给简化了。下面写Mybatis的程序运行需要的Dao(或者Mapper)就可以运行了
1
2
3
4
5
6
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 tbl_book where id = #{id}")
public Book getById(Integer id);
}
1
2
3
4
5
6
7
8
9
@SpringBootTest
class Springboot05MybatisApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
System.out.println(bookDao.getById(1));
}
}

​当前使用的SpringBoot版本是2.5.4,对应的坐标设置中Mysql驱动使用的是8x版本。当SpringBoot2.4.3(不含)版本之前会出现一个小BUG,就是MySQL驱动升级到8以后要求强制配置时区,如果不设置会出问题。解决方案很简单,驱动url上面添加上对应设置就行了

1
2
3
4
5
6
7
#2.配置相关信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root

​ 这里设置的UTC是全球标准时间,也可以理解为是英国时间,中国处在东八区,需要在这个基础上加上8小时,这样才能和中国地区的时间对应的,也可以修改配置不写UTC,写Asia/Shanghai也可以解决这个问题。

1
2
3
4
5
6
7
#2.配置相关信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai
username: root
password: root

​ 如果不想每次都设置这个东西,也可以去修改mysql中的配置文件mysql.ini,在mysqld项中添加default-time-zone=+8:00也可以解决这个问题。

此外在运行程序时还会给出一个提示,说数据库驱动过时的警告,根据提示修改配置即可,弃用com.mysql.jdbc.Driver,换用com.mysql.cj.jdbc.Driver。前面的例子中已经更换了驱动了,在此说明一下。

总结

  1. 整合操作需要勾选MyBatis技术,也就是导入MyBatis对应的starter
  2. 数据库连接相关信息转换成配置
  3. 数据库SQL映射需要添加@Mapper被容器识别到
  4. MySQL 8.X驱动强制要求设置时区
    • 修改url,添加serverTimezone设定
    • 修改MySQL数据库配置
  5. 驱动类过时,提醒更换为com.mysql.cj.jdbc.Driver

整合MyBatis-Plus

使用SpringBoot整合第三方技术的核心总结起来就两句话

  • 导入对应技术的starter坐标
  • 根据对应技术的要求做配置

​接下来在MyBatis的基础上升级一下,整合MyBaitsPlus(简称MP)

步骤一:导入对应的starter

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>

Mybatis-plus的起步依赖与之前使用的略有不同,这是第三方提供的启动器,之前使用的都是官方提供的
| starter所属 | 命名规则 | 示例 |
| —————- | —————————————————————————————- | ——————————————————————————- |
| 官方提供 | spring-boot-starter-技术名称 | spring-boot-starter-web
spring-boot-starter-test |
| 第三方提供 | 第三方技术名称-spring-boot-starter | druid-spring-boot-starter |
| 第三方提供 | 第三方技术名称-boot-starter(第三方技术名称过长,简化命名) | mybatis-plus-boot-starter |

没有办法在创建项目时通过勾选的形式添加MyBatis-Plus的起步依赖,因为Springboot的官网还未收录此坐标,所以需要手工添加坐标,如果换用阿里云的url创建项目可以找到对应坐标

步骤二:配置数据源相关信息

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db
username: root
password: root

步骤三:编写映射接口

1
2
3
@Mapper
public interface BookDao extends BaseMapper<Book> {
}

核心在于Dao接口继承了一个BaseMapper的接口,这个接口中帮助开发者预定了若干个常用的API接口,简化了通用API接口的开发工作。

目前数据库的表名定义规则是tbl_模块名称,为了能和实体类相对应,需要做一个配置,相关知识各位小伙伴可以到MyBatisPlus课程中去学习,此处仅给出解决方案。配置application.yml文件,添加如下配置即可,设置所有表名的通用前缀名

1
2
3
4
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_ #设置所有表的通用前缀名称为tbl_

总结

  1. 手工添加MyBatis-Plus对应的starter
  2. 数据层接口使用BaseMapper简化开发
  3. 需要使用的第三方技术无法通过勾选确定时,需要手工添加坐标

整合Druid

可以发现SpringBoot整合第三方技术的核心就是导入对应的starter坐标,然后根据对应技术的要求做配置。只需要一直强化这套思想即可。

前面整合Mybatis和MP时使用的数据源对象是SpringBoot默认的数据源对象,接下来我们手动指定一个数据源对象。

1
2
3
4
5
6
7
# 没有指定数据源时的配置
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。通过启动日志可以查看到对应的身影。

1
2
3
2021-11-29 09:39:15.202  INFO 12260 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-11-29 09:39:15.208 WARN 12260 --- [ main] com.zaxxer.hikari.util.DriverDataSource : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
2021-11-29 09:39:15.551 INFO 12260 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.

​ 上述信息中每一行都有HiKari的身影,如果需要更换数据源,其实只需要两步即可。

  1. 导入对应的技术坐标
  2. 配置使用指定的数据源类型

​下面就切换一下数据源对象

步骤一:导入对应的坐标(注意,是坐标,此处不是starter)

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
</dependencies>

步骤二:修改配置,在数据源配置中有一个type属性,专用于指定数据源类型

1
2
3
4
5
6
7
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource

这里又出现了新的问题:目前数据源配置格式是一个通用格式,不管使用什么数据源都能这么配置,若对数据源进行个性化配置,如配置数据源对应的连接数量,每个数据源技术对应的配置名称不可能完全一样,这时候该如何解决?

  • SpringBoot给出的解决方法是导入对应的starter,使用对应配置。

步骤一:导入对应的starter

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
</dependencies>

步骤二:修改配置

1
2
3
4
5
6
7
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root

可以发现相较于之前的配置,在datasource下面多了一个druid节点,这就是druid专用的配置节点,用来配置druid数据源的相关属性。

​与druid相关的配置超过200条以上,可以对druid数据源进行各种个性化配置

SSMP整合综合案例

可以通过SpringBoot快速整合各种技术,接下来通过一个小案例来回顾复习前面学到的知识。

image-20211129113447844

添加

image-20211129113522459

删除

image-20211129113550829

修改

image-20211129113610966

分页

image-20211129113628969

条件查询

image-20211129113650369

​ 整体案例中需要采用的技术如下,先了解一下,做到哪一个说哪一个

  1. 实体类开发————使用Lombok快速制作实体类
  2. Dao开发————整合MyBatisPlus,制作数据层测试
  3. Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类
  4. Controller开发————基于Restful开发,使用PostMan测试接口功能
  5. Controller开发————前后端开发协议制作
  6. 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理
    • 列表
    • 新增
    • 修改
    • 删除
    • 分页
    • 查询
  7. 项目异常处理
  8. 按条件查询————页面功能调整、Controller修正功能、Service修正功能

​ 可以看的出来,东西还是很多的,希望通过这个案例,各位小伙伴能够完成基础开发的技能训练。整体开发过程采用做一层测一层的形式进行,过程完整,战线较长,希望各位能跟进进度,完成这个小案例的制作。

模块创建

​ 对于这个案例如果按照企业开发的形式进行应该制作后台微服务,前后端分离的开发,但目前难度过高,所以简化一下只做单体服务器,一个服务器即充当后台服务调用,又负责前端页面展示,降低学习门槛。

下面创建一个新的模块,加载要使用的技术对应的starter,修改配置文件格式为yml格式,并把web访问端口先设置成80。

这里直接在创建时导入Web,mysql驱动,Mybatis模块

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</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>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
</dependencies>

1
2
server:
port: 80

实体类开发

  • 新建表:tbl_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
    -- ----------------------------
    -- Table structure for tbl_book
    -- ----------------------------
    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;

    -- ----------------------------
    -- Records of tbl_book
    -- ----------------------------
    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+');
  • 创建实体类:Book

    1
    2
    3
    4
    5
    6
    public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;
    }

数据层开发——基础CRUD

​ 数据层开发本次使用MyBatisPlus技术,数据源使用前面学习的Druid,学都学了都用上,课程中提供的是SpringBoot2的各种版本,而我在学习中使用的是SpringBoot3,所以会有些许不同

步骤一:导入MyBatisPlus与Druid对应的starter,当然mysql的驱动不能少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.14</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>

步骤二:配置数据库连接相关的数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 80

spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root

mybatis-plus:
global-config:
db-config:
table-prefix: tbl_

SpringBoot3更新了数据源的配置方式,需要指定type为Druid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 369640sh
type: com.alibaba.druid.pool.DruidDataSource

mybatis-plus:
global-config:
db-config:
table-prefix: tbl_

接下来分别提供使用Mybatis和MybatisPlus两种方式快速开发基础CURD

步骤三:创建BookDao接口
仿照查询操作自行添加增删改操作

1
2
3
4
5
@Mapper
public interface BookDao {
@Select("select * from tbl_book where id=#{id}")
Book getBookById(int id);
}

步骤四:创建测试类
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
public class BookDaoTest {
@Autowired
private BookDao bookDao;
@Test
void testGetById(){
Book book = bookDao.getBookById(1);
System.out.println(book);
}
}

步骤三:令Book接口继承BaseMapper

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
@SpringBootTest
public class BookDaoTest {
@Autowired
private BookDao bookDao;

@Test
void testSelectById(){
System.out.println(bookDao.selectById(1));
}
@Test
void testSave(){
Book book = new Book();
book.setType("测试222");
book.setName("测试222");
book.setDescription("测试222");
bookDao.insert(book);
}
@Test
void testDelete(){
bookDao.deleteById(51);
}
@Test
void testGetAll(){
bookDao.selectList(null);
}
}

注意
​ MP技术默认的主键生成策略为雪花算法,生成的主键ID长度较大,和目前的数据库设定规则不相符,在SpringBoot中执行测试会直接失败,在SpringBoot3中能够在数据库看到数据,但无法根据Id来获取数据。在application.yml中添加对应配置即可,具体如下:

1
2
3
4
5
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_ #设置表名通用前缀
id-type: auto #设置主键id字段的生成策略为参照数据库设定的策略,当前数据库设置id生成策略为自增

小技巧:查看MP运行信息

​ 在进行数据层测试的时候,因为基础的CRUD操作均由MP来提供,所以开发者不需要书写SQL语句了,但也因此对于程序中运行的SQL语句完全不知道了。这时候如果程序正常运行还好,如果报错了,这个时候就很崩溃,甚至都不知道从何下手,所以查看执行期运行的SQL语句就成为当务之急。

​ SpringBoot整合MP的时候充分考虑到了这点,通过配置的形式就可以查阅执行期SQL语句,配置如下

1
2
3
4
5
6
7
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_
id-type: auto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

再来看运行结果,此时就显示了运行期执行SQL的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c9a6717] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6ca30b8a] will not be managed by Spring
==> Preparing: SELECT id,type,name,description FROM tbl_book
==> Parameters:
<== Columns: id, type, name, description
<== Row: 1, 计算机理论, Spring实战 第5版, Spring入门经典教程,深入理解Spring原理技术内幕
<== Row: 2, 计算机理论, Spring 5核心原理与30个类手写实战, 十年沉淀之作,手写Spring精华思想
<== Row: 3, 计算机理论, Spring 5 设计模式, 深入Spring源码剖析Spring源码中蕴含的10大设计模式
<== Row: 4, 计算机理论, Spring MVC+MyBatis开发从入门到项目实战, 全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手
<== Row: 5, 计算机理论, 轻量级Java Web企业应用实战, 源码级剖析Spring框架,适合已掌握Java基础的读者
<== Row: 6, 计算机理论, Java核心技术 卷I 基础知识(原书第11版), Core Java 第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新
<== Row: 7, 计算机理论, 深入理解Java虚拟机, 5个维度全面剖析JVM,大厂面试知识点全覆盖
<== Row: 8, 计算机理论, Java编程思想(第4版), Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉
<== Row: 9, 计算机理论, 零基础学Java(全彩版), 零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术
<== Row: 10, 市场营销, 直播就该这么做:主播高效沟通实战指南, 李子柒、李佳琦、薇娅成长为网红的秘密都在书中
<== Row: 11, 市场营销, 直播销讲实战一本通, 和秋叶一起学系列网络营销书籍
<== Row: 12, 市场营销, 直播带货:淘宝、天猫直播从新手到高手, 一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+
<== Row: 13, 测试类型, 测试数据, 测试描述数据
<== Row: 14, 测试数据update, 测试数据update, 测试数据update
<== Row: 15, -----------------, 测试数据123, 测试数据123
<== Total: 15

​ 其中清晰的标注了当前执行的SQL语句是什么,携带了什么参数,对应的执行结果是什么,所有信息应有尽有。此处设置的是日志显示形式为控制台输出,当然还可以由更多的选择,根据需求切换即可

数据层开发——分页功能制作

温馨提示:

  • 这里如果使用的Mybatis-Plus版本是3.5.9+,那么想使用分页功能需要额外添加一个依赖管理和一个依赖,官方说明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-bom</artifactId>
    <version>3.5.11</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    </dependency>

​MP提供的分页操作API如下

1
2
3
4
5
6
7
8
9
10
@Test
void testGetPage(){
IPage page = new Page(2,5);
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描述了分页所有的信息。下面5个操作就是IPage对象中封装的所有信息了

1
2
3
4
5
6
7
8
9
10
@Test
void testGetPage(){
IPage page = new Page(2,5);
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()); //详细数据
}

此时就知道这些数据如何获取了,但当我们执行后却发现MP并没有为我们执行分页。这个要源于MP的内部机制。

  • 对于MySQL的分页操作使用limit关键字进行,但不是所有的数据库都使用limit关键字实现,这个时候MP为了制作的兼容性强,将分页操作设置为基础查询操作的升级版,可以理解为iphone6与iphone6S-PLUS的关系。

  • 基础操作中有查询全部的功能,而在这个基础上只需要升级一下(PLUS)就可以得到分页操作。所以MP将分页操作做成了一个开关,只需要在使用时去打开这个开关。配置拦截器就是打开这个开关

定义MP拦截器并将其设置为Spring管控的bean

1
2
3
4
5
6
7
8
9
@Configuration
public class MPConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}

数据层开发——条件查询功能制作

除了分页功能,MP还提供了强大的条件查询功能。以往我们写条件查询要自己动态拼写复杂的SQL语句,现在MP将这些操作制作成API接口,调用一个又一个的方法就可以实现各种套件的拼装。这里只简单普及一下基本格式

下面的操作就是执行一个模糊匹配对应的操作,由like条件书写变为了like方法的调用

1
2
3
4
5
6
7
@Test
void testGetBy(){
// select * from t_book where name like '%Spring%'
QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name","Spring");
bookDao.selectList(queryWrapper);
}

​ 第一句QueryWrapper对象是用于封装查询条件的对象,该对象可以动态使用API调用的方法来添加条件,最终转化成对应的SQL语句。第二句就是一个条件了,需要什么条件,使用QueryWapper对象直接调用对应操作即可。比如做大于、小于关系,就可以使用lt或gt方法,等于使用eq方法,等等……

这组API使用方式较简单,但是属性名一旦写错,编译器将无法检查,运行时才会报错,非常不友好,所以MP针对字段进行功能升级,支持Lambda表达式,同时将QueryWrapper对象升级为LambdaQueryWrapper对象

1
2
3
4
5
6
7
@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
@Test
void testGetBy2(){
String name = "1";
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
//if(name != null) lqw.like(Book::getName,name); //方式一:JAVA代码控制
// 拥有三个参数,第一个参数控制条件判断,第二个参数是字段名,第三个参数是字段值
lqw.like(name != null,Book::getName,name); //方式二:API接口提供控制开关
bookDao.selectList(lqw);
}

其实第三种只是个格式,没有本质上的区别,根据需求选择使用

业务层开发

​ 数据层开发告一段落,下面进行业务层开发,其实标准业务层开发很多初学者认为就是调用数据层,这个理解是没有大问题的,但更精准的说法应该是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用。有什么差别呢?目标是为了组织出符合需求的业务逻辑功能,至于调不调用数据层根据需求来决定,有需求就调用,没有需求就不调用。

​一个常识性的知识:

  • 业务层的方法名定义一定要与业务有关,例如登录操作

    1
    login(String username,String password);
  • 数据层的方法名定义一定与业务无关,是一定,不是可能,也不是有可能,例如根据用户名密码查询

    1
    selectByUserNameAndPassword(String username,String password);

在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。比如一个人制作数据层,他就可以不知道业务是什么样子,拿到的需求文档要求可能是这样的

1
2
3
接口:传入用户名与密码字段,查询出对应结果,结果是单条数据
接口:传入ID字段,查询出对应结果,结果是单条数据
接口:传入离职字段,查询出对应结果,结果是多条数据

但是进行业务功能开发的人,拿到的需求文档要求差别就很大

1
接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,8到24位,对喵喵喵字段做特殊字符校验,不允许存在空格,查询结果为对象。如果为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
38
@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 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
@SpringBootTest
public class BookServiceTest {
@Autowired
private BookService bookService;

@Test
void testGetById(){
System.out.println(bookService.getById(4));
}
@Test
void testSave(){
Book book = new Book();
book.setType("测试数据123");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.save(book);
}
@Test
void testUpdate(){
Book book = new Book();
book.setId(17);
book.setType("-----------------");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.update(book);
}
@Test
void testDelete(){
bookService.delete(18);
}

@Test
void testGetAll(){
bookService.getAll();
}

}

业务层快速开发

​MP不仅提供了数据层快速开发方案,业务层MP也给了一个通用接口,其实就是一个封装+继承的思想,代码给出,实际开发需要慎用

接口前面带上I,表示这是一个接口,而不是一个类,这是大多数公司的规范,不强求

1
2
3
public interface IBookService extends IService<Book> {
//添加非通用操作API接口
}

业务层接口实现类快速开发,关注继承的类需要传入两个泛型,一个是数据层接口,另一个是实体类

1
2
3
4
5
6
@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
@Autowired
private BookDao bookDao;
//添加非通用操作API
}

测试类:

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
@SpringBootTest
public class BookServiceTest {
@Autowired
private IBookService bookService;

@Test
void testGetById(){
System.out.println(bookService.getById(4));
}
@Test
void testSave(){
Book book = new Book();
book.setType("测试数据123");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.save(book);
}
@Test
void testUpdate(){
Book book = new Book();
book.setId(17);
book.setType("-----------------");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.updateById(book);
}
@Test
void testDelete(){
bookService.removeById(18);
}

@Test
void testGetAll(){
bookService.list();
}

@Test
void testGetPage(){
IPage<Book> page = new Page<Book>(2,5);
bookService.page(page);
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提供的功能不足以支撑你的使用需要,只需要在原始基础上定义新的方法或者对旧的方法进行重载即可,但是不要和已有的API接口名冲突。

表现层开发

  • 前面搞的都是基础工作,现在开始表现层的处理,表现层的开发基于Restful风格,功能测试使用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
@RestController
@RequestMapping("/books")
public class BookController2 {

@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.delete(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);
}
}

​在使用Postman测试时关注提交类型,对应上即可,不然就会报405的错误码

普通GET请求

POST请求传递json数据,后台实用@RequestBody接收数据

GET请求传递路径变量,后台实用@PathVariable接收数据

表现层消息一致性处理

​ 目前我们通过Postman测试后业务层接口功能时通的,但是这样的结果给到前端开发者会出现一个小问题。不同的操作结果所展示的数据格式差异化严重

需求:通过Postman测试时会发现,如果调用不同的方法,此时返回的结果类型是不同的,这样的结果会导致前端在处理时数据格式差异化严重,所以需要统一处理。


1
true
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": "十年沉淀之作"
    }
]

  • 因为不同操作的返回数据格式不同,前端不好对数据进行统一的处理,必须将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,这也称为前后端数据协议
1
2
3
4
5
@Data
public class Result {
private Boolean flag;
private Object data;
}

​ 其中flag用于标识操作是否成功,data用于封装操作数据,现在的数据格式就变了

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
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
@RestController
@RequestMapping("/books")
public class BookController {

@Autowired
private IBookService bookService;

@GetMapping
public Result getAll(){
return new Result(true,bookService.list());
}

@PostMapping
public Result save(@RequestBody Book book){
return new Result(bookService.save(book));
}

@PutMapping
public Result update(@RequestBody Book book){
return new Result(bookService.modify(book));
}

@DeleteMapping("{id}")
public Result delete(@PathVariable Integer id){
return new Result(bookService.delete(id));
}

@GetMapping("{id}")
public Result getById(@PathVariable Integer id){
return new Result(true,bookService.getById(id));
}

@GetMapping("{currentPage}/{pageSize}")
public Result getPage(@PathVariable int currentPage, @PathVariable int pageSize){
return new Result(true,bookService.getPage(currentPage,pageSize));
}
}

此时不管执行哪种操作,返回的数据格式都一样了

前后端联通性测试

  • 此时后端的表现层接口开发完毕,就可以进行前端的开发了,将前端人员开发的页面保存到lresources目录下的static目录中,建议执行maven的clean生命周期,避免缓存的问题出现。
  • 导入前端文件后,重新启动项目就可以在浏览器中搜索localhost:8080/pages/books.html来查看前端页面了

  • 在进行具体的功能开发之前,先做联通性的测试,通过页面发送异步提交(axios),这一步调试通过后再进行进一步的功能开发,先打开pages/books.html页面,编辑钩子函数和getAll方法

    1
    2
    3
    4
    5
    //钩子函数,VUE对象初始化完成后自动执行
    created() {
    // 调用查询结构数据的函数
    this.getAll();
    }
    1
    2
    3
    4
    5
    6
    //列表
    getAll() {
    axios.get("/books").then((res)=>{
    console.log(res.data);
    });
    },

    此时刷新页面,在控制台中就可以看到数据了

只要后台代码能够正常工作,前端能够在日志中接收到数据,就证明前后端是通的,也就可以进行下一步的功能开发了

页面基础功能开发

列表功能(非分页版)

需求:列表功能主要操作就是加载完数据,将数据展示到页面上

此处利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可
页面数据模型定义

1
2
3
4
data:{
dataList: [],//当前页要展示的列表数据
...
},
1
2
3
4
5
6
//列表
getAll() {
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
},

此时页面加载时就可以获取到数据,并且由VUE将数据展示到页面上了

添加功能

需求:添加功能主要操作是打开一个弹窗,将数据录入到弹窗中,点击弹窗的保存按钮,将数据保存到数据库中,并关闭弹窗,重新加载数据列表

  • 此处以切换弹窗状态为隐藏或显示来实现弹窗的打开和关闭,页面加载时,设置弹窗为不可显示

    1
    2
    3
    4
    data:{
    dialogFormVisible: false,//添加表单是否可见
    ...
    },
  • 可以看到页面的新建按钮关联了handleCreate()方法,点击按钮时,会调用该方法,因此只要在该方法中设置弹窗可见即可

    1
    2
    3
    4
    5
    6
    7
    <div class="filter-container">
    <el-input placeholder="图书类别" style="width: 200px;" class="filter-item"></el-input>
    <el-input placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input>
    <el-input placeholder="图书描述" 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>
    1
    2
    3
    4
    //弹出添加窗口
    handleCreate() {
    this.dialogFormVisible = true;
    },

由于每次添加数据都是使用同一个弹窗录入数据,需要在每次操作之前需要先清理掉上次弹窗中的操作痕迹

1
2
3
4
//重置表单
resetForm() {
this.formData = {};
},
1
2
3
4
5
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},

此时准备工作已经完成,可以调用后台来完成添加操作了

  • 通过查看代码,发现确定和取消都绑定了相应的方法,针对不同按钮编辑不同方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //添加
    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();
    });
    },
    1
    2
    3
    4
    5
    //取消
    cancel(){
    this.dialogFormVisible = false;
    this.$message.info("操作取消");
    },
删除功能

需求:模仿添加操作来制作删除功能

删除操作

  • 发现删除操作绑定的方法是handleDelete(row),这里row是封装了当前行的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 删除
    handleDelete(row) {
    axios.delete("/books/"+row.id).then((res)=>{
    if(res.data.flag){
    this.$message.success("删除成功");
    }else{
    this.$message.error("删除失败");
    }
    }).finally(()=>{
    this.getAll();
    });
    },
  • 给删除操作添加上提示信息,以免用户误操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 删除
    // row是传递的这一整行的数据
    handleDelete(row) {
    //1.弹出提示框
    this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
    type:'info'
    }).then(()=>{
    //2.做删除业务
    axios.delete("/books/"+row.id).then((res)=>{
    if(res.data.flag){
    this.$message.success("删除成功");
    }else{
    this.$message.error("删除失败");
    }
    }).finally(()=>{
    this.getAll();
    });
    }).catch(()=>{
    //3.取消删除
    this.$message.info("取消删除操作");
    });
    },
修改功能

​修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:

  1. 页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都要有弹窗
  2. 弹出的窗口中需要加载待修改的数据,而数据需要通过查询得到,这一点与查询相同,都要查数据
  3. 查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
  4. 修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都要传递数据
  • 首先为修改操作弹出一个带有数据的弹窗
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //弹出编辑窗口
    handleUpdate(row) {
    axios.get("/books/"+row.id).then((res)=>{
    if(res.data.flag&&res.data.data){
    //展示弹层,加载数据
    this.formData = res.data.data;
    this.dialogFormVisible4Edit = true;
    }else{
    this.$message.error("数据同步失败,自动刷新");
    }
    });
    },
  • 当我们修改弹窗中的数据后,点击确定按钮,将数据发送到后台进行修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //修改
    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();
    });
    },

异常处理

目前的基本功能制作大致完成了正常使用的情况,但是如果这个程序出现Bug导致系统抛出一个异常,此时就会出现一些问题,这里我们在后台模拟抛出一个异常然后查看前端接收到的数据。

手动在程序中抛出一个异常,查看前端接收到的数据

1
2
3
4
5
6
7
@GetMapping
public Result getAll(){
if(true){
throw new RuntimeException("服务器异常");
}
return new Result(true,bookService.list());
}

  • 此时会发现这又是一个数据格式,与之前的格式不同。

​因此我们不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理,在返回错误数据格式的时候,添加一个字段,用来返回错误消息

1
2
3
4
5
6
@Data
public class Result{
private Boolean flag;
private Object data;
private String msg; //用于封装消息
}

同样要添加好对应的构造函数,因为需要封装错误信息时一般都是操作失败,此时数据也没有意义了,所以只需要添加一个包含错误信息字段的构造函数即可

1
2
3
4
public Result(String errorMessage) {
this.flag = false;
this.errorMessage = errorMessage;
}

​然后在表现层做统一的异常处理,使用SpringMVC提供的@RestControllerAdvice@ExceptionHandler两个注解做统一的异常处理

1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)
public R doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
ex.printStackTrace();
// 为处理异常添加一个字段,返回一个错误消息
return new R("系统错误,请稍后再试!");
}
}

此时测试后端程序可以看到返回的内容,格式已经符合要求

​当页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果false,就读取后台传递的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//添加
handleAdd () {
//发送ajax请求
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();
});
},

页面功能开发

分页功能

需求:实现页面数据分页展示

  • 使用el分页组件来完成分页展示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--分页组件-->
    <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
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
});
},
  • 此时不着急显示数据,可以在网页上查看请求的是否为设置的分页参数
  • 现在来进行页面数据的显示以及分页组件数据的绑定,​页面根据分页操作结果读取对应数据,并进行数据模型绑定
1
2
3
4
5
6
7
8
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
this.pagination.total = res.data.data.total;
this.pagination.currentPage = res.data.data.current;
this.pagination.pagesize = res.data.data.size;
this.dataList = res.data.data.records;
});
},

​对切换页码操作设置调用当前分页操作

1
2
3
4
5
//切换页码
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);
}

这样做只是一种简单的补救性方案,如果某一时刻同时删除大量数据,依旧会出现问题,应当根据不同情况来做相应处理

条件查询功能
  • 此时页面的大部分功能已经完成,接下来来完成分页查询操作,目前查询条件还无法添加,当输入数据时会直接显示undefined

条件查询分析:条件查询可以理解为分页查询的时候除了携带分页数据再多带几个查询条件的查询。比较一下不带条件的分页查询与带条件的分页查询差别之处

  • 页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递2个分页相关的数据转换成2个分页数据加若干个条件
  • 后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是null,现在换成具体条件,差别不大
  • 查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略

    经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别

    页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装url参数的形式传递

    页面封装查询条件字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pagination: {		
    //分页相关模型数据
    currentPage: 1, //当前页码
    pageSize:10, //每页显示的记录数
    total:0, //总记录数
    name: "",
    type: "",
    description: ""
    },

    页面添加查询条件字段对应的数据模型绑定名称

    1
    2
    3
    4
    5
    6
    7
    <div class="filter-container">
    <el-input placeholder="图书类别" v-model="pagination.type" class="filter-item"/>
    <el-input placeholder="图书名称" v-model="pagination.name" class="filter-item"/>
    <el-input placeholder="图书描述" v-model="pagination.description" class="filter-item"/>
    <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
    getAll() {
    //1.获取查询条件,拼接查询条件
    param = "?name="+this.pagination.name;
    param += "&type="+this.pagination.type;
    param += "&description="+this.pagination.description;
    console.log("-----------------"+ param);
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => {
    this.dataList = res.data.data.records;
    });
    },

    后台代码中定义实体类封查询条件

    1
    2
    3
    4
    5
    6
    @GetMapping("{currentPage}/{pageSize}")
    public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) {
    System.out.println("参数=====>"+book);
    IPage<Book> pageBook = bookService.getPage(currentPage,pageSize);
    return new R(null != pageBook ,pageBook);
    }

    对应业务层接口与实现类进行修正

    1
    2
    3
    public interface IBookService extends IService<Book> {
    IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Service
    public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
    public IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook){
    IPage page = new Page(currentPage,pageSize);
    LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
    lqw.like(Strings.isNotEmpty(queryBook.getName()),Book::getName,queryBook.getName());
    lqw.like(Strings.isNotEmpty(queryBook.getType()),Book::getType,queryBook.getType());
    lqw.like(Strings.isNotEmpty(queryBook.getDescription()),Book::getDescription,queryBook.getDescription());
    return bookDao.selectPage(page,lqw);
    }
    }

    页面回显数据

1
2
3
4
5
6
7
8
9
10
11
12
13
getAll() {
//1.获取查询条件,拼接查询条件
param = "?name="+this.pagination.name;
param += "&type="+this.pagination.type;
param += "&description="+this.pagination.description;
console.log("-----------------"+ param);
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => {
this.pagination.total = res.data.data.total;
this.pagination.currentPage = res.data.data.current;
this.pagination.pagesize = res.data.data.size;
this.dataList = res.data.data.records;
});
},

基础篇总结

创建SpringBoot项目

  • SpringBoot项目拥有四种创建方式
    1. IDEA中直接勾选创建SpringBoot项目
    2. 在官网创建好后下载到本地
    3. 阿里提供的工程创建地址
    4. 手动创建工程并导入相关SpringBoot依赖

前三种创建方式都需要联网进行,而第四种不需要联网,但前提是在maven仓库中有需要的依赖,即之前联网下载过该依赖

SpringBoot简化操作

  • parent:通过继承一个SpringBoot父类,来统一管理各种坐标的版本,避免不同坐标之间的版本冲突
  • starter:SpringBoot提供的起步器,在该起步器中已经定义了各种需要的依赖。
  • 引导类:这是SpringBoot项目的入口,在该类运行后会直接创建一个Spring容器,并启动SpringBoot项目
  • 内嵌Tomcat:无需配置,只需要添加web的起步器,便可以在运行引导类时启动Tomcat,除Tomcat以外,SpringBoot还提供了两款内置服务器,可以通过排除Tomcat依赖,然后引入 jettyundertow依赖,来启动内嵌服务器
    • Tomcat(默认):apache出品,粉丝多,应用面广,负载了若干较重的组件
    • jetty:更轻量级,负载性能远不及tomcat
    • undertow:负载性能勉强跑赢tomcat

SpringBoot基础配置

  • 属性配置:SpringBoot提供了各种默认配置,这些默认配置可以通过配置文件来修改,比如数据库连接信息,端口号,日志级别等等,可以在SpringBoot官方文档中查看所有配置项
  • 配置文件:SpringBoot提供了三种格式的配置文件分别是:
    • properties
    • yml
    • yaml

其中yml和yaml格式完全相同,只是后缀不同,在配置SpringBoot时使用yml格式较多,因为该格式更简单直接

  • 配置优先级:即文件加载的优先级别,相同配置下优先加载高优先级的配置,优先级顺序:application.properties > application.yml > application.yaml

如果配置多种格式的配置文件,那么相同的配置项会根据加载优先级进行覆盖,不同配置文件中的不同配置会全部保留

  • 读取配置文件中的数据
    • 读取少量数据:直接通过@Value配合${一级属性名.二级属性名……}读取
    • 读取大量数据:SpringBoot提供了一个对象,可以通过自动装配将所有数据封装到该对象中
      • 通过Autowired注解来将数据装配到一个Environment类中
      • 通过Environment接口提供的getProperties(String)方法获取数据,参数填写属性名
    • 读取对象数据:上面两种读取方式的获取范围都不合适,SpringBoot提供了@ConfigurationProperties注解来快速读取,只需要在该注解的prefix属性中填写数据前缀即可
  • 自定义的属性可能IDEA不会自动提示
  • 直接获取对象数据的前提是该类在Spring容器中
  • 配置文件中的数据引用:在书写配置文件中如果多个文件中都有相同的文件前缀,则可以将相同的前缀提取出来,下面再对其进行引用,如果属性中有特殊字符则需要使用双引号包裹起来。

    SpringBoot运维实用篇

实用篇是在基础篇的基础上,补全SpringBoot的知识图谱。比如在基础篇中学习了yaml的语法格式,但是具体写yaml文件的时候还有很多实用开发过程中的坑,这些在实用篇中都要进行学习。

实用篇共分为两块内容,分别是运维实用篇开发实用篇。两个阶段的划分是为了更好的将同类知识点进行归类,帮助学习者找到知识之间的关联性,这样有助于知识的记忆存储转换,经过一系列的知识反复出现与强化练习,将临时记忆转换成永久性记忆。做课程嘛,不能仅以讲完为目标,要以学习者的学习收获为目标,这也是我这么多年教学秉承的基本理念。

运维使用pianzhong

  • SpringBoot程序的打包与运行
  • 配置高级
  • 多环境开发
  • 日志

SpringBoot程序的打包与运行

我们现在都是在IDEA中对代码进行书写并运行,但在实际开发完成后,制作的程序并不是直接在自己电脑的IDEA中运行,而是运行在专用的服务器上,


​想要将我们准备好的程序运行在服务器上,这就需要将准备好的程序先组织成一个文件,然后把文件传输到服务器上。这个操作包含两个过程,一个是打包的过程,另一个是运行的过程。打包指的是将程序转换成一个可执行的文件,运行指的是不依赖开发环境执行打包产生的文件

企业项目上线为了保障环境适配性会采用下面流程发布项目,这里不讨论此过程。

  1. 开发部门使用GitSVN等版本控制工具上传工程到版本服务器
  2. 服务器使用版本控制工具下载工程
  3. 服务器上使用Maven工具在当前真机环境下重新构建项目
  4. 启动服务器

程序打包

SpringBoot程序是基于Maven创建的,在Maven中提供有打包的指令,叫做package。本操作可以在Idea环境下执行。

1
mvn package

打包后会产生一个与工程名类似的jar文件,其名称由模块名+版本号+.jar组成。

注意:

  • SpringBoot工程下执行package指令生成的jar包会将所有使用到的依赖库打包到jar文件中,所以可以直接运行
  • Maven自身的package指令生成的jar包,需要先将依赖库添加到classpath中,才能运行

程序运行

运行jar包的前提是计算机安装了java的JDK环境,因为程序执行的是java指令。

程序包打好以后,就可以直接执行了。在程序包所在路径下,执行指令。

1
java -jar 工程包名.jar

执行程序打包指令后,程序正常运行,与在Idea下执行程序没有区别。

这段配置在SpringBoot工程的pom.xml文件中,它在普通Maven项目的基础上扩展了功能,所以在SpringBoot环境下打包的项目可以直接运行。

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>

打包问题

前面提到SpringBoot环境下打的包和普通Maven打的包不一样,下面来具体分析一下哪里不同。

  • 直接运行Maven下载的jar包,会出现以下错误

  • 之前在项目中我们也经常导入各种jar包,如Mysql的驱动jar包,使用前面学习的指令java -jar xxx.jar运行,也会出现上面的错误,但使用SpringBoot打包的jar包,就能正常运行。

  • 前面就提到了,这里的配置是SpringBoot打包的关键
    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>

开启这段配置和注释掉这段配置分别执行两次打包,然后观察两次打包后的程序包的差别,共有3处比较明显的特征

  • 打包后文件的大小不同
  • 打包后所包含的内容不同
  • 打包程序中个别文件内容不同
  1. 文件大小不同。带有配置时打包生成的程序包大小如下:

​这里带有配置的程序包体积比不带配置的大了30倍,接下来看看里面内容的差异。

可以看到两个文件内容也不一样,打开带配置的文件中的BOOT-INF目录下的classes目录,可以看到这个目录下的内容和不带配置的程序包的内容一模一样,这说明带配置的程序包中包含了不带配置的程序包,除此之外还多了许多东西。

回到BOOT-INF目录下,打开lib目录,发现里面有着各种jar包

这些jar文件都是制作这个工程时导入的坐标对应的文件。这就是SpringBoot生成的JAR包能够独立运行的原因,这样可以不依赖程序包外部的任何资源来独立运行当前程序。

除此以外,在带配置的程序包的最外层目录包含一个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

​ 大文件中明显比小文件中多了几行信息,其中最后一行信息是Main-Class: org.springframework.boot.loader.JarLauncher。这句话的意思是如果使用java -jar执行此程序包,将执行Main-Class属性配置的类,而这个类恰巧就是前面看到的那个文件。原来SpringBoot打包程序中出现Spring框架的东西是为这里服务的。而这个org.springframework.boot.loader.JarLauncher类内部要查找Start-Class属性中配置的类,并执行对应的类。这个属性在当前配置中也存在,对应的就是程序的引导类类名。

SpringBoot制作的jar包能够直接运行的原因:

  1. SpringBoot程序添加配置后会打出一个特殊的包,包含Spring框架部分功能,原始工程内容,原始工程依赖的jar包
  2. 首先读取MANIFEST.MF文件中的Main-Class属性,用来标记执行java -jar命令后运行的类
  3. JarLauncher类执行时会找到Start-Class属性,也就是启动类类名
  4. 运行启动类时会运行当前工程的内容
  5. 运行当前工程时会使用依赖的jar包,从lib目录中查找

之前的报错就是由于打包时没有使用那段配置,打了一个普通的jar包,在MANIFEST.MF文件中也就没有了Main-Class对应的属性了,所以运行时提示找不到主清单属性。

  • 这一节对实际编程意义不大,但能更好的理解SpringBoot整体的实现过程。

命令行启动常见问题及解决方案

在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 "进程名称"

关于打包与运行程序其实还有一系列的配置和参数,这里只是最简单的默认打包运行方式,之后再继续深入学习。

配置(高级)

  • 在基础篇中简单学习了配置文件的分类以及如何读取配置文件的内容等基本使用,在实用篇则重点学习配置的使用。

临时属性设置

假设此时我们的程序包打好了,已经准备发布了。但是程序包打好以后,里面的配置就固定了,比如配置了服务器的端口是8080。如果在启动项目时,发现当前服务器上已经有应用启动起来并且占用了8080端口,这个时候难道要重新把打包好的程序修改一下吗?比如把打包好的程序启动端口改成80。

  • SpringBoot为此提供了灵活的配置方式,我们可以在启动时使用临时属性的方式快速修改某些配置,方法非常简单,只需要在启动时添加对应参数即可
    1
    java –jar springboot.jar –-server.port=80
  • 在命令输入完毕后,空一格,然后输入两个-号。下面按照属性名=属性值的形式添加对应参数就可以了。这里的格式不是yaml中的书写格式,当属性存在多级名称时,中间使用点分隔,和properties文件中的属性格式完全相同。

  • 如果要修改的属性不止一个,可以按照上述格式继续写,属性与属性之间使用空格分隔。

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

  • SpringBoot提供了14种配置方式,第三条指的是配置文件,第十一条说的就是使用命令行参数来临时配置。
  • 这14条配置的顺序就是SpringBoot配置的加载顺序,越在下面优先级越高。
  • 不需要死记硬背,只需要在配置的优先级出错时对照该表排查即可,例如在基础篇提到的user属性值系统属性的加载优先级高于配置文件的加载优先级,那么在配置文件配置的user属性值就会失效。

开发环境中使用临时属性

  • 前面使用临时属性是通过命令行来指定,但是上线的时候需要确保通过命令行输入的临时属性必须是正确的,所以这些属性配置值必须在开发环境中测试好(不通过直接修改配置文件来测试因为要用与上线相同的启动方式(命令行参数或环境变量)进行验证,确保这些动态配置的参数名、值、格式都正确,而不是仅仅依赖修改配置文件。)

打开SpringBoot引导类的运行界面,在里面找到配置项。其中Program arguments对应的位置就是添加临时属性的,可以加几个试试效果。

其实这里就是我们运行main方法时main方法的args参数

1
2
public static void main(String[] args) {
}

原来通过这个args就可以获取到参数。这个参数在程序中会被传递给run方法

1
2
3
public static void main(String[] args) {
SpringApplication.run(SSMPApplication.class,args);
}
  • args参数通过以上方法传递到程序中,言外之意,如果我们不传递这个参数,那么外部传递临时属性的入口就断开了,下面的调用方式,就无法使用外部临时属性了
    1
    2
    3
    public static void main(String[] args) {
    SpringApplication.run(SSMPApplication.class);
    }
  • 或者还可以使用如下格式来玩这个操作,就是将配置不写在配置文件中,直接写成一个字符串数组,传递给程序入口。当然,这种做法并没有什么实际开发意义。
1
2
3
4
5
public static void main(String[] args) {
String[] arg = new String[1];
arg[0] = "--server.port=8082";
SpringApplication.run(SSMPApplication.class, arg);
}

思考
现在可以通过临时属性在启动项目前临时更改配置了,但是新的问题又出来了。如果需要配置多个临时属性,假如有需求要一下配置20个,那么配置起来就非常麻烦了,这该如何解决?

配置文件分类

  • SpringBoot提供了配置文件临时属性的方式来对程序进行配置。这里再详细说说配置文件。我们前面使用的配置文件其实只是SpringBoot提供的4级配置文件中的其中一个级别。4个级别分别是:

    • 类路径下配置文件(一直使用的是这个,也就是resources目录中的application.yml文件)
    • 类路径下config目录下配置文件
    • 程序包所在目录中配置文件
    • 程序包所在目录中config目录下配置文件
  • 分为四种但说到底都是配置文件,功能都是配置属性,他们之前的差异就是文件存放的位置导致的优先级不同,这四个文件的加载优先顺序为:

  1. file :config/application.yml 【最高】
  2. file :application.yml
  3. classpath:config/application.yml
  4. classpath:application.yml(一直使用的就是这个) 【最低】
  • 设计这么多种配置文件的原因:
    • 场景A:作为一个开发者,做程序的时候为了方便自己写代码,配置的数据库肯定是自己本机的,咱们使用4这个级别,也就是之前一直用的application.yml。
    • 场景B:现在项目开发到了一个阶段,要联调测试了,连接的数据库是测试服务器的数据库,肯定要换一组配置吧。你可以选择把你之前的文件中的内容都改了,目前还不麻烦。
    • 场景C:测试完了,一切OK。可以继续往下写代码了,但这时就会发现原先的配置文件都被更改了,需要重新改回来,改来改去就非常繁琐。

​而使用上面3这个级别的配置文件就可以快速解决该问题。两个配置文件共存,因为config目录中的配置加载优先级更高,所以配置项如果和级别4里面的内容相同就覆盖了。

总结

  1. 配置文件分为4种
    • 工程路径config目录中配置文件:服务于运维经理整体调控
    • 工程路径配置文件:服务于运维人员配置涉密线上环境
    • 项目类路径config目录中配置文件:服务于项目经理整体调控
    • 项目类路径配置文件:服务于开发人员本机开发与测试
  2. 多层级配置文件间的属性采用叠加并覆盖的形式作用于程序

自定义配置文件

之前使用的配置文件都是application.yml,其实可以定义多个配置文件,使用其他名称的配置文件。比如2020年4月1日搞活动,走了一组配置,2020年5月1日活动取消,恢复原始配置,这个时候只需要重新更换一下配置文件就可以了。但是你总不能在原始配置文件上修改吧,不然搞完活动以后,活动的配置就留不下来了,不利于维护。

​自定义配置文件方式有如下两种:

方式一:使用临时属性设置配置文件名,注意仅仅是名称,不要带扩展名

方式二:使用临时属性设置配置文件路径,这个是全路径名

  • 也可以设置加载多个配置文件

方式一使用的属性是spring.config.name,方式二使用的是spring.config.location,这个一定要区别清楚。

温馨提示

现在研究的都是SpringBoot单体项目,就是单服务器版本。其实企业开发现在更多的是使用基于SpringCloud技术的多服务器项目。这种配置方式和现在学习的完全不一样,所有的服务器将不再设置自己的配置文件,而是通过配置中心获取配置,动态加载配置信息。为什么这样做?方便集中管理,之后再详细学习。

多环境开发

多环境:我们书写的程序大多需要放到服务器上运行,而每个电脑环境都不一样,这就是多环境。常见的多环境开发主要兼顾3种环境设置,开发环境——自己用的,测试环境——自己公司用的,生产环境——甲方用的。因为这是绝对不同的三台电脑,所以环境肯定有所不同,比如连接的数据库不一样,设置的访问端口不一样等等。

多环境开发(yaml单文件版)

  • 多环境开发就是针对不同的环境设置不同的配置属性。比如自己开发时,配置的端口如下:
1
2
server:
port: 80
  • ​在yaml中可以使用三个减号来分隔开不同环境的配置
    1
    2
    3
    4
    5
    server:
    port: 80
    ---
    server:
    port: 81
  • 通过为不同环境设置不同的profile名称,就可以在启动时指定使用哪一个环境的配置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    profiles: pro
    server:
    port: 80
    ---
    spring:
    profiles: dev
    server:
    port: 81
  • 设置默认启动哪个环境的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    spring:
    profiles:
    active: pro # 默认启动pro
    ---
    spring:
    profiles: pro
    server:
    port: 80
    ---
    spring:
    profiles: dev
    server:
    port: 81
  • 可以区分各种环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
profiles:
active: pro # 启动pro
---
spring:
profiles: pro
server:
port: 80
---
spring:
profiles: dev
server:
port: 81
---
spring:
profiles: test
server:
port: 82

总结

  1. 多环境开发需要设置若干种常用环境,例如开发、生产、测试环境
  2. yaml格式中设置多环境使用—-区分环境设置边界
  3. 每种环境的区别在于加载的配置属性不同
  4. 启用某种环境时需要指定启动时使用该环境

多环境开发(yaml多文件版)

  • 前面单文件开发是将所有配置都放在一个配置文件中,使用---来分隔,但当配置文件中有很多项配置时,明显将不同环境的配置项写在一个文件中是不合理的。于是就有了将一个配置文件拆分成多个配置文件的想法。拆分后,每个配置文件中写自己的配置,主配置文件中写清楚用哪一个配置文件就好了。

主配置文件

1
2
3
spring:
profiles:
active: pro # 启动pro

环境配置文件

1
2
server:
port: 80

环境配置文件因为每一个都是配置自己的项,所以连名字都不用写在配置文件里面了,直接使用文件名区分即可。

1
2
server:
port: 80
1
2
server:
port: 81

总结:

  • 配置文件的命名规则为:application-环境名.yml。
  • 主配置文件中设置公共配置(全局)
  • 环境分类配置文件中常用于设置冲突属性(局部)

多环境开发(properties多文件版)

  • SpringBoot最早期提供的配置文件格式是properties格式的,properties不支持单文件,这种格式的多环境配置作为了解即可

主配置文件

1
spring.profiles.active=pro

application-pro.properties

1
server.port=80

application-dev.properties

1
server.port=81

  • 文件的命名规则为:application-环境名.properties

多环境开发配置文件书写技巧

作为程序员在搞配置的时候往往处于一种分久必合合久必分的局面。开始先写一起,后来为了方便维护就拆分。对于多环境开发也是如此,下面给大家说一下如何基于多环境开发做配置独立管理,务必掌握。

准备工作

将所有的配置根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下

  • application-devDB.yml
  • application-devRedis.yml
  • application-devMVC.yml

使用
使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔,这样更加清晰直接,除了加载dev配置还加载了哪些环境的配置文件。

1
2
3
4
spring:
profiles:
active: dev
include: devDB,devRedis,devMVC

​当主环境dev与其他环境有相同属性时,主环境属性生效;其他环境中有相同属性时,最后加载的环境属性生效

改良
但是上面的设置也有一个问题,比如我要切换dev环境为pro时,include也要修改。因为include属性只能使用一次,这就比较麻烦了。SpringBoot从2.4版开始使用group属性替代include属性,降低了配置书写量。简单说就是我先写好,你爱用哪个用哪个。

1
2
3
4
5
6
7
spring:
profiles:
active: dev
group:
"dev": devDB,devRedis,devMVC
"pro": proDB,proRedis,proMVC
"test": testDB,testRedis,testMVC

多环境开发控制

  • 多环境开发在配置方面的内容基本就这些,最后一个冲突就是maven和SpringBoot同时设置多环境的话怎么搞。处理这个冲突问题前,需要先明确一个关系,究竟谁在多环境开发中其主导地位。也就是说如果现在都设置了多环境,谁的应该是保留下来的,另一个应该遵从相同的设置。

  • maven是做什么的?

    • 项目构建管理的,最终生成代码包的,
  • SpringBoot是干什么的?简化开发的。
  • 简化说明SpringBoot并不起主导作用,最终还是靠Maven管理整个工程,所以最终肯定是保留Maven的配置。大体思路:
    • 在Maven中设置好具体用哪些环境
    • 在SpringBoot中读取Maven设置的环境

maven中设置多环境(使用属性方式区分环境)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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
spring:
profiles:
active: @profile.active@

此处的@属性名@就是读取maven中配置的属性值的语法格式。

总结

  1. 当Maven与SpringBoot同时对多环境进行控制时,以Mavn为主,SpringBoot使用@..@占位符读取Maven对应的配置属性值
  2. 基于SpringBoot读取Maven配置属性的前提下,如果在Idea下测试工程时pom.xml每次更新需要手动compile方可生效

日志

  • 日志都不陌生,简单介绍一下。日志其实就是记录程序日常运行的信息,主要作用如下:
  • 编程期调试代码
  • 运营期记录信息
  • 记录日常运营重要信息(峰值流量、平均响应时长……)
  • 记录应用报错信息(错误堆栈)
  • 记录运维过程数据(扩容、宕机、报警……)

代码中使用日志工具记录日志

日志的使用格式非常固定,直接上操作步骤:

步骤一:添加日志记录操作
直接在类中定义日志对象并调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/books")
public class BookController extends BaseClass{
private static final Logger log = LoggerFactory.getLogger(BookController.class);
@GetMapping
public String getById(){
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
# 开启debug模式,输出调试信息,常用于检查系统运行状况
debug: true

这么设置太简单粗暴了,日志系统通常都提供了细粒度的控制

1
2
3
4
5
6
7
# 开启debug模式,输出调试信息,常用于检查系统运行状况
debug: true

# 设置日志级别,root表示根节点,即整体应用日志级别
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
@RestController
@RequestMapping("/books")
public class BookController extends BaseClass{
private static final Logger log = LoggerFactory.getLogger(BookController.class); //这一句可以不写了
}

导入lombok后使用注解搞定,日志对象名为log

1
2
3
4
5
6
@Slf4j		//这个注解替代了下面那一行
@RestController
@RequestMapping("/books")
public class BookController extends BaseClass{
private static final Logger log = LoggerFactory.getLogger(BookController.class); //这一句可以不写了
}

日志输出格式控制

  • 日志已经能够记录了,但是目前记录的格式是SpringBoot给我们提供的,如果想自定义控制就需要自己设置了。先分析一下当前日志的记录格式。
  • 对于单条日志信息来说,日期,触发位置,记录信息是最核心的信息。级别用于做筛选过滤,PID与线程名用于做精准分析。了解这些信息后就可以DIY日志格式了。这里不做详细的研究。下面给出课程中模拟的官方日志模板
1
2
3
logging:
pattern:
console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"

日志文件

  • 日志不仅能显示在控制台,还可以将日志记录到文件中,以便后期维护查阅。
  • 对于日志文件的使用存在各种各样的策略,例如每日记录,分类记录,报警后记录等。这里主要研究日志文件如何记录。

​记录日志到文件中格式非常简单,设置日志文件名即可。

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是一个递增变量,用于区分日志文件。

总结

运维实用篇完结

​在整体运维实用篇中带着大家学习了4块内容,首先学习了如何运行SpringBoot程序,也就是程序的打包与运行,接下来对配置进行了升级学习,不再局限在配置文件中进行设置,通过临时属性,外部配置文件对项目的配置进行管控。在多环境开发中给大家介绍了多种多环境开发的格式,其实掌握一种即可,此外还给大家讲了多环境开发的一些技巧以及与maven的冲突解决方案。最后给大家介绍了日志系统,老实说日志这里讲的相当的潦草,因为大部分日志相关的知识都不应该在这门课中学习,这里只是告诉大家如何整合实用而已。

​ 看了各位小伙伴的评论,知道你们再催更,我也在加油,一起努力吧,实用开发篇再会。实用开发篇会提高更新频度,不全部做完给大家更新了,我先把做好的一部分开放出来,随后做完一点就更新一点,额,好吧,就说到这里吧。

SpringBoot开发实用篇

​开发实用篇中主要内容是SpringBoot整合各种各样的技术,具体包含的内容如下:

  • 热部署
  • 配置高级
  • 测试
  • 数据层解决方案
  • 整合第三方技术
  • 监控

热部署

热部署:在应用程序运行的过程中,不需要停止或重启整个系统,就能更新、替换或加载代码、配置或资源文件的技术。简单说就是程序修改了,不需要重新启动服务器,服务器会自己悄悄的把更新后的程序给重新加载一遍,这就是热部署。

热部署的功能是如何实现的呢?这就要分两种情况来说了,非springboot工程和springboot工程的热部署实现方式完全不一样。先说一下原始的非springboot项目是如何实现热部署的。

非SpringBoot项目启动热部署实现原理
  • 开发非springboot项目时,我们要制作一个web工程并通过tomcat启动,通常需要先安装tomcat服务器到磁盘中,开发的程序配置发布到安装的tomcat服务器上。
  • 实现热部署的效果有两种做法:

    • 一种是在tomcat服务器的配置文件中进行配置,这种做法与使用什么IDE工具无关,不管你使用eclipse还是idea都行。
    • 另一种是通过IDE工具进行配置,比如在idea工具中进行设置,这种形式需要依赖IDE工具,每款IDE工具不同,对应的配置也不太一样。但是核心思想是一样的,就是使用服务器去监控其中加载的应用,发现产生了变化就重新加载一次。
  • 看起来非springboot项目实现热部署是一个非常简单的过程,几乎每个小伙伴都能自己写出来。最简单的想法就是启动一个定时任务,任务启动时记录每个文件的大小,以后每5秒比对一下每个文件的大小是否有改变,或者是否有新文件。如果没有改变,放行,如果有改变,刷新当前记录的文件信息,然后重新启动服务器,这就可以实现热部署了。当然,这个过程肯定不能这么做,比如把一个打印输出的字符串”abc”改成”cba”,比对大小是没有变化的,但是内容缺实变了,所以这么做肯定不行,只是这么打个比方,而且重启服务器这就是冷启动了,不能算热部署。

  • 看上去这个过程也没多复杂,在springboot项目中难道还有其他的弯弯绕吗?还真有。
SpringBoot项目启动热部署实现原理
  • 基于springboot开发的web工程有一个显著的特征,就是tomcat服务器内嵌了。基础篇学过,服务器以一个对象的形式在spring容器中运行。本来我们期望于tomcat服务器加载程序后由tomcat服务器盯着程序,你变化后我就重新启动重新加载,但是现在tomcat和我们的程序是平级的,都是spring容器中的组件,这下Tomcat缺乏了一个直接的管理权,那该怎么做呢?简单,再搞一个程序X在spring容器中盯着你原始开发的程序A不就行了吗?确实,搞一个盯着程序A的程序X就行了,如果你自己开发的程序A变化了,那么程序X就命令tomcat容器重新加载程序A就OK了。并且这样做有一个好处,spring容器中东西不用全部重新加载一遍,只需要重新加载你开发的程序那一部分就可以了,这下效率又高了,挺好。

  • 程序X肯定不需要我们自己手写,springboot早就做好了,搞一个坐标导入进去就行了。

手动启动热部署

步骤一:导入开发者工具对应的坐标

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

步骤二:构建项目,可以使用快捷键CTRL + F9激活此功能

这就实现了springboot工程的热部署,使用起来非常的简单,接下来普及一下底层的工作工程

重启与重载

一个springboot项目在运行时实际上是分两个过程进行的,根据加载的东西不同,分成base类加载器restart类加载器

  • base类加载器:用来加载jar包中的类,jar包中的类和配置文件由于不会发生变化,因此不管加载多少次,加载的内容不会发生变化
  • restart类加载器:用来加载开发者自己开发的类、配置文件、页面等信息,这一类文件受开发者影响

当springboot项目启动时,base类加载器执行,加载jar包中的信息后,restart类加载器执行,加载开发者制作的内容。当执行构建项目后,由于jar中的信息不会变化,因此base类加载器无需再次执行,所以仅仅运行restart类加载即可,也就是将开发者自己制作的内容重新加载就行了,这就完成了一次热部署的过程,也可以说热部署的过程实际上是重新加载restart类加载器中的信息。

自动启动热部署

  • 前面学习的启动热部署无论是点击按钮还是使用快捷键都需要开发者自己执行,我们还可以设置自动热部署实现代码修改后程序自己执行热部署功能,减少开发者操作。
  • 自动热部署其实就是设计一个开关,打开这个开关后,IDE工具就可以自动热部署。因此这个操作和IDE工具有关,以下设置idea中启动热部署

步骤一:设置自动构建项目
打开【File】,选择【settings…】,在面板左侧的菜单中找到【Compile】选项,然后勾选【Build project automatically】,意思是自动构建项目

步骤二:允许在程序运行时进行自动构建
使用快捷键【Ctrl】+【Alt】+【Shit】+【/】打开维护面板,选择第1项【Registry…】

在选项中搜索comple,然后勾选对应项即可

​此时程序在运行的时候就可以进行自动构建了,实现了热部署的效果。

注意:

  • 如果每输入一个字母服务器都重构一次未免过于频繁,所以IDEA设置当IDEA工具失去焦点五秒后执行热部署操作。其实就是从idea工具中切换到其他工具时进行热部署,比如改完程序需要到浏览器上去调试,这个时候idea就自动进行热部署操作。

思考

  • 现在已经实现了热部署了,但是到企业开发的时候会发现,为了便于管理,在你的程序目录中除了有代码,还有可能有文档,如果你修改了一下文档,这个时候会进行热部署吗?不管是否进行热部署,这个过程我们需要自己控制才比较合理

参与热部署监控的文件范围配置

  • 前面实现了自动热部署,但是在企业开发中,程序目录中除了有代码还可能有文档,如果文档被修改了,IDEA会进行热部署吗?不管是否进行,这个过程我们自己控制才比较合理,接下来就来对热部署监控文件范围进行配置
  • 通过修改项目中的文件,可以发现其实并不是所有的文件修改都会激活热部署的,原因在于在开发者工具中有一组配置,当满足了配置中的条件后,才会启动热部署。
  • /META-INF/maven
  • /META-INF/resources
  • /resources
  • /static
  • /public
  • /templates
    以上目录中的文件如果发生变化,是不参与热部署的。如果想修改配置,可以通过application.yml文件进行设定哪些文件不参与热部署操作
1
2
3
4
5
spring:
devtools:
restart:
# 设置不参与热部署的文件或文件夹
exclude: static/**,public/**,config/application.yml

关闭热部署

  • 热部署功能显然是一个在开发阶段使用的功能,到了线上环境运行时是不可能使用热部署功能的,所以需要强制关闭此功能,通过配置可以关闭此功能。
1
2
3
4
spring:
devtools:
restart:
enabled: false
  • 如果担心配置文件层级过多导致相互覆盖最终引起配置失效,可以提高配置的层级,在更高层级中配置关闭热部署。例如在启动容器前通过系统属性设置关闭热部署功能。
1
2
3
4
5
6
7
@SpringBootApplication
public class SSMPApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(SSMPApplication.class);
}
}

其实上述担心略微有点多余,因为线上环境的维护是不可能出现修改代码的操作的,这么做唯一的作用是降低资源消耗,毕竟那双盯着你项目是不是产生变化的眼睛只要闭上了,就不具有热部署功能了,这个开关的作用就是禁用对应功能。

配置高级

  • 在前面的基础篇和运维使用篇都学习了一部分的开发配置,这里再详细介绍

@ConfigurationProperties

在基础篇学习了@ConfigurationProperties注解,使用此注解后可以加载配置文件的信息为Bean绑定属性。

1
2
3
4
servers:
ip-address: 192.168.0.1
port: 2345
timeout: -1

开发一个用来封装数据的实体类,注意要提供属性对应的setter方法,因为默认是set注入

1
2
3
4
5
6
7
@Component
@Data
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}

使用@ConfigurationProperties注解就可以将配置中的属性值关联到开发的模型类上

1
2
3
4
5
6
7
8
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}

使用该方法能轻松为我们自己定义的Bean加载配置属性,但对第三方类的属性赋值时无法使用这种方法,因为无法在第三方类的源代码上添加@ConfigurationProperties注解,该如何解决这个问题呢?

使用@ConfigurationProperties注解其实可以为第三方bean加载属性,格式特殊一点而已。

步骤一:使用@Bean注解定义第三方bean

1
2
3
4
5
@Bean
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}

步骤二:在yml中定义要绑定的属性,注意datasource此时全小写

1
2
datasource:
driverClassName: com.mysql.jdbc.Driver

步骤三:使用@ConfigurationProperties注解为第三方bean进行属性绑定,注意前缀是全小写的datasource

1
2
3
4
5
6
@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}

与之前的操作方式完全一样,只不过@ConfigurationProperties注解不仅能添加到类上,还可以添加到方法上,添加到类上是为spring容器管理的当前类的对象绑定属性,添加到方法上是为spring容器管理的当前方法的返回值对象绑定属性

  • @ConfigurationProperties即可以放在方法上也可以放在类上,相应的问题是这样找起来就比较麻烦了,Spring提供了一个全新的注解,专门标注使用@ConfigurationProperties注解绑定属性的bean是哪些。这个注解叫做@EnableConfigurationProperties

步骤一:在主配置类上开启@EnableConfigurationProperties注解,并标注要使用@ConfigurationProperties注解绑定属性的类

1
2
3
4
@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class Springboot13ConfigurationApplication {
}

步骤二:在对应的类上直接使用@ConfigurationProperties进行属性绑定

1
2
3
4
5
6
7
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}

区别:

  • 绑定属性的ServerConfig类不需要声明@Component注解。当使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean,因此无需再次声明@Component注解了。
  • 自己定义的类只需要在主配置文件中添加@EnableConfigurationProperties,第三方Bean使用@Bean+@ConfigurationProperties

最后再说一个小技巧,使用@ConfigurationProperties注解时,会出现一个提示信息

出现这个提示后只需要添加一个坐标此提醒就消失了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

总结

  1. 使用@ConfigurationProperties可以为使用@Bean声明的第三方bean绑定属性
  2. 当使用@EnableConfigurationProperties声明进行属性绑定的bean后,无需使用@Component注解再次进行bean声明

总结:

  • 为第三方Bean进行属性绑定时,使用@Bean+@ConfigurationProperties
  • 自己的Bean进行属性绑定时,使用@EnableConfigurationProperties,不需要再添加@Component注解,系统会自动将bean进行声明

松散绑定

引入

在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将原本全部为小写的属性名严格按照驼峰命名法书写,在yml配置文件中将datasource修改为dataSource,如下:

1
2
dataSource:
driverClassName: com.mysql.jdbc.Driver

此时直接启动程序,却发现只修改了配置文件中的名称,未更改程序中的名称,程序却可以正常运行,然后又将代码中的前缀datasource修改为dataSource,如下:
1
2
3
4
5
6
@Bean
@ConfigurationProperties(prefix = "dataSource")
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}

但此时却会出现错误,提示我们配置属性名无效,这就是SpringBoot为开发者提供的松散绑定功能。
1
2
3
4
5
6
7
8
Configuration property name 'dataSource' is not valid:

Invalid characters: 'S'
Bean: datasource
Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

Action:
Modify 'dataSource' so that it conforms to the canonical names requirements.

松散绑定:配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容。这是SpringBoot编程时人性化设计的一个体现,而且不仅是大小写兼容,几乎所有的命名格式都支持。例如:

1
2
3
4
5
6
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
}

可以与下面的配置属性名规则全兼容

1
2
3
4
5
servers:
ipAddress: 192.168.0.2 # 驼峰模式
ip_address: 192.168.0.2 # 下划线模式
ip-address: 192.168.0.2 # 烤肉串模式
IP_ADDRESS: 192.168.0.2 # 常量模式

为什么这四种模式都可以匹配到servers的属性名呢?因为SpringBoot在进行匹配时,会将配置中的名称去掉中划线和下划线后,忽略大小写的情况下与java代码中的属性名进行忽略大小写的等值匹配,以上4种命名去掉下划线中划线忽略大小写后都是一个词ipaddress,java代码中的属性名忽略大小写后也是ipaddress,这样就可以进行等值匹配了。

虽然这四种方式都能匹配,不过springboot官方推荐使用烤肉串模式,也就是中划线模式

  • 接下来再了解一下命名规范问题,观察前面的报错信息

    1
    2
    3
    4
    5
    6
    7
    8
    Configuration property name 'dataSource' is not valid:

    Invalid characters: 'S'
    Bean: datasource
    Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

    Action:
    Modify 'dataSource' so that it conforms to the canonical names requirements.
  • Reason描述了报错的原因,规范的名称应该是烤肉串模式(kebabcase),即使用-分隔,使用小写字母数字作为标准字符,且必须以字母开头。名称dataSource就不满足上述要求。

注意:

  • 以上规则仅针对springboot中@ConfigurationProperties注解进行属性绑定时有效,对@Value注解进行属性映射无效。

总结

  1. @ConfigurationProperties绑定属性时支持属性名宽松绑定,这个宽松体现在属性名的命名规则上
  2. @Value注解不支持松散绑定规则
  3. 绑定前缀名推荐采用烤肉串命名规则,即使用中划线做分隔符

常用计量单位绑定

  • 在前面的配置中,我们书写了如下配置值,其中第三项超时时间timeout描述了服务器操作超时时间,当前值是-1表示永不超时。
1
2
3
4
servers:
ip-address: 192.168.0.1
port: 2345
timeout: -1
  • -1引发的歧义可能会比较小,但如果此时timeout的值为240,这时候可能就会出现较大的歧义了,比如线上服务器完成一次主从备份,配置超时时间240,这个240如果单位是秒,就是4分钟,如果单位是分钟就是4小时。面对一次线上服务器的主从备份,设置4分钟,简直是开玩笑,别说拷贝过程,备份之前的压缩过程4分钟也搞不定,这可能会引发许多问题。
  • springboot利用JDK8中提供的全新的用来表示计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个JDK8中新增的类,分别是DurationDataSize
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
// 时间单位为小时
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;
// 大小单位为MB
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize dataSize;
}

  • Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)

  • DataSize:表示存储空间,可以通过@DataSizeUnit注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)

校验

  • 因为SpringBoot提供的宽松绑定机制,在属性名称的书写上我们可以放飞自我了,但如果在书写时一时忘记了模型类中的数据类型,就会出现类型不匹配的问题,如属性需要int类型,但在配置中给了一个字符串,这肯定无法完成有效的属性绑定,还会引发一些错误。
  • SpringBoot为我们集成了强大的数据校验功能来避免此类问题的发生。
  • 在JAVAEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用Hibernate提供的校验框架来作为实现进行数据校验。书写应用格式非常固定

步骤一:开启校验框架

1
2
3
4
5
6
7
8
9
10
<!--1.导入JSR303规范-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校验器做实现-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

步骤二:在需要开启校验功能的类上使用注解@Validated开启校验功能

1
2
3
4
5
6
7
@Component
@Data
@ConfigurationProperties(prefix = "servers")
//开启对当前bean的属性注入校验
@Validated
public class ServerConfig {
}

步骤三:对具体的字段设置校验规则

1
2
3
4
5
6
7
8
9
10
11
@Component
@Data
@ConfigurationProperties(prefix = "servers")
//开启对当前bean的属性注入校验
@Validated
public class ServerConfig {
//设置具体的规则
@Max(value = 8888,message = "最大值不能超过8888")
@Min(value = 202,message = "最小值不能低于202")
private int port;
}

通过设置数据格式校验可以有效避免非法数据加载,而且格式非常固定,使用起来非常简单

数据类型转换

  • 关于SpringBoot属性注入的问题基本结束,在学习过程中遇到的问题往往复杂度较低,但线上开发时遇到的问题往往要复杂很多,比如数据库连接失败,如下报错信息,显示数据库的密码错误。
1
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
  • 这段报错提示的非常明确了,是密码错误的问题,但是问题就出在自己认为输入的密码是正确的,如果是第一次遇到这种问题就会十分崩溃,无论如何修改,总是会报出密码错误

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    username: root
    password: 0127
  • 可以看到配置文件中的密码设置为了0127,一般人看到就会认为因为使用者的生日是1月27日,所以密码使用0127,但是计算机在接收数据时会认为这是一个八进制数,因为该数以0开头且每个数字均小于8,所以会被转化为十进制下的87存储起来。

解决方法:

  1. 使用字符串时标准加上引号包裹,如"0127"
  2. 遇到0开头的数据多加注意。

测试

测试是保障程序正确性的唯一屏障,在企业级开发中更是不可缺少,但是由于测试代码往往不产生实际效益,可能很多初学者并不是很关注,其实测试非常重要

加载测试专用属性

临时属性

测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置

1
2
3
env:
maxMemory: 32GB
minMemory: 16GB

但是现在想测试对应的兼容性,需要测试如下配置

1
2
3
env:
maxMemory: 16GB
minMemory: 8GB

这个时候我们肯定不能每次测试的时候都去修改源码application.yml中的配置进行测试。于是我们想到在前面也学过添加临时属性,那么测试时如何添加临时属性呢?

​springboot为测试时使用临时属性提供了对应的功能入口。在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
//properties属性可以为当前测试用例添加临时的属性配置
@SpringBootTest(properties = {"test.prop=testValue1"})
public class PropertiesAndArgsTest {

@Value("${test.prop}")
private String msg;

@Test
void testProperties(){
System.out.println(msg);
}
}

临时参数

除了上述这种情况,在前面讲解使用命令行启动springboot程序时讲过,通过命令行参数也可以设置属性值。而且线上启动程序时,通常都会添加一些专用的配置信息。作为运维人员他们才不懂java,更不懂这些配置的信息具体格式该怎么写,那如果我们作为开发者提供了对应的书写内容后,能否提前测试一下这些配置信息是否有效呢?当时是可以的,还是通过注解@SpringBootTest的另一个属性来进行设定。

1
2
3
4
5
6
7
8
9
10
11
12
//args属性可以为当前测试用例添加临时的命令行参数
@SpringBootTest(args={"--test.prop=testValue2"})
public class PropertiesAndArgsTest {

@Value("${test.prop}")
private String msg;

@Test
void testProperties(){
System.out.println(msg);
}
}

​使用注解@SpringBootTest的args属性就可以为当前测试用例模拟命令行参数并进行测试。

  • 此时可能会有一个问题,那就是添加的临时属性和临时参数谁的优先级更高呢?
    • 这个问题在前面运维实用篇遇到过类似的,而结果也相同,属性加载时会严格按照下面的顺序进行加载,越往下的优先级越高。在这个属性加载优先级的顺序中,明确规定了命令行参数的优先级排序是11,而配置属性的优先级是3,结果就不言而喻了

加载测试专用配置

  • 在测试过程中,偶尔需要临时加载一些Bean,专门应用于测试环境,这时候就可以使用@Import注解来实现。

  • 在之前学习Spring时我们就知道一个spring环境中可以设置若干个配置文件或配置类,若干个配置信息可以同时生效。现在我们的需求就是在测试环境中再添加一个配置类,然后启动测试环境时,生效此配置就行了。做法和spring环境中加载多个配置信息的方式完全一样。具体操作步骤如下:

步骤一:在测试包test中创建专用的测试环境配置类

1
2
3
4
5
6
7
@Configuration
public class MsgConfig {
@Bean
public String msg(){
return "bean msg";
}
}

上述配置仅用于演示当前实验效果,实际开发可不能这么注入String类型的数据

步骤二:在启动测试环境时,导入测试环境专用的配置类,使用@Import注解即可实现

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {

@Autowired
private String msg;

@Test
void testConfiguration(){
System.out.println(msg);
}
}

到这里就通过@Import属性实现了基于开发环境的配置基础上,对配置进行测试环境的追加操作,这样我们就可以实现每一个不同的测试用例加载不同的bean的效果,丰富测试用例的编写,同时不影响开发环境的配置。

Web环境模拟测试

  • 目前已经能够实现业务层和数据层的测试,并通过设置临时配置控制每个测试用例加载不同的测试数据,但是我们目前对表现层的测试都是通过postman进行手工测试的,没有在打包过程中体现表现层能被测试通过,现在便来学习如何测试表现层。
  • 对表现层功能进行测试需要一个基础和一个功能,一个基础即测试程序时必须启动Web环境,一个功能即测试程序需要具备发送Web请求的能力。
  • 测试表现层接口这项工作成功转换成两件事
    1. 启动Web环境
    2. 发送Web请求

启动Web环境

  • 在每个SpringBoot测试类的上方会标注@SpringBootTest注解,而在这个注解中还有一个属性叫做WebEnvironment,可以通过设置该属性,在测试用例中启动Web环境:
    1
    2
    3
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class WebTest {
    }
    测试类中启动web环境时,可以指定启动的Web环境对应的端口,springboot提供了4种设置值,分别如下:
  • MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
  • DEFINED_PORT:使用自定义的端口作为web服务器端口
  • RANDOM_PORT:使用随机端口作为web服务器端口
  • NONE:不启动web环境

​ 通过上述配置,现在启动测试程序时就可以正常启用web环境了,建议在测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。

测试类中发送请求

对于测试类中发送请求,其实java的API就提供对应的功能,只不过平时很少使用,所以较为陌生。springboot为了便于开发者进行对应的功能开发,对其又进行了包装,简化了开发步骤,具体操作如下:

步骤一:在测试类中开启web虚拟调用功能,通过注解@AutoConfigureMockMvc实现此功能的开启

1
2
3
4
5
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {
}

步骤二:定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象

1
2
3
4
5
6
7
8
9
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {

@Test
void testWeb(@Autowired MockMvc mvc) {
}
}

步骤三:创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {

@Test
void testWeb(@Autowired MockMvc mvc) throws Exception {
//http://localhost:8080/books
//创建虚拟请求,当前访问/books
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
//执行对应的请求
mvc.perform(builder);
}
}

执行测试程序,现在就可以正常的发送/books对应的请求了,注意访问路径不要写http://localhost:8080/books,因为前面的服务器IP地址和端口使用的是当前虚拟的web环境,无需指定,仅指定请求的具体路径即可。

Web环境请求结果比对

  • 目前已经能成功发送请求了,但是还没有起到测试的效果,需要比对预计值和真实值是否一致才能确认是否成功通过测试。
  • 其实发完请求后得到的信息只有一种,即响应对象。至于响应对象中包含什么,就可以比对什么。常见的比对内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    void testStatus(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义本次调用的预期值
    StatusResultMatchers status = MockMvcResultMatchers.status();
    //预计本次调用时成功的:状态200
    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("springboot2");
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(result);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @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\":1,\"name\":\"springboot2\",\"type\":\"springboot\"}");
    //添加预计值到本次调用过程中进行匹配
    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 contentType = header.string("Content-Type", "application/json");
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(contentType);
    }

此时头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void testGetById(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions action = mvc.perform(builder);

StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
action.andExpect(ok);

HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type", "application/json");
action.andExpect(contentType);

ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot\",\"type\":\"springboot\"}");
action.andExpect(result);
}

数据层测试回滚

此时测试程序可以进行表现层、业务层、数据层接口对应的功能测试了,但是测试用例开发完成后,在打包的阶段由于test生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。该如何避免这个情况呢?

springboot针对此问题给出了最简单的解决方案,在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@Transactional
@Rollback(true)
public class DaoTest {
@Autowired
private BookService bookService;

@Test
void testSave(){
Book book = new Book();
book.setName("springboot3");
book.setType("springboot3");
book.setDescription("springboot3");

bookService.save(book);
}
}

如果开发者想提交事务,再添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务

总结

  1. 在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务
  2. 通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用

测试用例数据设定

  • 对于测试用例的数据固定书写肯定是不合理的,springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:
1
2
3
4
5
6
7
8
testcase:
book:
id: ${random.int}
id2: ${random.int(10)}
type: ${random.int!5,10!}
name: ${random.value}
uuid: ${random.uuid}
publishTime: ${random.long}
  • 当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可
1
2
3
4
5
6
7
8
9
10
11
@Component
@Data
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {
private int id;
private int id2;
private int type;
private String name;
private String uuid;
private long publishTime;
}

​ 对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:

  • ${random.int}表示随机整数
  • ${random.int(10)}表示10以内的随机数
  • ${random.int(10,20)}表示10到20的随机数
  • 其中()可以是任意字符,例如[],!!均可

数据层解决方案

​开发实用篇前三章基本上是开胃菜,从这里开始,不再是单纯的在springboot内部搞事情了,要涉及到很多相关知识。

SQL

之前做SSMP整合的时候数据层解决方案涉及到了MySQL数据库与MyBatisPlus框架,后面又学了Druid数据源的配置,所以现在数据层解决方案可以说是Mysql+Druid+MyBatisPlus。而三个技术分别对应了数据层操作的三个层面:

  • 数据源技术:Druid
  • 持久化技术:MyBatisPlus
  • 数据库技术:MySQL

数据源技术

目前使用的数据源技术是Druid,运行时可以在日志中看到对应的数据源初始化信息,具体如下:

1
2
INFO 28600 --- [           main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource
INFO 28600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited

如果不使用Druid数据源,程序运行后是什么样子呢?是独立的数据库连接对象还是有其他的连接池技术支持呢?将Druid技术对应的starter去掉再次运行程序可以在日志中找到如下初始化信息:

1
2
INFO 31820 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.

​此时虽然没有DruidDataSource相关的信息了,但是日志中出现了HikariDataSource这个信息,这就是springboot内嵌数据源。

​springboot提供了3款内嵌数据源技术,分别如下:

  • HikariCP
  • Tomcat提供DataSource
  • Commons DBCP
  • SpringBoot默认使用的数据源是HikartCP,如果不想要HikartCP,且使用Tomcay作为Web服务器进行Web程序的开发,那么可以使用第二种Tomcat提供DataSource,只需要把HikartCP技术的坐标排除掉就可以了。而第三种DBCP的使用条件就更苛刻了,需要将前面两种都排除掉才会默认使用。

之前配置druid时使用的配置如下:

1
2
3
4
5
6
7
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

换成是默认的数据源HikariCP后,直接吧druid删掉就行了,如下:

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

也可以写上是对hikari做的配置,但是url地址要单独配置,如下:

1
2
3
4
5
6
7
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

如果想对hikari做进一步的配置,可以继续配置其独立的属性。例如:

1
2
3
4
5
6
7
8
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
maximum-pool-size: 50

如果不想使用hikari数据源,使用tomcat的数据源或者DBCP配置格式也是一样的。学习到这里,以后我们做数据层时,数据源对象的选择就不再是单一的使用druid数据源技术了,可以根据需要自行选择。

持久化技术

springboot给开发者提供了一套现成的数据层技术,叫做JdbcTemplate。其实这个技术不能说是springboot提供的,因为不使用springboot技术,一样能使用它,他是由spring技术提供的,所以在springboot技术范畴中,这个技术也是存在的,毕竟springboot技术是加速spring程序开发而创建的。

​这个技术其实就是回归到jdbc最原始的编程形式来进行数据层的开发,下面直接上操作步骤:
步骤一:导入jdbc对应的坐标,记得是starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency

步骤二:自动装配JdbcTemplate对象

1
2
3
4
5
6
@SpringBootTest
class Springboot15SqlApplicationTests {
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
}
}

步骤三:使用JdbcTemplate实现查询操作(非实体类封装数据的查询操作)

1
2
3
4
5
6
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
String sql = "select * from tbl_book";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
System.out.println(maps);
}

步骤四:使用JdbcTemplate实现查询操作(实体类封装数据的查询操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
String sql = "select * from tbl_book";
RowMapper<Book> rm = new RowMapper<Book>() {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book temp = new Book();
temp.setId(rs.getInt("id"));
temp.setName(rs.getString("name"));
temp.setType(rs.getString("type"));
temp.setDescription(rs.getString("description"));
return temp;
}
};
List<Book> list = jdbcTemplate.query(sql, rm);
System.out.println(list);
}

步骤五:使用JdbcTemplate实现增删改操作

1
2
3
4
5
@Test
void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate){
String sql = "insert into tbl_book values(3,'springboot1','springboot2','springboot3')";
jdbcTemplate.update(sql);
}

​ 如果想对JdbcTemplate对象进行相关配置,可以在yml文件中进行设定,具体如下:

1
2
3
4
5
6
spring:
jdbc:
template:
query-timeout: -1 # 查询超时时间
max-rows: 500 # 最大行数
fetch-size: -1 # 缓存行数

数据库技术

截止到目前,springboot给开发者提供了内置的数据源解决方案和持久化解决方案,在数据层解决方案三件套中还剩下一个数据库,莫非springboot也提供有内置的解决方案?还真有,还不是一个,三个,这一节就来说说内置的数据库解决方案。

​springboot提供了3款内置的数据库,分别是

  • H2
  • HSQL
  • Derby

​以上三款数据库除了可以独立安装之外,还可以像tomcat服务器一样,采用内嵌的形式运行在spirngboot容器中。像tomcat一样内嵌在容器中运行,说明他们也是java对象,这三款数据库的底层就是使用java语言开发的。

使用内嵌服务的原因:

  • 这三款数据库采用内嵌容器的形式运行,在应用程序运行后,如果我们进行测试工作,此时测试的数据无需存储在磁盘上,但是又要测试使用,内嵌数据库就方便了,运行在内存中,该测试测试,该运行运行,等服务器关闭后,一切烟消云散,省去了维护数据库的麻烦

以H2数据库为例讲解如何使用这些内嵌数据库,操作步骤也非常简单

步骤一:导入H2数据库对应的坐标,一共2个

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

步骤二:将工程设置为web工程,启动工程时启动H2数据库

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

步骤三:通过配置开启H2数据库控制台访问程序,也可以使用其他的数据库连接软件操作

1
2
3
4
5
spring:
h2:
console:
enabled: true
path: /h2

web端访问路径/h2,访问密码123456,如果访问失败,先配置下列数据源,启动程序运行后再次访问/h2路径就可以正常访问了

1
2
3
4
5
6
datasource:
url: jdbc:h2:~/test
hikari:
driver-class-name: org.h2.Driver
username: sa
password: 123456

步骤四:使用JdbcTemplateMyBatisPlus技术操作数据库
(略)

  • 其实只是换了一个数据库而已,其他的东西都不受影响。需要注意的是如果产品上线,需要把内存级数据库关闭,采用MySQL数据库作为数据持久化方案,关闭方式是设置enabled属性为false。

经过这一通学习,发现SQL相关的数据层可选技术一下多了很多

  • 数据源技术:Druid、Hikari、tomcat DataSource、DBCP
  • 持久化技术:MyBatisPlus、MyBatis、JdbcTemplate
  • 数据库技术:MySQL、H2、HSQL、Derby

NoSQL

  • 对于NoSQL,SpringBoot官方文档中提供了十种相关技术的整合方案,接下来对国内最流行的几款NoSQL数据库进行整合,分别是RedisMongoDBES
  • 对于每种技术在整合前会先说明一下安装和基本使用,然后再讲整合。

SpringBoot整合Redis

  • Redis是一款采用key-value数据存储格式的内存级NoSQL数据库,重点关注数据存储格式,是key-value格式,也就是键值对的存储形式。
  • 与MySQL数据库不同,MySQL数据库有表、有字段、有记录,Redis没有这些东西,就是一个名称对应一个值,并且数据以存储在内存中使用为主。什么叫以存储在内存中为主?其实Redis有它的数据持久化方案,分别是RDBAOF,但是Redis自身并不是为了数据持久化而生的,主要是在内存中保存数据,加速数据访问的,所以说是一款内存级数据库。
  • Redis支持多种数据存储格式,比如可以直接存字符串,也可以存一个map集合,list集合
基本操作
  • 启动服务器

    1
    redis-server.exe redis.windows.conf

    初学者无需调整服务器对外服务端口,默认6379。

  • 启动客户端

    1
    redis-cli.exe

    如果启动redis服务器失败,可以先启动客户端,然后执行shutdown操作后退出,此时redis服务器就可以正常执行了。

  • 服务器启动后,使用客户端就可以连接服务器,类似于启动完MySQL数据库,然后启动SQL命令行操作数据库。

  • 放置一个字符串数据到redis中,先为数据定义一个名称,比如name,age等,然后使用命令set设置数据到redis服务器中即可

1
2
set name itheima
set age 12
  • 从redis中取出已经放入的数据,根据名称取,就可以得到对应数据。如果没有对应数据就会得到(null)
1
2
get name
get age
  • 以上使用的数据存储是一个名称对应一个值,如果要维护的数据过多,可以使用别的数据存储结构。例如hash,它是一种一个名称下可以存储多个数据的存储模型,并且每个数据也可以有自己的二级存储名称。向hash结构中存储数据格式如下:
1
2
hset a a1 aa1		#对外key名称是a,在名称为a的存储模型中,a1这个key中保存了数据aa1
hset a a2 aa2
  • 获取hash结构中的数据命令如下
1
2
hget a a1			#得到aa1
hget a a2 #得到aa2
整合
  • 在进行整合之前先梳理一下整合的思想,springboot整合任何技术其实就是在springboot中使用对应技术的API。如果两个技术没有交集,就不存在整合的概念了。所谓整合其实就是使用springboot技术去管理其他技术,几个问题是躲不掉的。
  1. 需要先导入对应技术的坐标,而整合之后,这些坐标都有了一些变化
  2. 任何技术通常都会有一些相关的设置信息,整合之后,这些信息如何写,写在哪是一个问题
  3. 没有整合之前操作如果是模式A的话,整合之后如果没有给开发者带来一些便捷操作,那整合将毫无意义,所以整合后操作肯定要简化一些,那对应的操作方式自然也有所不同
  • 按照上面的三个问题去思考springboot整合所有技术是一种通用思想,在整合的过程中会逐步摸索出整合的套路,而且适用性非常强,经过若干种技术的整合后基本上可以总结出一套固定思维。

  • 下面就开始springboot整合redis,操作步骤如下:

步骤一:导入springboot整合redis的starter坐标
可以在创建模块时勾选,归属在NoSQL中,也可以通过pom.xml导入

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

步骤二:进行基础配置
操作redis,最基本的信息就是操作哪一台redis服务器,所以服务器地址属于基础配置信息,不可缺少。但是即便你不配置,目前也是可以用的。因为以上两组信息hostport都有默认配置,刚好就是上述配置值。

1
2
3
4
spring:
redis:
host: localhost
port: 6379

步骤三:使用springboot整合redis的专用客户端接口操作,此处使用的是RedisTemplate

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
@SpringBootTest
class Springboot16RedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void set() {
ValueOperations ops = redisTemplate.opsForValue();
ops.set("age",41);
}
@Test
void get() {
ValueOperations ops = redisTemplate.opsForValue();
Object age = ops.get("name");
System.out.println(age);
}
@Test
void hset() {
HashOperations ops = redisTemplate.opsForHash();
ops.put("info","b","bb");
}
@Test
void hget() {
HashOperations ops = redisTemplate.opsForHash();
Object val = ops.get("info", "b");
System.out.println(val);
}
}

​ 在操作redis时,需要先确认操作何种数据,根据数据种类得到操作接口。例如使用opsForValue()获取string类型的数据操作接口,使用opsForHash()获取hash类型的数据操作接口,剩下的就是调用对应api操作了。各种类型的数据操作接口如下:

  • 由于redis内部不提供java对象的存储格式,因此当操作的数据以对象的形式存在时,会进行转码,转换成字符串格式后进行操作。为了方便开发者使用基于字符串为数据的操作,springboot整合redis时提供了专用的API接口StringRedisTemplate,可以理解为这是RedisTemplate的一种指定数据泛型的操作API。
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class StringRedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void get(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String name = ops.get("name");
System.out.println(name);
}
}

redis客户端选择

  • springboot整合redis技术提供了多种客户端兼容模式,默认提供的是lettucs客户端技术,也可以根据需要切换成指定客户端技术,例如jedis客户端技术,切换成jedis客户端技术操作步骤如下:

步骤一:导入jedis坐标

1
2
3
4
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

​ jedis坐标受springboot管理,无需提供版本号

步骤二:配置客户端技术类型,设置为jedis

1
2
3
4
5
spring:
redis:
host: localhost
port: 6379
client-type: jedis

步骤三:根据需要设置对应的配置

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: localhost
port: 6379
client-type: jedis
lettuce:
pool:
max-active: 16
jedis:
pool:
max-active: 16

lettcus与jedis区别

  • jedis连接Redis服务器是直连模式,当多线程模式下使用jedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,这样整体性能就大受影响
  • lettcus基于Netty框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnectionStatefulRedisConnection自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。当然lettcus也支持多连接实例一起工作

SpringBoot整合MongoDB

使用Redis技术可以有效的提高数据访问速度,但是由于Redis的数据格式单一性,无法操作结构化数据,当操作对象型的数据时,Redis就显得捉襟见肘。在保障访问速度的情况下,如果想操作结构化数据,此时就需要使用全新的数据存储结束来解决此问题,本节讲解springboot如何整合MongoDB技术。

MongoDB是一个开源、高性能、无模式文档型数据库,它是NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库。

什么是无模式呢?简单说就是作为一款数据库,没有固定的数据存储结构,第一条数据可能有A、B、C一共3个字段,第二条数据可能有D、E、F也是3个字段,第三条数据可能是A、C、E3个字段,也就是说数据的结构不固定,这就是无模式。有人会说这有什么用啊?灵活,随时变更,不受约束。基于上述特点,MongoDB的应用面也会产生一些变化。以下列出了一些可以使用MongoDB作为数据存储的场景,但是并不是必须使用MongoDB的场景:

  • 淘宝用户数据
    • 存储位置:数据库
    • 特征:永久性存储,修改频度极低
  • 游戏装备数据、游戏道具数据
    • 存储位置:数据库、Mongodb
    • 特征:永久性存储与临时存储相结合、修改频度较高
  • 直播数据、打赏数据、粉丝数据
    • 存储位置:数据库、Mongodb
    • 特征:永久性存储与临时存储相结合,修改频度极高
  • 物联网数据
    • 存储位置:Mongodb
    • 特征:临时存储,修改频度飞速
基本操作

MongoDB虽然是一款数据库,但是它的操作并不使用SQL语句进行,因此操作方式可能比较陌生,好在有一些类似于Navicat的数据库客户端软件,能够便捷的操作MongoDB,先安装一个客户端,再来操作MongoDB。

​ 同类型的软件较多,本次安装的软件时Robo3t,Robot3t是一款绿色软件,无需安装,解压缩即可。解压缩完毕后进入安装目录双击robot3t.exe即可使用。

image-20220224114911573

​ 打开软件首先要连接MongoDB服务器,选择【File】菜单,选择【Connect…】

image-20220224115202422

​ 进入连接管理界面后,选择左上角的【Create】链接,创建新的连接设置

image-20220224115254200

​ 如果输入设置值即可连接(默认不修改即可连接本机27017端口)

image-20220224115300266

​ 连接成功后在命令输入区域输入命令即可操作MongoDB。

​ 创建数据库:在左侧菜单中使用右键创建,输入数据库名称即可

​ 创建集合:在Collections上使用右键创建,输入集合名称即可,集合等同于数据库中的表的作用

​ 新增文档:(文档是一种类似json格式的数据,初学者可以先把数据理解为就是json数据)

1
db.集合名称.insert/save/insertOne(文档)

​ 删除文档:

1
db.集合名称.remove(条件)

​ 修改文档:

1
db.集合名称.update(条件,{操作种类:{文档}})

​ 查询文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
基础查询
查询全部: db.集合.find();
查第一条: db.集合.findOne()
查询指定数量文档: db.集合.find().limit(10) //查10条文档
跳过指定数量文档: db.集合.find().skip(20) //跳过20条文档
统计: db.集合.count()
排序: db.集合.sort({age:1}) //按age升序排序
投影: db.集合名称.find(条件,{name:1,age:1}) //仅保留name与age域

条件查询
基本格式: db.集合.find({条件})
模糊查询: db.集合.find({域名:/正则表达式/}) //等同SQL中的like,比like强大,可以执行正则所有规则
条件比较运算: db.集合.find({域名:{$gt:值}}) //等同SQL中的数值比较操作,例如:name>18
包含查询: db.集合.find({域名:{$in:[值1,值2]}}) //等同于SQL中的in
条件连接查询: db.集合.find({$and:[{条件1},{条件2}]}) //等同于SQL中的and、or

​ 有关MongoDB的基础操作就普及到这里,需要全面掌握MongoDB技术,请参看相关教程学习。

整合

​ 使用springboot整合MongDB该如何进行呢?其实springboot为什么使用的开发者这么多,就是因为他的套路几乎完全一样。导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB同样如此。

​ 第一,先导入对应技术的整合starter坐标

​ 第二,配置必要信息

​ 第三,使用提供的API操作即可

​ 下面就开始springboot整合MongoDB,操作步骤如下:

步骤①:导入springboot整合MongoDB的starter坐标

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

​ 上述坐标也可以在创建模块的时候通过勾选的形式进行选择,同样归属NoSQL分类中

image-20220224120721626

步骤②:进行基础配置

1
2
3
4
spring:
data:
mongodb:
uri: mongodb://localhost/itheima

​ 操作MongoDB需要的配置与操作redis一样,最基本的信息都是操作哪一台服务器,区别就是连接的服务器IP地址和端口不同,书写格式不同而已。

步骤③:使用springboot整合MongoDB的专用客户端接口MongoTemplate来进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
class Springboot17MongodbApplicationTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
void contextLoads() {
Book book = new Book();
book.setId(2);
book.setName("springboot2");
book.setType("springboot2");
book.setDescription("springboot2");
mongoTemplate.save(book);
}
@Test
void find(){
List<Book> all = mongoTemplate.findAll(Book.class);
System.out.println(all);
}
}

​ 整合工作到这里就做完了,感觉既熟悉也陌生。熟悉的是这个套路,三板斧,就这三招,导坐标做配置用API操作,陌生的是这个技术,里面具体的操作API可能会不熟悉,有关springboot整合MongoDB我们就讲到这里。有兴趣可以继续学习MongoDB的操作,然后再来这里通过编程的形式操作MongoDB。

总结

  1. springboot整合MongoDB步骤
    1. 导入springboot整合MongoDB的starter坐标
    2. 进行基础配置
    3. 使用springboot整合MongoDB的专用客户端接口MongoTemplate操作

SpringBoot整合ES

​ NoSQL解决方案已经讲完了两种技术的整合了,Redis可以使用内存加载数据并实现数据快速访问,MongoDB可以在内存中存储类似对象的数据并实现数据的快速访问,在企业级开发中对于速度的追求是永无止境的。下面要讲的内容也是一款NoSQL解决方案,只不过他的作用不是为了直接加速数据的读写,而是加速数据的查询的,叫做ES技术。

​ ES(Elasticsearch)是一个分布式全文搜索引擎,重点是全文搜索。

​ 那什么是全文搜索呢?比如用户要买一本书,以Java为关键字进行搜索,不管是书名中还是书的介绍中,甚至是书的作者名字,只要包含java就作为查询结果返回给用户查看,上述过程就使用了全文搜索技术。搜索的条件不再是仅用于对某一个字段进行比对,而是在一条数据中使用搜索条件去比对更多的字段,只要能匹配上就列入查询结果,这就是全文搜索的目的。而ES技术就是一种可以实现上述效果的技术。

​ 要实现全文搜索的效果,不可能使用数据库中like操作去进行比对,这种效率太低了。ES设计了一种全新的思想,来实现全文搜索。具体操作过程如下:

  1. 将被查询的字段的数据全部文本信息进行查分,分成若干个词

    • 例如“中华人民共和国”就会被拆分成三个词,分别是“中华”、“人民”、“共和国”,此过程有专业术语叫做分词。分词的策略不同,分出的效果不一样,不同的分词策略称为分词器。
  2. 将分词得到的结果存储起来,对应每条数据的id

    • 例如id为1的数据中名称这一项的值是“中华人民共和国”,那么分词结束后,就会出现“中华”对应id为1,“人民”对应id为1,“共和国”对应id为1

    • 例如id为2的数据中名称这一项的值是“人民代表大会“,那么分词结束后,就会出现“人民”对应id为2,“代表”对应id为2,“大会”对应id为2

    • 此时就会出现如下对应结果,按照上述形式可以对所有文档进行分词。需要注意分词的过程不是仅对一个字段进行,而是对每一个参与查询的字段都执行,最终结果汇总到一个表格中

      | 分词结果关键字 | 对应id |
      | ——————— | ——— |
      | 中华 | 1 |
      | 人民 | 1,2 |
      | 共和国 | 1 |
      | 代表 | 2 |
      | 大会 | 2 |

  3. 当进行查询时,如果输入“人民”作为查询条件,可以通过上述表格数据进行比对,得到id值1,2,然后根据id值就可以得到查询的结果数据了。

​ 上述过程中分词结果关键字内容每一个都不相同,作用有点类似于数据库中的索引,是用来加速数据查询的。但是数据库中的索引是对某一个字段进行添加索引,而这里的分词结果关键字不是一个完整的字段值,只是一个字段中的其中的一部分内容。并且索引使用时是根据索引内容查找整条数据,全文搜索中的分词结果关键字查询后得到的并不是整条的数据,而是数据的id,要想获得具体数据还要再次查询,因此这里为这种分词结果关键字起了一个全新的名称,叫做倒排索引

​ 通过上述内容的学习,发现使用ES其实准备工作还是挺多的,必须先建立文档的倒排索引,然后才能继续使用。快速了解一下ES的工作原理,下面直接开始我们的学习,老规矩,先安装,再操作,最后说整合。

安装

​ windows版安装包下载地址:https://www.elastic.co/cn/downloads/elasticsearch

​ 下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件

image-20220225132756400

  • bin目录:包含所有的可执行命令
  • config目录:包含ES服务器使用的配置文件
  • jdk目录:此目录中包含了一个完整的jdk工具包,版本17,当ES升级时,使用最新版本的jdk确保不会出现版本支持性不足的问题
  • lib目录:包含ES运行的依赖jar文件
  • logs目录:包含ES运行后产生的所有日志文件
  • modules目录:包含ES软件中所有的功能模块,也是一个一个的jar包。和jar目录不同,jar目录是ES运行期间依赖的jar包,modules是ES软件自己的功能jar包
  • plugins目录:包含ES软件安装的插件,默认为空

启动服务器

1
elasticsearch.bat

​ 双击elasticsearch.bat文件即可启动ES服务器,默认服务端口9200。通过浏览器访问http://localhost:9200看到如下信息视为ES服务器正常启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name" : "CZBK-**********",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "j137DSswTPG8U4Yb-0T1Mg",
"version" : {
"number" : "7.16.2",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb",
"build_date" : "2021-12-18T19:42:46.604893745Z",
"build_snapshot" : false,
"lucene_version" : "8.10.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
基本操作

​ ES中保存有我们要查询的数据,只不过格式和数据库存储数据格式不同而已。在ES中我们要先创建倒排索引,这个索引的功能又点类似于数据库的表,然后将数据添加到倒排索引中,添加的数据称为文档。所以要进行ES的操作要先创建索引,再添加文档,这样才能进行后续的查询操作。

​ 要操作ES可以通过Rest风格的请求来进行,也就是说发送一个请求就可以执行一个操作。比如新建索引,删除索引这些操作都可以使用发送请求的形式来进行。

  • 创建索引,books是索引名称,下同

    1
    PUT请求		http://localhost:9200/books

    发送请求后,看到如下信息即索引创建成功

    1
    2
    3
    4
    5
    {
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "books"
    }

    重复创建已经存在的索引会出现错误信息,reason属性中描述错误原因

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "error": {
    "root_cause": [
    {
    "type": "resource_already_exists_exception",
    "reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists",
    "index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
    "index": "books"
    }
    ],
    "type": "resource_already_exists_exception",
    "reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists", # books索引已经存在
    "index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
    "index": "book"
    },
    "status": 400
    }
  • 查询索引

    1
    GET请求		http://localhost:9200/books

    查询索引得到索引相关信息,如下

    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
    {
    "book": {
    "aliases": {},
    "mappings": {},
    "settings": {
    "index": {
    "routing": {
    "allocation": {
    "include": {
    "_tier_preference": "data_content"
    }
    }
    },
    "number_of_shards": "1",
    "provided_name": "books",
    "creation_date": "1645768584849",
    "number_of_replicas": "1",
    "uuid": "VgC_XMVAQmedaiBNSgO2-w",
    "version": {
    "created": "7160299"
    }
    }
    }
    }
    }

    如果查询了不存在的索引,会返回错误信息,例如查询名称为book的索引后信息如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    "error": {
    "root_cause": [
    {
    "type": "index_not_found_exception",
    "reason": "no such index [book]",
    "resource.type": "index_or_alias",
    "resource.id": "book",
    "index_uuid": "_na_",
    "index": "book"
    }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index [book]", # 没有book索引
    "resource.type": "index_or_alias",
    "resource.id": "book",
    "index_uuid": "_na_",
    "index": "book"
    },
    "status": 404
    }
  • 删除索引

    1
    DELETE请求	http://localhost:9200/books

    删除所有后,给出删除结果

    1
    2
    3
    {
    "acknowledged": true
    }

    如果重复删除,会给出错误信息,同样在reason属性中描述具体的错误原因

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    "error": {
    "root_cause": [
    {
    "type": "index_not_found_exception",
    "reason": "no such index [books]",
    "resource.type": "index_or_alias",
    "resource.id": "book",
    "index_uuid": "_na_",
    "index": "book"
    }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index [books]", # 没有books索引
    "resource.type": "index_or_alias",
    "resource.id": "book",
    "index_uuid": "_na_",
    "index": "book"
    },
    "status": 404
    }
  • 创建索引并指定分词器

    ​ 前面创建的索引是未指定分词器的,可以在创建索引时添加请求参数,设置分词器。目前国内较为流行的分词器是IK分词器,使用前先在下对应的分词器,然后使用。IK分词器下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases

    ​ 分词器下载后解压到ES安装目录的plugins目录中即可,安装分词器后需要重新启动ES服务器。使用IK分词器创建索引格式:

    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
    PUT请求		http://localhost:9200/books

    请求参数如下(注意是json格式的参数)
    {
        "mappings":{ #定义mappings属性,替换创建索引时对应的mappings属性
            "properties":{ #定义索引中包含的属性设置
                "id":{ #设置索引中包含id属性
                    "type":"keyword" #当前属性可以被直接搜索
                },
                "name":{ #设置索引中包含name属性
                    "type":"text",              #当前属性是文本信息,参与分词  
    "analyzer":"ik_max_word",   #使用IK分词器进行分词             
    "copy_to":"all" #分词结果拷贝到all属性中
                },
                "type":{
                    "type":"keyword"
                },
                "description":{
                    "type":"text",                 
    "analyzer":"ik_max_word",                
    "copy_to":"all"
                },
                "all":{ #定义属性,用来描述多个字段的分词结果集合,当前属性可以参与查询
                    "type":"text",                 
    "analyzer":"ik_max_word"
                }
            }
        }
    }

    ​ 创建完毕后返回结果和不使用分词器创建索引的结果是一样的,此时可以通过查看索引信息观察到添加的请求参数mappings已经进入到了索引属性中

    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
    {
    "books": {
    "aliases": {},
    "mappings": { #mappings属性已经被替换
    "properties": {
    "all": {
    "type": "text",
    "analyzer": "ik_max_word"
    },
    "description": {
    "type": "text",
    "copy_to": [
    "all"
    ],
    "analyzer": "ik_max_word"
    },
    "id": {
    "type": "keyword"
    },
    "name": {
    "type": "text",
    "copy_to": [
    "all"
    ],
    "analyzer": "ik_max_word"
    },
    "type": {
    "type": "keyword"
    }
    }
    },
    "settings": {
    "index": {
    "routing": {
    "allocation": {
    "include": {
    "_tier_preference": "data_content"
    }
    }
    },
    "number_of_shards": "1",
    "provided_name": "books",
    "creation_date": "1645769809521",
    "number_of_replicas": "1",
    "uuid": "DohYKvr_SZO4KRGmbZYmTQ",
    "version": {
    "created": "7160299"
    }
    }
    }
    }
    }

目前我们已经有了索引了,但是索引中还没有数据,所以要先添加数据,ES中称数据为文档,下面进行文档操作。

  • 添加文档,有三种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST请求	http://localhost:9200/books/_doc		#使用系统生成id
    POST请求 http://localhost:9200/books/_create/1 #使用指定id
    POST请求 http://localhost:9200/books/_doc/1 #使用指定id,不存在创建,存在更新(版本递增)

    文档通过请求参数传递,数据格式json
    {
        "name":"springboot",
        "type":"springboot",
        "description":"springboot"
    }
  • 查询文档

    1
    2
    GET请求	http://localhost:9200/books/_doc/1		 #查询单个文档 		
    GET请求 http://localhost:9200/books/_search #查询全部文档
  • 条件查询

    1
    GET请求	http://localhost:9200/books/_search?q=name:springboot	# q=查询属性名:查询属性值
  • 删除文档

    1
    DELETE请求	http://localhost:9200/books/_doc/1
  • 修改文档(全量更新)

    1
    2
    3
    4
    5
    6
    7
    8
    PUT请求	http://localhost:9200/books/_doc/1

    文档通过请求参数传递,数据格式json
    {
        "name":"springboot",
        "type":"springboot",
        "description":"springboot"
    }
  • 修改文档(部分更新)

    1
    2
    3
    4
    5
    6
    7
    8
    POST请求	http://localhost:9200/books/_update/1

    文档通过请求参数传递,数据格式json
    {
    "doc":{ #部分更新并不是对原始文档进行更新,而是对原始文档对象中的doc属性中的指定属性更新
         "name":"springboot" #仅更新提供的属性值,未提供的属性值不参与更新操作
    }
    }
整合

​ 使用springboot整合ES该如何进行呢?老规矩,导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB如此,整合ES依然如此。太没有新意了,其实不是没有新意,这就是springboot的强大之处,所有东西都做成相同规则,对开发者来说非常友好。

​ 下面就开始springboot整合ES,操作步骤如下:

步骤①:导入springboot整合ES的starter坐标

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

步骤②:进行基础配置

1
2
3
4
spring:
elasticsearch:
rest:
uris: http://localhost:9200

​ 配置ES服务器地址,端口9200

步骤③:使用springboot整合ES的专用客户端接口ElasticsearchRestTemplate来进行操作

1
2
3
4
5
@SpringBootTest
class Springboot18EsApplicationTests {
@Autowired
private ElasticsearchRestTemplate template;
}

​ 上述操作形式是ES早期的操作方式,使用的客户端被称为Low Level Client,这种客户端操作方式性能方面略显不足,于是ES开发了全新的客户端操作方式,称为High Level Client。高级别客户端与ES版本同步更新,但是springboot最初整合ES的时候使用的是低级别客户端,所以企业开发需要更换成高级别的客户端模式。

​ 下面使用高级别客户端方式进行springboot整合ES,操作步骤如下:

步骤①:导入springboot整合ES高级别客户端的坐标,此种形式目前没有对应的starter

1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

步骤②:使用编程的形式设置连接的ES服务器,并获取客户端对象

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
@Test
void testCreateClient() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);

client.close();
}
}

​ 配置ES服务器地址与端口9200,记得客户端使用完毕需要手工关闭。由于当前客户端是手工维护的,因此不能通过自动装配的形式加载对象。

步骤③:使用客户端对象操作ES,例如创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
@Test
void testCreateIndex() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);

CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);

client.close();
}
}

​ 高级别客户端操作是通过发送请求的方式完成所有操作的,ES针对各种不同的操作,设定了各式各样的请求对象,上例中创建索引的对象是CreateIndexRequest,其他操作也会有自己专用的Request对象。

​ 当前操作我们发现,无论进行ES何种操作,第一步永远是获取RestHighLevelClient对象,最后一步永远是关闭该对象的连接。在测试中可以使用测试类的特性去帮助开发者一次性的完成上述操作,但是在业务书写时,还需要自行管理。将上述代码格式转换成使用测试类的初始化方法和销毁方法进行客户端对象的维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootTest
class Springboot18EsApplicationTests {
@BeforeEach //在测试类中每个操作运行前运行的方法
void setUp() {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
}

@AfterEach //在测试类中每个操作运行后运行的方法
void tearDown() throws IOException {
client.close();
}

private RestHighLevelClient client;

@Test
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);
}
}

​ 现在的书写简化了很多,也更合理。下面使用上述模式将所有的ES操作执行一遍,测试结果

创建索引(IK分词器)

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
@Test
void testCreateIndexByIK() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
String json = "{\n" +
" \"mappings\":{\n" +
" \"properties\":{\n" +
" \"id\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"type\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"description\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"all\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
//设置请求中的参数
request.source(json, XContentType.JSON);
client.indices().create(request, RequestOptions.DEFAULT);
}

​ IK分词器是通过请求参数的形式进行设置的,设置请求参数使用request对象中的source方法进行设置,至于参数是什么,取决于你的操作种类。当请求中需要参数时,均可使用当前形式进行参数设置。

添加文档

1
2
3
4
5
6
7
8
9
@Test
//添加文档
void testCreateDoc() throws IOException {
Book book = bookDao.selectById(1);
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
client.index(request,RequestOptions.DEFAULT);
}

​ 添加文档使用的请求对象是IndexRequest,与创建索引使用的请求对象不同。

批量添加文档

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
//批量添加文档
void testCreateDocAll() throws IOException {
List<Book> bookList = bookDao.selectList(null);
BulkRequest bulk = new BulkRequest();
for (Book book : bookList) {
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
bulk.add(request);
}
client.bulk(bulk,RequestOptions.DEFAULT);
}

​ 批量做时,先创建一个BulkRequest的对象,可以将该对象理解为是一个保存request对象的容器,将所有的请求都初始化好后,添加到BulkRequest对象中,再使用BulkRequest对象的bulk方法,一次性执行完毕。

按id查询文档

1
2
3
4
5
6
7
8
@Test
//按id查询
void testGet() throws IOException {
GetRequest request = new GetRequest("books","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
String json = response.getSourceAsString();
System.out.println(json);
}

​ 根据id查询文档使用的请求对象是GetRequest。

按条件查询文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
//按条件查询
void testSearch() throws IOException {
SearchRequest request = new SearchRequest("books");

SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("all","spring"));
request.source(builder);

SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
//System.out.println(source);
Book book = JSON.parseObject(source, Book.class);
System.out.println(book);
}
}

​ 按条件查询文档使用的请求对象是SearchRequest,查询时调用SearchRequest对象的termQuery方法,需要给出查询属性名,此处支持使用合并字段,也就是前面定义索引属性时添加的all属性。

​ springboot整合ES的操作到这里就说完了,与前期进行springboot整合redis和mongodb的差别还是蛮大的,主要原始就是我们没有使用springboot整合ES的客户端对象。至于操作,由于ES操作种类过多,所以显得操作略微有点复杂。有关springboot整合ES就先学习到这里吧。

总结

  1. springboot整合ES步骤
    1. 导入springboot整合ES的High Level Client坐标
    2. 手工管理客户端对象,包括初始化和关闭操作
    3. 使用High Level Client根据操作的种类不同,选择不同的Request对象完成对应操作

KF-5.整合第三方技术

​ 通过第四章的学习,我们领略到了springboot在整合第三方技术时强大的一致性,在第五章中我们要使用springboot继续整合各种各样的第三方技术,通过本章的学习,可以将之前学习的springboot整合第三方技术的思想贯彻到底,还是那三板斧。导坐标、做配置、调API。

​ springboot能够整合的技术实在是太多了,可以说是万物皆可整。本章将从企业级开发中常用的一些技术作为出发点,对各种各样的技术进行整合。

KF-5-1.缓存

​ 企业级应用主要作用是信息处理,当需要读取数据时,由于受限于数据库的访问效率,导致整体系统性能偏低。

image-20220226154148303

​ 应用程序直接与数据库打交道,访问效率低

​ 为了改善上述现象,开发者通常会在应用程序与数据库之间建立一种临时的数据存储机制,该区域中的数据在内存中保存,读写速度较快,可以有效解决数据库访问效率低下的问题。这一块临时存储数据的区域就是缓存。

image-20220226154233010

                                         使用缓存后,应用程序与缓存打交道,缓存与数据库打交道,数据访问效率提高

​ 缓存是什么?缓存是一种介于数据永久存储介质与应用程序之间的数据临时存储介质,使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能。此外缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间。而springboot提供了对市面上几乎所有的缓存技术进行整合的方案,下面就一起开启springboot整合缓存之旅。

SpringBoot内置缓存解决方案

​ springboot技术提供有内置的缓存解决方案,可以帮助开发者快速开启缓存技术,并使用缓存技术进行数据的快速操作,例如读取缓存数据和写入数据到缓存。

步骤①:导入springboot提供的缓存技术对应的starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

步骤②:启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存

1
2
3
4
5
6
7
8
@SpringBootApplication
//开启缓存功能
@EnableCaching
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}

步骤③:设置操作的数据是否使用缓存

1
2
3
4
5
6
7
8
9
10
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;

@Cacheable(value="cacheSpace",key="#id")
public Book getById(Integer id) {
return bookDao.selectById(id);
}
}

​ 在业务方法上面使用注解@Cacheable声明当前方法的返回值放入缓存中,其中要指定缓存的存储位置,以及缓存中保存当前方法返回值对应的名称。上例中value属性描述缓存的存储位置,可以理解为是一个存储空间名,key属性描述了缓存中保存数据的名称,使用#id读取形参中的id值作为缓存名称。

​ 使用@Cacheable注解后,执行当前操作,如果发现对应名称在缓存中没有数据,就正常读取数据,然后放入缓存;如果对应名称在缓存中有数据,就终止当前业务方法执行,直接返回缓存中的数据。

手机验证码案例

​ 为了便于下面演示各种各样的缓存技术,我们创建一个手机验证码的案例环境,模拟使用缓存保存手机验证码的过程。

​ 手机验证码案例需求如下:

  • 输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟)
  • 输入手机号和验证码验证结果

​ 为了描述上述操作,我们制作两个表现层接口,一个用来模拟发送短信的过程,其实就是根据用户提供的手机号生成一个验证码,然后放入缓存,另一个用来模拟验证码校验的过程,其实就是使用传入的手机号和验证码进行匹配,并返回最终匹配结果。下面直接制作本案例的模拟代码,先以上例中springboot提供的内置缓存技术来完成当前案例的制作。

步骤①:导入springboot提供的缓存技术对应的starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

步骤②:启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存

1
2
3
4
5
6
7
8
@SpringBootApplication
//开启缓存功能
@EnableCaching
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}

步骤③:定义验证码对应的实体类,封装手机号与验证码两个属性

1
2
3
4
5
@Data
public class SMSCode {
private String tele;
private String code;
}

步骤④:定义验证码功能的业务层接口与实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface SMSCodeService {
public String sendCodeToSMS(String tele);
public boolean checkCode(SMSCode smsCode);
}

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;

@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
return code;
}

public boolean checkCode(SMSCode smsCode) {
//取出内存中的验证码与传递过来的验证码比对,如果相同,返回true
String code = smsCode.getCode();
String cacheCode = codeUtils.get(smsCode.getTele());
return code.equals(cacheCode);
}
}

​ 获取验证码后,当验证码失效时必须重新获取验证码,因此在获取验证码的功能上不能使用@Cacheable注解,@Cacheable注解是缓存中没有值则放入值,缓存中有值则取值。此处的功能仅仅是生成验证码并放入缓存,并不具有从缓存中取值的功能,因此不能使用@Cacheable注解,应该使用仅具有向缓存中保存数据的功能,使用@CachePut注解即可。

​ 对于校验验证码的功能建议放入工具类中进行。

步骤⑤:定义验证码的生成策略与根据手机号读取验证码的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class CodeUtils {
private String [] patch = {"000000","00000","0000","000","00","0",""};

public String generator(String tele){
int hash = tele.hashCode();
int encryption = 20206666;
long result = hash ^ encryption;
long nowTime = System.currentTimeMillis();
result = result ^ nowTime;
long code = result % 1000000;
code = code < 0 ? -code : code;
String codeStr = code + "";
int len = codeStr.length();
return patch[len] + codeStr;
}

@Cacheable(value = "smsCode",key="#tele")
public String get(String tele){
return null;
}
}

步骤⑥:定义验证码功能的web层接口,一个方法用于提供手机号获取验证码,一个方法用于提供手机号和验证码进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/sms")
public class SMSCodeController {
@Autowired
private SMSCodeService smsCodeService;

@GetMapping
public String getCode(String tele){
String code = smsCodeService.sendCodeToSMS(tele);
return code;
}

@PostMapping
public boolean checkCode(SMSCode smsCode){
return smsCodeService.checkCode(smsCode);
}
}

SpringBoot整合Ehcache缓存

​ 手机验证码的案例已经完成了,下面就开始springboot整合各种各样的缓存技术,第一个整合Ehcache技术。Ehcache是一种缓存技术,使用springboot整合Ehcache其实就是变更一下缓存技术的实现方式,话不多说,直接开整

步骤①:导入Ehcache的坐标

1
2
3
4
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

​ 此处为什么不是导入Ehcache的starter,而是导入技术坐标呢?其实springboot整合缓存技术做的是通用格式,不管你整合哪种缓存技术,只是实现变化了,操作方式一样。这也体现出springboot技术的优点,统一同类技术的整合方式。

步骤②:配置缓存技术实现使用Ehcache

1
2
3
4
5
spring:
cache:
type: ehcache
ehcache:
config: ehcache.xml

​ 配置缓存的类型type为ehcache,此处需要说明一下,当前springboot可以整合的缓存技术中包含有ehcach,所以可以这样书写。其实这个type不可以随便写的,不是随便写一个名称就可以整合的。

​ 由于ehcache的配置有独立的配置文件格式,因此还需要指定ehcache的配置文件,以便于读取相应配置

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
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="D:\ehcache" />

<!--默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxElementsInMemory:最大缓存数量-->
<!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
<!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码-->
<!-- timeToLiveSeconds:最大存活时间-->
<!-- memoryStoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />

<cache
name="smsCode"
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
memoryStoreEvictionPolicy="LRU" />
</ehcache>

​ 注意前面的案例中,设置了数据保存的位置是smsCode

1
2
3
4
5
@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
return code;
}

​ 这个设定需要保障ehcache中有一个缓存空间名称叫做smsCode的配置,前后要统一。在企业开发过程中,通过设置不同名称的cache来设定不同的缓存策略,应用于不同的缓存数据。

​ 到这里springboot整合Ehcache就做完了,可以发现一点,原始代码没有任何修改,仅仅是加了一组配置就可以变更缓存供应商了,这也是springboot提供了统一的缓存操作接口的优势,变更实现并不影响原始代码的书写。

总结

  1. springboot使用Ehcache作为缓存实现需要导入Ehcache的坐标
  2. 修改设置,配置缓存供应商为ehcache,并提供对应的缓存配置文件

SpringBoot整合Redis缓存

​ 上节使用Ehcache替换了springboot内置的缓存技术,其实springboot支持的缓存技术还很多,下面使用redis技术作为缓存解决方案来实现手机验证码案例。

​ 比对使用Ehcache的过程,加坐标,改缓存实现类型为ehcache,做Ehcache的配置。如果还成redis做缓存呢?一模一样,加坐标,改缓存实现类型为redis,做redis的配置。差别之处只有一点,redis的配置可以在yml文件中直接进行配置,无需制作独立的配置文件。

步骤①:导入redis的坐标

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

步骤②:配置缓存技术实现使用redis

1
2
3
4
5
6
spring:
redis:
host: localhost
port: 6379
cache:
type: redis

​ 如果需要对redis作为缓存进行配置,注意不是对原始的redis进行配置,而是配置redis作为缓存使用相关的配置,隶属于spring.cache.redis节点下,注意不要写错位置了。

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
use-key-prefix: false
key-prefix: sms_
cache-null-values: false
time-to-live: 10s

总结

  1. springboot使用redis作为缓存实现需要导入redis的坐标
  2. 修改设置,配置缓存供应商为redis,并提供对应的缓存配置

SpringBoot整合Memcached缓存

​ 目前我们已经掌握了3种缓存解决方案的配置形式,分别是springboot内置缓存,ehcache和redis,本节研究一下国内比较流行的一款缓存memcached。

​ 按照之前的套路,其实变更缓存并不繁琐,但是springboot并没有支持使用memcached作为其缓存解决方案,也就是说在type属性中没有memcached的配置选项,这里就需要更变一下处理方式了。在整合之前先安装memcached。

安装

​ windows版安装包下载地址:https://www.runoob.com/memcached/window-install-memcached.html

​ 下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件

image-20220226174957040

​ 可执行文件只有一个memcached.exe,使用该文件可以将memcached作为系统服务启动,执行此文件时会出现报错信息,如下:

image-20220226175141986

​ 此处出现问题的原因是注册系统服务时需要使用管理员权限,当前账号权限不足导致安装服务失败,切换管理员账号权限启动命令行

image-20220226175302903

​ 然后再次执行安装服务的命令即可,如下:

1
memcached.exe -d install

​ 服务安装完毕后可以使用命令启动和停止服务,如下:

1
2
memcached.exe -d start		# 启动服务
memcached.exe -d stop # 停止服务

​ 也可以在任务管理器中进行服务状态的切换

image-20220226175441675

变更缓存为Memcached

​ 由于memcached未被springboot收录为缓存解决方案,因此使用memcached需要通过手工硬编码的方式来使用,于是前面的套路都不适用了,需要自己写了。

​ memcached目前提供有三种客户端技术,分别是Memcached Client for Java、SpyMemcached和Xmemcached,其中性能指标各方面最好的客户端是Xmemcached,本次整合就使用这个作为客户端实现技术了。下面开始使用Xmemcached

步骤①:导入xmemcached的坐标

1
2
3
4
5
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.7</version>
</dependency>

步骤②:配置memcached,制作memcached的配置类

1
2
3
4
5
6
7
8
9
@Configuration
public class XMemcachedConfig {
@Bean
public MemcachedClient getMemcachedClient() throws IOException {
MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder("localhost:11211");
MemcachedClient memcachedClient = memcachedClientBuilder.build();
return memcachedClient;
}
}

​ memcached默认对外服务端口11211。

步骤③:使用xmemcached客户端操作缓存,注入MemcachedClient对象

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
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private MemcachedClient memcachedClient;

public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
try {
memcachedClient.set(tele,10,code);
} catch (Exception e) {
e.printStackTrace();
}
return code;
}

public boolean checkCode(SMSCode smsCode) {
String code = null;
try {
code = memcachedClient.get(smsCode.getTele()).toString();
} catch (Exception e) {
e.printStackTrace();
}
return smsCode.getCode().equals(code);
}
}

​ 设置值到缓存中使用set操作,取值使用get操作,其实更符合我们开发者的习惯。

​ 上述代码中对于服务器的配置使用硬编码写死到了代码中,将此数据提取出来,做成独立的配置属性。

定义配置属性

​ 以下过程采用前期学习的属性配置方式进行,当前操作有助于理解原理篇中的很多知识。

  • 定义配置类,加载必要的配置属性,读取配置文件中memcached节点信息

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    @ConfigurationProperties(prefix = "memcached")
    @Data
    public class XMemcachedProperties {
    private String servers;
    private int poolSize;
    private long opTimeout;
    }
  • 定义memcached节点信息

    1
    2
    3
    4
    memcached:
    servers: localhost:11211
    poolSize: 10
    opTimeout: 3000
  • 在memcached配置类中加载信息

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class XMemcachedConfig {
@Autowired
private XMemcachedProperties props;
@Bean
public MemcachedClient getMemcachedClient() throws IOException {
MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(props.getServers());
memcachedClientBuilder.setConnectionPoolSize(props.getPoolSize());
memcachedClientBuilder.setOpTimeout(props.getOpTimeout());
MemcachedClient memcachedClient = memcachedClientBuilder.build();
return memcachedClient;
}
}

总结

  1. memcached安装后需要启动对应服务才可以对外提供缓存功能,安装memcached服务需要基于windows系统管理员权限
  2. 由于springboot没有提供对memcached的缓存整合方案,需要采用手工编码的形式创建xmemcached客户端操作缓存
  3. 导入xmemcached坐标后,创建memcached配置类,注册MemcachedClient对应的bean,用于操作缓存
  4. 初始化MemcachedClient对象所需要使用的属性可以通过自定义配置属性类的形式加载

思考

​ 到这里已经完成了三种缓存的整合,其中redis和mongodb需要安装独立的服务器,连接时需要输入对应的服务器地址,这种是远程缓存,Ehcache是一个典型的内存级缓存,因为它什么也不用安装,启动后导入jar包就有缓存功能了。这个时候就要问了,能不能这两种缓存一起用呢?咱们下节再说。

SpringBoot整合jetcache缓存

​ 目前我们使用的缓存都是要么A要么B,能不能AB一起用呢?这一节就解决这个问题。springboot针对缓存的整合仅仅停留在用缓存上面,如果缓存自身不支持同时支持AB一起用,springboot也没办法,所以要想解决AB缓存一起用的问题,就必须找一款缓存能够支持AB两种缓存一起用,有这种缓存吗?还真有,阿里出品,jetcache。

​ jetcache严格意义上来说,并不是一个缓存解决方案,只能说他算是一个缓存框架,然后把别的缓存放到jetcache中管理,这样就可以支持AB缓存一起用了。并且jetcache参考了springboot整合缓存的思想,整体技术使用方式和springboot的缓存解决方案思想非常类似。下面咱们就先把jetcache用起来,然后再说它里面的一些小的功能。

​ 做之前要先明确一下,jetcache并不是随便拿两个缓存都能拼到一起去的。目前jetcache支持的缓存方案本地缓存支持两种,远程缓存支持两种,分别如下:

  • 本地缓存(Local)
    • LinkedHashMap
    • Caffeine
  • 远程缓存(Remote)
    • Redis
    • Tair

​ 其实也有人问我,为什么jetcache只支持2+2这么4款缓存呢?阿里研发这个技术其实主要是为了满足自身的使用需要。最初肯定只有1+1种,逐步变化成2+2种。下面就以LinkedHashMap+Redis的方案实现本地与远程缓存方案同时使用。

纯远程方案

步骤①:导入springboot整合jetcache对应的坐标starter,当前坐标默认使用的远程方案是redis

1
2
3
4
5
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>

步骤②:远程方案基本配置

1
2
3
4
5
6
7
8
jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50

​ 其中poolConfig是必配项,否则会报错

步骤③:启用缓存,在引导类上方标注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式创建缓存

1
2
3
4
5
6
7
8
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}

步骤④:创建缓存对象Cache,并使用注解@CreateCache标记当前缓存的信息,然后使用Cache对象的API操作缓存,put写缓存,get读缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;

@CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
private Cache<String ,String> jetCache;

public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}

public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}

​ 通过上述jetcache使用远程方案连接redis可以看出,jetcache操作缓存时的接口操作更符合开发者习惯,使用缓存就先获取缓存对象Cache,放数据进去就是put,取数据出来就是get,更加简单易懂。并且jetcache操作缓存时,可以为某个缓存对象设置过期时间,将同类型的数据放入缓存中,方便有效周期的管理。

​ 上述方案中使用的是配置中定义的default缓存,其实这个default是个名字,可以随便写,也可以随便加。例如再添加一种缓存解决方案,参照如下配置进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50

​ 如果想使用名称是sms的缓存,需要再创建缓存时指定参数area,声明使用对应缓存即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;

@CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
private Cache<String ,String> jetCache;

public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}

public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
纯本地方案

​ 远程方案中,配置中使用remote表示远程,换成local就是本地,只不过类型不一样而已。

步骤①:导入springboot整合jetcache对应的坐标starter

1
2
3
4
5
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>

步骤②:本地缓存基本配置

1
2
3
4
5
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson

​ 为了加速数据获取时key的匹配速度,jetcache要求指定key的类型转换器。简单说就是,如果你给了一个Object作为key的话,我先用key的类型转换器给转换成字符串,然后再保存。等到获取数据时,仍然是先使用给定的Object转换成字符串,然后根据字符串匹配。由于jetcache是阿里的技术,这里推荐key的类型转换器使用阿里的fastjson。

步骤③:启用缓存

1
2
3
4
5
6
7
8
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}

步骤④:创建缓存对象Cache时,标注当前使用本地缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL)
private Cache<String ,String> jetCache;

public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}

public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}

​ cacheType控制当前缓存使用本地缓存还是远程缓存,配置cacheType=CacheType.LOCAL即使用本地缓存。

本地+远程方案

​ 本地和远程方法都有了,两种方案一起使用如何配置呢?其实就是将两种配置合并到一起就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50

​ 在创建缓存的时候,配置cacheType为BOTH即则本地缓存与远程缓存同时使用。

1
2
3
4
5
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH)
private Cache<String ,String> jetCache;
}

​ cacheType如果不进行配置,默认值是REMOTE,即仅使用远程缓存方案。关于jetcache的配置,参考以下信息

属性默认值说明
jetcache.statIntervalMinutes0统计间隔,0表示不统计
jetcache.hiddenPackages自动生成name时,隐藏指定的包名前缀
jetcache.[local\remote].${area}.type缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair
jetcache.[local\remote].${area}.keyConvertorkey转换器,当前仅支持fastjson
jetcache.[local\remote].${area}.valueEncoderjava仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local\remote].${area}.valueDecoderjava仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local\remote].${area}.limit100仅local类型的缓存需要指定,缓存实例最大元素数
jetcache.[local\remote].${area}.expireAfterWriteInMillis无穷大默认过期时间,毫秒单位
jetcache.local.${area}.expireAfterAccessInMillis0仅local类型的缓存有效,毫秒单位,最大不活动间隔

​ 以上方案仅支持手工控制缓存,但是springcache方案中的方法缓存特别好用,给一个方法添加一个注解,方法就会自动使用缓存。jetcache也提供了对应的功能,即方法缓存。

方法缓存

​ jetcache提供了方法缓存方案,只不过名称变更了而已。在对应的操作接口上方使用注解@Cached即可

步骤①:导入springboot整合jetcache对应的坐标starter

1
2
3
4
5
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.2</version>
</dependency>

步骤②:配置缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson
valueEncode: java
valueDecode: java
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50

​ 由于redis缓存中不支持保存对象,因此需要对redis设置当Object类型数据进入到redis中时如何进行类型转换。需要配置keyConvertor表示key的类型转换方式,同时标注value的转换类型方式,值进入redis时是java类型,标注valueEncode为java,值从redis中读取时转换成java,标注valueDecode为java。

​ 注意,为了实现Object类型的值进出redis,需要保障进出redis的Object类型的数据必须实现序列化接口。

1
2
3
4
5
6
7
@Data
public class Book implements Serializable {
private Integer id;
private String type;
private String name;
private String description;
}

步骤③:启用缓存时开启方法缓存功能,并配置basePackages,说明在哪些包中开启方法缓存

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
//开启方法注解缓存
@EnableMethodCache(basePackages = "com.itheima")
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}

步骤④:使用注解@Cached标注当前方法使用缓存

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;

@Override
@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
public Book getById(Integer id) {
return bookDao.selectById(id);
}
}
远程方案的数据同步

​ 由于远程方案中redis保存的数据可以被多个客户端共享,这就存在了数据同步问题。jetcache提供了3个注解解决此问题,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据

更新缓存

1
2
3
4
@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book) {
return bookDao.updateById(book) > 0;
}

删除缓存

1
2
3
4
@CacheInvalidate(name="book_",key = "#id")
public boolean delete(Integer id) {
return bookDao.deleteById(id) > 0;
}

定时刷新缓存

1
2
3
4
5
@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {
return bookDao.selectById(id);
}
数据报表

​ jetcache还提供有简单的数据报表功能,帮助开发者快速查看缓存命中信息,只需要添加一个配置即可

1
2
jetcache:
statIntervalMinutes: 1

​ 设置后,每1分钟在控制台输出缓存数据命中信息

1
2
3
4
5
[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger  : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache | qps| rate| get| hit| fail| expire| avgLoadTime| maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_ | 0.66| 75.86%| 29| 22| 0| 0| 28.0| 188
---------+-------+-------+------+-------+-------+---------+--------------+--------------

总结

  1. jetcache是一个类似于springcache的缓存解决方案,自身不具有缓存功能,它提供有本地缓存与远程缓存多级共同使用的缓存解决方案
  2. jetcache提供的缓存解决方案受限于目前支持的方案,本地缓存支持两种,远程缓存支持两种
  3. 注意数据进入远程缓存时的类型转换问题
  4. jetcache提供方法缓存,并提供了对应的缓存更新与刷新功能
  5. jetcache提供有简单的缓存信息命中报表方便开发者即时监控缓存数据命中情况

思考

​ jetcache解决了前期使用缓存方案单一的问题,但是仍然不能灵活的选择缓存进行搭配使用,是否存在一种技术可以灵活的搭配各种各样的缓存使用呢?有,咱们下一节再讲。

SpringBoot整合j2cache缓存

​ jetcache可以在限定范围内构建多级缓存,但是灵活性不足,不能随意搭配缓存,本节介绍一种可以随意搭配缓存解决方案的缓存整合框架,j2cache。下面就来讲解如何使用这种缓存框架,以Ehcache与redis整合为例:

步骤①:导入j2cache、redis、ehcache坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-core</artifactId>
<version>2.8.4-release</version>
</dependency>
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-spring-boot2-starter</artifactId>
<version>2.8.0-release</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

​ j2cache的starter中默认包含了redis坐标,官方推荐使用redis作为二级缓存,因此此处无需导入redis坐标

步骤②:配置一级与二级缓存,并配置一二级缓存间数据传递方式,配置书写在名称为j2cache.properties的文件中。如果使用ehcache还需要单独添加ehcache的配置文件

1
2
3
4
5
6
7
8
9
10
11
# 1级缓存
j2cache.L1.provider_class = ehcache
ehcache.configXml = ehcache.xml

# 2级缓存
j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379

# 1级缓存中的数据如何到达二级缓存
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy

​ 此处配置不能乱配置,需要参照官方给出的配置说明进行。例如1级供应商选择ehcache,供应商名称仅仅是一个ehcache,但是2级供应商选择redis时要写专用的Spring整合Redis的供应商类名SpringRedisProvider,而且这个名称并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必须参照官方文档配置,而且还要去找专用的整合包,导入对应坐标才可以使用。

​ 一级与二级缓存最重要的一个配置就是两者之间的数据沟通方式,此类配置也不是随意配置的,并且不同的缓存解决方案提供的数据沟通方式差异化很大,需要查询官方文档进行设置。

步骤③:使用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;

@Autowired
private CacheChannel cacheChannel;

public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
cacheChannel.set("sms",tele,code);
return code;
}

public boolean checkCode(SMSCode smsCode) {
String code = cacheChannel.get("sms",smsCode.getTele()).asString();
return smsCode.getCode().equals(code);
}
}

​ j2cache的使用和jetcache比较类似,但是无需开启使用的开关,直接定义缓存对象即可使用,缓存对象名CacheChannel。

​ j2cache的使用不复杂,配置是j2cache的核心,毕竟是一个整合型的缓存框架。缓存相关的配置过多,可以查阅j2cache-core核心包中的j2cache.properties文件中的说明。如下:

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
#J2Cache configuration
#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################
j2cache.broadcast = redis

# jgroups properties
jgroups.channel.name = j2cache
jgroups.configXml = /network.xml

# RabbitMQ properties
rabbitmq.exchange = j2cache
rabbitmq.host = localhost
rabbitmq.port = 5672
rabbitmq.username = guest
rabbitmq.password = guest

# RocketMQ properties
rocketmq.name = j2cache
rocketmq.topic = j2cache
# use ; to split multi hosts
rocketmq.hosts = 127.0.0.1:9876

#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################

j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = redis

# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis

# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true

# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = true

#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
#########################################

j2cache.serialization = json
#json.map.person = net.oschina.j2cache.demo.Person

#########################################
# Ehcache configuration
#########################################

# ehcache.configXml = /ehcache.xml

# ehcache3.configXml = /ehcache3.xml
# ehcache3.defaultHeapSize = 1000

#########################################
# Caffeine configuration
# caffeine.region.[name] = size, xxxx[s|m|h|d]
#
#########################################
caffeine.properties = /caffeine.properties

#########################################
# Redis connection configuration
#########################################

#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################

redis.mode = single

#redis storage mode (generic|hash)
redis.storage = generic

## redis pub/sub channel name
redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
redis.channel.host =

#cluster name just for sharded
redis.cluster_name = j2cache

## redis cache namespace optional, default[empty]
redis.namespace =

## redis command scan parameter count, default[1000]
#redis.scanCount = 1000

## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379

redis.hosts = 127.0.0.1:6379
redis.timeout = 2000
redis.password =
redis.database = 0
redis.ssl = false

## redis pool properties
redis.maxTotal = 100
redis.maxIdle = 10
redis.maxWaitMillis = 5000
redis.minEvictableIdleTimeMillis = 60000
redis.minIdle = 1
redis.numTestsPerEvictionRun = 10
redis.lifo = false
redis.softMinEvictableIdleTimeMillis = 10
redis.testOnBorrow = true
redis.testOnReturn = false
redis.testWhileIdle = true
redis.timeBetweenEvictionRunsMillis = 300000
redis.blockWhenExhausted = false
redis.jmxEnabled = false

#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
#########################################

#########################################
# Lettuce Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################

## redis command scan parameter count, default[1000]
#lettuce.scanCount = 1000
lettuce.mode = single
lettuce.namespace =
lettuce.storage = hash
lettuce.channel = j2cache
lettuce.scheme = redis
lettuce.hosts = 127.0.0.1:6379
lettuce.password =
lettuce.database = 0
lettuce.sentinelMasterId =
lettuce.maxTotal = 100
lettuce.maxIdle = 10
lettuce.minIdle = 10
# timeout in milliseconds
lettuce.timeout = 10000
# redis cluster topology refresh interval in milliseconds
lettuce.clusterTopologyRefresh = 3000

#########################################
# memcached server configurations
# refer to https://gitee.com/mirrors/XMemcached
#########################################

memcached.servers = 127.0.0.1:11211
memcached.username =
memcached.password =
memcached.connectionPoolSize = 10
memcached.connectTimeout = 1000
memcached.failureMode = false
memcached.healSessionInterval = 1000
memcached.maxQueuedNoReplyOperations = 100
memcached.opTimeout = 100
memcached.sanitizeKeys = false

总结

  1. j2cache是一个缓存框架,自身不具有缓存功能,它提供多种缓存整合在一起使用的方案
  2. j2cache需要通过复杂的配置设置各级缓存,以及缓存之间数据交换的方式
  3. j2cache操作接口通过CacheChannel实现

KF-5-2.任务

​ springboot整合第三方技术第二部分我们来说说任务系统,其实这里说的任务系统指的是定时任务。定时任务是企业级开发中必不可少的组成部分,诸如长周期业务数据的计算,例如年度报表,诸如系统脏数据的处理,再比如系统性能监控报告,还有抢购类活动的商品上架,这些都离不开定时任务。本节将介绍两种不同的定时任务技术。

Quartz

​ Quartz技术是一个比较成熟的定时任务框架,怎么说呢?有点繁琐,用过的都知道,配置略微复杂。springboot对其进行整合后,简化了一系列的配置,将很多配置采用默认设置,这样开发阶段就简化了很多。再学习springboot整合Quartz前先普及几个Quartz的概念。

  • 工作(Job):用于定义具体执行的工作
  • 工作明细(JobDetail):用于描述定时工作相关的信息
  • 触发器(Trigger):描述了工作明细与调度器的对应关系
  • 调度器(Scheduler):用于描述触发工作的执行规则,通常使用cron表达式定义规则

​ 简单说就是你定时干什么事情,这就是工作,工作不可能就是一个简单的方法,还要设置一些明细信息。工作啥时候执行,设置一个调度器,可以简单理解成设置一个工作执行的时间。工作和调度都是独立定义的,它们两个怎么配合到一起呢?用触发器。完了,就这么多。下面开始springboot整合Quartz。

步骤①:导入springboot整合Quartz的starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

步骤②:定义任务Bean,按照Quartz的开发规范制作,继承QuartzJobBean

1
2
3
4
5
6
public class MyQuartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("quartz task run...");
}
}

步骤③:创建Quartz配置类,定义工作明细(JobDetail)与触发器的(Trigger)bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class QuartzConfig {
@Bean
public JobDetail printJobDetail(){
//绑定具体的工作
return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
}
@Bean
public Trigger printJobTrigger(){
ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
//绑定对应的工作明细
return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build();
}
}

​ 工作明细中要设置对应的具体工作,使用newJob()操作传入对应的工作任务类型即可。

​ 触发器需要绑定任务,使用forJob()操作传入绑定的工作明细对象。此处可以为工作明细设置名称然后使用名称绑定,也可以直接调用对应方法绑定。触发器中最核心的规则是执行时间,此处使用调度器定义执行时间,执行时间描述方式使用的是cron表达式。有关cron表达式的规则,各位小伙伴可以去参看相关课程学习,略微复杂,而且格式不能乱设置,不是写个格式就能用的,写不好就会出现冲突问题。

总结

  1. springboot整合Quartz就是将Quartz对应的核心对象交给spring容器管理,包含两个对象,JobDetail和Trigger对象
  2. JobDetail对象描述的是工作的执行信息,需要绑定一个QuartzJobBean类型的对象
  3. Trigger对象定义了一个触发器,需要为其指定绑定的JobDetail是哪个,同时要设置执行周期调度器

思考

​ 上面的操作看上去不多,但是Quartz将其中的对象划分粒度过细,导致开发的时候有点繁琐,spring针对上述规则进行了简化,开发了自己的任务管理组件——Task,如何用呢?咱们下节再说。

Task

​ spring根据定时任务的特征,将定时任务的开发简化到了极致。怎么说呢?要做定时任务总要告诉容器有这功能吧,然后定时执行什么任务直接告诉对应的bean什么时间执行就行了,就这么简单,一起来看怎么做

步骤①:开启定时任务功能,在引导类上开启定时任务功能的开关,使用注解@EnableScheduling

1
2
3
4
5
6
7
8
@SpringBootApplication
//开启定时任务功能
@EnableScheduling
public class Springboot22TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot22TaskApplication.class, args);
}
}

步骤②:定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式

1
2
3
4
5
6
7
@Component
public class MyBean {
@Scheduled(cron = "0/1 * * * * ?")
public void print(){
System.out.println(Thread.currentThread().getName()+" :spring task run...");
}
}

​ 完事,这就完成了定时任务的配置。总体感觉其实什么东西都没少,只不过没有将所有的信息都抽取成bean,而是直接使用注解绑定定时执行任务的事情而已。

​ 如何想对定时任务进行相关配置,可以通过配置文件进行

1
2
3
4
5
6
7
8
9
spring:
task:
scheduling:
pool:
size: 1 # 任务调度线程池大小 默认 1
thread-name-prefix: ssm_ # 调度线程名称前缀 默认 scheduling-
shutdown:
await-termination: false # 线程池关闭时等待所有任务完成
await-termination-period: 10s # 调度线程关闭前最大等待时间,确保最后一定关闭

总结

  1. spring task需要使用注解@EnableScheduling开启定时任务功能

  2. 为定时执行的的任务设置执行周期,描述方式cron表达式

KF-5-3.邮件

​ springboot整合第三方技术第三部分我们来说说邮件系统,发邮件是java程序的基本操作,springboot整合javamail其实就是简化开发。不熟悉邮件的小伙伴可以先学习完javamail的基础操作,再来看这一部分内容才能感触到springboot整合javamail究竟简化了哪些操作。简化的多码?其实不多,差别不大,只是还个格式而已。

​ 学习邮件发送之前先了解3个概念,这些概念规范了邮件操作过程中的标准。

  • SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
  • POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
  • IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议

​ 简单说就是SMPT是发邮件的标准,POP3是收邮件的标准,IMAP是对POP3的升级。我们制作程序中操作邮件,通常是发邮件,所以SMTP是使用的重点,收邮件大部分都是通过邮件客户端完成,所以开发收邮件的代码极少。除非你要读取邮件内容,然后解析,做邮件功能的统一处理。例如HR的邮箱收到求职者的简历,可以读取后统一处理。但是为什么不制作独立的投递简历的系统呢?所以说,好奇怪的需求,因为要想收邮件就要规范发邮件的人的书写格式,这个未免有点强人所难,并且极易收到外部攻击,你不可能使用白名单来收邮件。如果能使用白名单来收邮件然后解析邮件,还不如开发个系统给白名单中的人专用呢,更安全,总之就是鸡肋了。下面就开始学习springboot如何整合javamail发送邮件。

发送简单邮件

步骤①:导入springboot整合javamail的starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

步骤②:配置邮箱的登录信息

1
2
3
4
5
spring:
mail:
host: smtp.126.com
username: test@126.com
password: test

​ java程序仅用于发送邮件,邮件的功能还是邮件供应商提供的,所以这里是用别人的邮件服务,要配置对应信息。

​ host配置的是提供邮件服务的主机协议,当前程序仅用于发送邮件,因此配置的是smtp的协议。

​ password并不是邮箱账号的登录密码,是邮件供应商提供的一个加密后的密码,也是为了保障系统安全性。不然外部人员通过地址访问下载了配置文件,直接获取到了邮件密码就会有极大的安全隐患。有关该密码的获取每个邮件供应商提供的方式都不一样,此处略过。可以到邮件供应商的设置页面找POP3或IMAP这些关键词找到对应的获取位置。下例仅供参考:

image-20220228111251036

步骤③:使用JavaMailSender接口发送邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class SendMailServiceImpl implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;

//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文内容";

@Override
public void sendMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from+"(小甜甜)");
message.setTo(to);
message.setSubject(subject);
message.setText(context);
javaMailSender.send(message);
}
}

​ 将发送邮件的必要信息(发件人、收件人、标题、正文)封装到SimpleMailMessage对象中,可以根据规则设置发送人昵称等。

发送多组件邮件(附件、复杂正文)

​ 发送简单邮件仅需要提供对应的4个基本信息就可以了,如果想发送复杂的邮件,需要更换邮件对象。使用MimeMessage可以发送特殊的邮件。

发送网页正文邮件

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
@Service
public class SendMailServiceImpl2 implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;

//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "<img src='ABC.JPG'/><a href='https://www.itcast.cn'>点开有惊喜</a>";

public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(to+"(小甜甜)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context,true); //此处设置正文支持html解析

javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

发送带有附件的邮件

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
@Service
public class SendMailServiceImpl2 implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;

//发送人
private String from = "test@qq.com";
//接收人
private String to = "test@126.com";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文";

public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message,true); //此处设置支持附件
helper.setFrom(to+"(小甜甜)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context);

//添加附件
File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");
File f2 = new File("resources\\logo.png");

helper.addAttachment(f1.getName(),f1);
helper.addAttachment("最靠谱的培训结构.png",f2);

javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

总结

  1. springboot整合javamail其实就是简化了发送邮件的客户端对象JavaMailSender的初始化过程,通过配置的形式加载信息简化开发过程

KF-5-4.消息

​ springboot整合第三方技术最后一部分我们来说说消息中间件,首先先介绍一下消息的应用。

消息的概念

​ 从广义角度来说,消息其实就是信息,但是和信息又有所不同。信息通常被定义为一组数据,而消息除了具有数据的特征之外,还有消息的来源与接收的概念。通常发送消息的一方称为消息的生产者,接收消息的一方称为消息的消费者。这样比较后,发现其实消息和信息差别还是很大的。

​ 为什么要设置生产者和消费者呢?这就是要说到消息的意义了。信息通常就是一组数据,但是消息由于有了生产者和消费者,就出现了消息中所包含的信息可以被二次解读,生产者发送消息,可以理解为生产者发送了一个信息,也可以理解为生产者发送了一个命令;消费者接收消息,可以理解为消费者得到了一个信息,也可以理解为消费者得到了一个命令。对比一下我们会发现信息是一个基本数据,而命令则可以关联下一个行为动作,这样就可以理解为基于接收的消息相当于得到了一个行为动作,使用这些行为动作就可以组织成一个业务逻辑,进行进一步的操作。总的来说,消息其实也是一组信息,只是为其赋予了全新的含义,因为有了消息的流动,并且是有方向性的流动,带来了基于流动的行为产生的全新解读。开发者就可以基于消息的这种特殊解,将其换成代码中的指令。

​ 对于消息的理解,初学者总认为消息内部的数据非常复杂,这是一个误区。比如我发送了一个消息,要求接受者翻译发送过去的内容。初学者会认为消息中会包含被翻译的文字,已经本次操作要执行翻译操作而不是打印操作。其实这种现象有点过度解读了,发送的消息中仅仅包含被翻译的文字,但是可以通过控制不同的人接收此消息来确认要做的事情。例如发送被翻译的文字仅到A程序,而A程序只能进行翻译操作,这样就可以发送简单的信息完成复杂的业务了,是通过接收消息的主体不同,进而执行不同的操作,而不会在消息内部定义数据的操作行为,当然如果开发者希望消息中包含操作种类信息也是可以的,只是提出消息的内容可以更简单,更单一。

​ 对于消息的生产者与消费者的工作模式,还可以将消息划分成两种模式,同步消费与异步消息。

​ 所谓同步消息就是生产者发送完消息,等待消费者处理,消费者处理完将结果告知生产者,然后生产者继续向下执行业务。这种模式过于卡生产者的业务执行连续性,在现在的企业级开发中,上述这种业务场景通常不会采用消息的形式进行处理。

​ 所谓异步消息就是生产者发送完消息,无需等待消费者处理完毕,生产者继续向下执行其他动作。比如生产者发送了一个日志信息给日志系统,发送过去以后生产者就向下做其他事情了,无需关注日志系统的执行结果。日志系统根据接收到的日志信息继续进行业务执行,是单纯的记录日志,还是记录日志并报警,这些和生产者无关,这样生产者的业务执行效率就会大幅度提升。并且可以通过添加多个消费者来处理同一个生产者发送的消息来提高系统的高并发性,改善系统工作效率,提高用户体验。一旦某一个消费者由于各种问题宕机了,也不会对业务产生影响,提高了系统的高可用性。

​ 以上简单的介绍了一下消息这种工作模式存在的意义,希望对各位学习者有所帮助。

Java处理消息的标准规范

​ 目前企业级开发中广泛使用的消息处理技术共三大类,具体如下:

  • JMS
  • AMQP
  • MQTT

​ 为什么是三大类,而不是三个技术呢?因为这些都是规范,就想JDBC技术,是个规范,开发针对规范开发,运行还要靠实现类,例如MySQL提供了JDBC的实现,最终运行靠的还是实现。并且这三类规范都是针对异步消息进行处理的,也符合消息的设计本质,处理异步的业务。对以上三种消息规范做一下普及

JMS

​ JMS(Java Message Service),这是一个规范,作用等同于JDBC规范,提供了与消息服务相关的API接口。

JMS消息模型

​ JMS规范中规范了消息有两种模型。分别是点对点模型发布订阅模型

点对点模型:peer-2-peer,生产者会将消息发送到一个保存消息的容器中,通常使用队列模型,使用队列保存消息。一个队列的消息只能被一个消费者消费,或未被及时消费导致超时。这种模型下,生产者和消费者是一对一绑定的。

发布订阅模型:publish-subscribe,生产者将消息发送到一个保存消息的容器中,也是使用队列模型来保存。但是消息可以被多个消费者消费,生产者和消费者完全独立,相互不需要感知对方的存在。

​ 以上这种分类是从消息的生产和消费过程来进行区分,针对消息所包含的信息不同,还可以进行不同类别的划分。

JMS消息种类

​ 根据消息中包含的数据种类划分,可以将消息划分成6种消息。

  • TextMessage
  • MapMessage
  • BytesMessage
  • StreamMessage
  • ObjectMessage
  • Message (只有消息头和属性)

​ JMS主张不同种类的消息,消费方式不同,可以根据使用需要选择不同种类的消息。但是这一点也成为其诟病之处,后面再说。整体上来说,JMS就是典型的保守派,什么都按照J2EE的规范来,做一套规范,定义若干个标准,每个标准下又提供一大批API。目前对JMS规范实现的消息中间件技术还是挺多的,毕竟是皇家御用,肯定有人舔,例如ActiveMQ、Redis、HornetMQ。但是也有一些不太规范的实现,参考JMS的标准设计,但是又不完全满足其规范,例如:RabbitMQ、RocketMQ。

AMQP

​ JMS的问世为消息中间件提供了很强大的规范性支撑,但是使用的过程中就开始被人诟病,比如JMS设置的极其复杂的多种类消息处理机制。本来分门别类处理挺好的,为什么会被诟病呢?原因就在于JMS的设计是J2EE规范,站在Java开发的角度思考问题。但是现实往往是复杂度很高的。比如我有一个.NET开发的系统A,有一个Java开发的系统B,现在要从A系统给B系统发业务消息,结果两边数据格式不统一,没法操作。JMS不是可以统一数据格式吗?提供了6种数据种类,总有一款适合你啊。NO,一个都不能用。因为A系统的底层语言不是Java语言开发的,根本不支持那些对象。这就意味着如果想使用现有的业务系统A继续开发已经不可能了,必须推翻重新做使用Java语言开发的A系统。

​ 这时候有人就提出说,你搞那么复杂,整那么多种类干什么?找一种大家都支持的消息数据类型不就解决这个跨平台的问题了吗?大家一想,对啊,于是AMQP孕育而生。

​ 单从上面的说明中其实可以明确感知到,AMQP的出现解决的是消息传递时使用的消息种类的问题,化繁为简,但是其并没有完全推翻JMS的操作API,所以说AMQP仅仅是一种协议,规范了数据传输的格式而已。

​ AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS操作。
优点

​ 具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现

JMS消息种类

​ AMQP消息种类:byte[]

​ AMQP在JMS的消息模型基础上又进行了进一步的扩展,除了点对点和发布订阅的模型,开发了几种全新的消息模型,适应各种各样的消息发送。

AMQP消息模型

  • direct exchange
  • fanout exchange
  • topic exchange
  • headers exchange
  • system exchange

​ 目前实现了AMQP协议的消息中间件技术也很多,而且都是较为流行的技术,例如:RabbitMQ、StormMQ、RocketMQ

MQTT

​ MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一。由于与JavaEE企业级开发没有交集,此处不作过多的说明。

​ 除了上述3种J2EE企业级应用中广泛使用的三种异步消息传递技术,还有一种技术也不能忽略,Kafka。

KafKa

​ Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能。Kafka技术并不是作为消息中间件为主要功能的产品,但是其拥有发布订阅的工作模式,也可以充当消息中间件来使用,而且目前企业级开发中其身影也不少见。

​ 本节内容讲围绕着上述内容中的几种实现方案讲解springboot整合各种各样的消息中间件。由于各种消息中间件必须先安装再使用,下面的内容采用Windows系统安装,降低各位学习者的学习难度,基本套路和之前学习NoSQL解决方案一样,先安装再整合。

购物订单发送手机短信案例

​ 为了便于下面演示各种各样的消息中间件技术,我们创建一个购物过程生成订单时为用户发送短信的案例环境,模拟使用消息中间件实现发送手机短信的过程。

​ 手机验证码案例需求如下:

  • 执行下单业务时(模拟此过程),调用消息服务,将要发送短信的订单id传递给消息中间件

  • 消息处理服务接收到要发送的订单id后输出订单id(模拟发短信)

    由于不涉及数据读写,仅开发业务层与表现层,其中短信处理的业务代码独立开发,代码如下:

订单业务

业务层接口

1
2
3
public interface OrderService {
void order(String id);
}

​ 模拟传入订单id,执行下订单业务,参数为虚拟设定,实际应为订单对应的实体类

业务层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private MessageService messageService;

@Override
public void order(String id) {
//一系列操作,包含各种服务调用,处理各种业务
System.out.println("订单处理开始");
//短信消息处理
messageService.sendMessage(id);
System.out.println("订单处理结束");
System.out.println();
}
}

​ 业务层转调短信处理的服务MessageService

表现层服务

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/orders")
public class OrderController {

@Autowired
private OrderService orderService;

@PostMapping("{id}")
public void order(@PathVariable String id){
orderService.order(id);
}
}

​ 表现层对外开发接口,传入订单id即可(模拟)

短信处理业务

业务层接口

1
2
3
4
public interface MessageService {
void sendMessage(String id);
String doMessage();
}

​ 短信处理业务层接口提供两个操作,发送要处理的订单id到消息中间件,另一个操作目前暂且设计成处理消息,实际消息的处理过程不应该是手动执行,应该是自动执行,到具体实现时再进行设计

业务层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class MessageServiceImpl implements MessageService {
private ArrayList<String> msgList = new ArrayList<String>();

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
msgList.add(id);
}

@Override
public String doMessage() {
String id = msgList.remove(0);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}

​ 短信处理业务层实现中使用集合先模拟消息队列,观察效果

表现层服务

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/msgs")
public class MessageController {

@Autowired
private MessageService messageService;

@GetMapping
public String doMessage(){
String id = messageService.doMessage();
return id;
}
}

​ 短信处理表现层接口暂且开发出一个处理消息的入口,但是此业务是对应业务层中设计的模拟接口,实际业务不需要设计此接口。

​ 下面开启springboot整合各种各样的消息中间件,从严格满足JMS规范的ActiveMQ开始

SpringBoot整合ActiveMQ

​ ActiveMQ是MQ产品中的元老级产品,早期标准MQ产品之一,在AMQP协议没有出现之前,占据了消息中间件市场的绝大部分份额,后期因为AMQP系列产品的出现,迅速走弱,目前仅在一些线上运行的产品中出现,新产品开发较少采用。

安装

​ windows版安装包下载地址:https://activemq.apache.org/components/classic/download/

​ 下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件

image-20220228160001620

启动服务器

1
activemq.bat

​ 运行bin目录下的win32或win64目录下的activemq.bat命令即可,根据自己的操作系统选择即可,默认对外服务端口61616。

访问web管理服务

​ ActiveMQ启动后会启动一个Web控制台服务,可以通过该服务管理ActiveMQ。

1
http://127.0.0.1:8161/

​ web管理服务默认端口8161,访问后可以打开ActiveMQ的管理界面,如下:

image-20220228160844972

​ 首先输入访问用户名和密码,初始化用户名和密码相同,均为:admin,成功登录后进入管理后台界面,如下:

image-20220228161010401

​ 看到上述界面视为启动ActiveMQ服务成功。

启动失败

​ 在ActiveMQ启动时要占用多个端口,以下为正常启动信息:

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
wrapper  | --> Wrapper Started as Console
wrapper | Launching a JVM...
jvm 1 | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1 | Copyright 1999-2006 Tanuki Software, Inc. All Rights Reserved.
jvm 1 |
jvm 1 | Java Runtime: Oracle Corporation 1.8.0_172 D:\soft\jdk1.8.0_172\jre
jvm 1 | Heap sizes: current=249344k free=235037k max=932352k
jvm 1 | JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=7ySrCD75XhLCpLjd -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=9364 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
jvm 1 | Extensions classpath:
jvm 1 | [..\..\lib,..\..\lib\camel,..\..\lib\optional,..\..\lib\web,..\..\lib\extra]
jvm 1 | ACTIVEMQ_HOME: ..\..
jvm 1 | ACTIVEMQ_BASE: ..\..
jvm 1 | ACTIVEMQ_CONF: ..\..\conf
jvm 1 | ACTIVEMQ_DATA: ..\..\data
jvm 1 | Loading message broker from: xbean:activemq.xml
jvm 1 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@5f3ebfe0: startup date [Mon Feb 28 16:07:48 CST 2022]; root of context hierarchy
jvm 1 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:\soft\activemq\bin\win64\..\..\data\kahadb]
jvm 1 | INFO | KahaDB is version 7
jvm 1 | INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] started
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) is starting
jvm 1 | INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector openwire started
jvm 1 | INFO | Listening for connections at: amqp://CZBK-20210302VL:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector amqp started
jvm 1 | INFO | Listening for connections at: stomp://CZBK-20210302VL:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector stomp started
jvm 1 | INFO | Listening for connections at: mqtt://CZBK-20210302VL:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector mqtt started
jvm 1 | INFO | Starting Jetty server
jvm 1 | INFO | Creating Jetty connector
jvm 1 | WARN | ServletContext@o.e.j.s.ServletContextHandler@7350746f{/,null,STARTING} has uncovered http methods for path: /
jvm 1 | INFO | Listening for connections at ws://CZBK-20210302VL:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector ws started
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) started
jvm 1 | INFO | For help or more information please see: http://activemq.apache.org
jvm 1 | WARN | Store limit is 102400 mb (current store usage is 0 mb). The data directory: D:\soft\activemq\bin\win64\..\..\data\kahadb only has 68936 mb of usable space. - resetting to maximum available disk space: 68936 mb
jvm 1 | INFO | ActiveMQ WebConsole available at http://127.0.0.1:8161/
jvm 1 | INFO | ActiveMQ Jolokia REST API available at http://127.0.0.1:8161/api/jolokia/

​ 其中占用的端口有:61616、5672、61613、1883、61614,如果启动失败,请先管理对应端口即可。以下就是某个端口占用的报错信息,可以从抛出异常的位置看出,启动5672端口时端口被占用,显示java.net.BindException: Address already in use: JVM_Bind。Windows系统中终止端口运行的操作参看【命令行启动常见问题及解决方案】

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
wrapper  | --> Wrapper Started as Console
wrapper | Launching a JVM...
jvm 1 | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1 | Copyright 1999-2006 Tanuki Software, Inc. All Rights Reserved.
jvm 1 |
jvm 1 | Java Runtime: Oracle Corporation 1.8.0_172 D:\soft\jdk1.8.0_172\jre
jvm 1 | Heap sizes: current=249344k free=235038k max=932352k
jvm 1 | JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=QPJoy9ZoXeWmmwTS -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=14836 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
jvm 1 | Extensions classpath:
jvm 1 | [..\..\lib,..\..\lib\camel,..\..\lib\optional,..\..\lib\web,..\..\lib\extra]
jvm 1 | ACTIVEMQ_HOME: ..\..
jvm 1 | ACTIVEMQ_BASE: ..\..
jvm 1 | ACTIVEMQ_CONF: ..\..\conf
jvm 1 | ACTIVEMQ_DATA: ..\..\data
jvm 1 | Loading message broker from: xbean:activemq.xml
jvm 1 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
jvm 1 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:\soft\activemq\bin\win64\..\..\data\kahadb]
jvm 1 | INFO | KahaDB is version 7
jvm 1 | INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] started
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is starting
jvm 1 | INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1 | INFO | Connector openwire started
jvm 1 | ERROR | Failed to start Apache ActiveMQ (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1)
jvm 1 | java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1 | at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:28)
jvm 1 | at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2288)
jvm 1 | at org.apache.activemq.broker.BrokerService.startTransportConnector(BrokerService.java:2769)
jvm 1 | at org.apache.activemq.broker.BrokerService.startAllConnectors(BrokerService.java:2665)
jvm 1 | at org.apache.activemq.broker.BrokerService.doStartBroker(BrokerService.java:780)
jvm 1 | at org.apache.activemq.broker.BrokerService.startBroker(BrokerService.java:742)
jvm 1 | at org.apache.activemq.broker.BrokerService.start(BrokerService.java:645)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1748)
jvm 1 | at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1685)
jvm 1 | at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1615)
jvm 1 | at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
jvm 1 | at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
jvm 1 | at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
jvm 1 | at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
jvm 1 | at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
jvm 1 | at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
jvm 1 | at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:756)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1 | at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1 | at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1 | at java.lang.Thread.run(Thread.java:748)
jvm 1 | Caused by: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1 | at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:34)
jvm 1 | at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:146)
jvm 1 | at org.apache.activemq.transport.tcp.TcpTransportFactory.doBind(TcpTransportFactory.java:62)
jvm 1 | at org.apache.activemq.transport.TransportFactorySupport.bind(TransportFactorySupport.java:40)
jvm 1 | at org.apache.activemq.broker.TransportConnector.createTransportServer(TransportConnector.java:335)
jvm 1 | at org.apache.activemq.broker.TransportConnector.getServer(TransportConnector.java:145)
jvm 1 | at org.apache.activemq.broker.TransportConnector.asManagedConnector(TransportConnector.java:110)
jvm 1 | at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2283)
jvm 1 | ... 46 more
jvm 1 | Caused by: java.net.BindException: Address already in use: JVM_Bind
jvm 1 | at java.net.DualStackPlainSocketImpl.bind0(Native Method)
jvm 1 | at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
jvm 1 | at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
jvm 1 | at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
jvm 1 | at java.net.ServerSocket.bind(ServerSocket.java:375)
jvm 1 | at java.net.ServerSocket.<init>(ServerSocket.java:237)
jvm 1 | at javax.net.DefaultServerSocketFactory.createServerSocket(ServerSocketFactory.java:231)
jvm 1 | at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:143)
jvm 1 | ... 52 more
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutting down
jvm 1 | INFO | socketQueue interrupted - stopping
jvm 1 | INFO | Connector openwire stopped
jvm 1 | INFO | Could not accept connection during shutdown : null (null)
jvm 1 | INFO | Connector amqp stopped
jvm 1 | INFO | Connector stomp stopped
jvm 1 | INFO | Connector mqtt stopped
jvm 1 | INFO | Connector ws stopped
jvm 1 | INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] stopped
jvm 1 | INFO | Stopping async queue tasks
jvm 1 | INFO | Stopping async topic tasks
jvm 1 | INFO | Stopped KahaDB
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) uptime 0.426 seconds
jvm 1 | INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutdown
jvm 1 | INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
jvm 1 | WARN | Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.apache.activemq.xbean.XBeanBrokerService#0' defined in class path resource [activemq.xml]: Invocation of init method failed; nested exception is java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1 | ERROR: java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1 | java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1 | at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:91)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1 | at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1 | at java.lang.Thread.run(Thread.java:748)
jvm 1 | Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1 | at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1 | at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1 | ... 16 more
jvm 1 | ERROR: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1 | java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1 | at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
jvm 1 | at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1 | at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1 | at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1 | at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1 | at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1 | at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1 | at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1 | at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1 | at java.lang.Thread.run(Thread.java:748)
wrapper | <-- Wrapper Stopped
请按任意键继续. . .
整合

​ 做了这么多springboot整合第三方技术,已经摸到门路了,加坐标,做配置,调接口,直接开工

步骤①:导入springboot整合ActiveMQ的starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

步骤②:配置ActiveMQ的服务器地址

1
2
3
spring:
activemq:
broker-url: tcp://localhost:61616

步骤③:使用JmsMessagingTemplate操作ActiveMQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class MessageServiceActivemqImpl implements MessageService {
@Autowired
private JmsMessagingTemplate messagingTemplate;

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
messagingTemplate.convertAndSend("order.queue.id",id);
}

@Override
public String doMessage() {
String id = messagingTemplate.receiveAndConvert("order.queue.id",String.class);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}

​ 发送消息需要先将消息的类型转换成字符串,然后再发送,所以是convertAndSend,定义消息发送的位置,和具体的消息内容,此处使用id作为消息内容。

​ 接收消息需要先将消息接收到,然后再转换成指定的数据类型,所以是receiveAndConvert,接收消息除了提供读取的位置,还要给出转换后的数据的具体类型。

步骤④:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息

1
2
3
4
5
6
7
8
9
@Component
public class MessageListener {
@JmsListener(destination = "order.queue.id")
@SendTo("order.other.queue.id")
public String receive(String id){
System.out.println("已完成短信发送业务,id:"+id);
return "new:"+id;
}
}

​ 使用注解@JmsListener定义当前方法监听ActiveMQ中指定名称的消息队列。

​ 如果当前消息队列处理完还需要继续向下传递当前消息到另一个队列中使用注解@SendTo即可,这样即可构造连续执行的顺序消息队列。

步骤⑤:切换消息模型由点对点模型到发布订阅模型,修改jms配置即可

1
2
3
4
5
spring:
activemq:
broker-url: tcp://localhost:61616
jms:
pub-sub-domain: true

​ pub-sub-domain默认值为false,即点对点模型,修改为true后就是发布订阅模型。

总结

  1. springboot整合ActiveMQ提供了JmsMessagingTemplate对象作为客户端操作消息队列
  2. 操作ActiveMQ需要配置ActiveMQ服务器地址,默认端口61616
  3. 企业开发时通常使用监听器来处理消息队列中的消息,设置监听器使用注解@JmsListener
  4. 配置jms的pub-sub-domain属性可以在点对点模型和发布订阅模型间切换消息模型

SpringBoot整合RabbitMQ

​ RabbitMQ是MQ产品中的目前较为流行的产品之一,它遵从AMQP协议。RabbitMQ的底层实现语言使用的是Erlang,所以安装RabbitMQ需要先安装Erlang。

Erlang安装

​ windows版安装包下载地址:https://www.erlang.org/downloads

​ 下载完毕后得到exe安装文件,一键傻瓜式安装,安装完毕需要重启,需要重启,需要重启。

​ 安装的过程中可能会出现依赖Windows组件的提示,根据提示下载安装即可,都是自动执行的,如下:

image-20220228164851551

​ Erlang安装后需要配置环境变量,否则RabbitMQ将无法找到安装的Erlang。需要配置项如下,作用等同JDK配置环境变量的作用。

  • ERLANG_HOME
  • PATH
安装

​ windows版安装包下载地址:https://rabbitmq.com/install-windows.html

​ 下载完毕后得到exe安装文件,一键傻瓜式安装,安装完毕后会得到如下文件

image-20220228165151524

启动服务器

1
2
3
rabbitmq-service.bat start		# 启动服务
rabbitmq-service.bat stop # 停止服务
rabbitmqctl status # 查看服务状态

​ 运行sbin目录下的rabbitmq-service.bat命令即可,start参数表示启动,stop参数表示退出,默认对外服务端口5672。

​ 注意:启动rabbitmq的过程实际上是开启rabbitmq对应的系统服务,需要管理员权限方可执行。

​ 说明:有没有感觉5672的服务端口很熟悉?activemq与rabbitmq有一个端口冲突问题,学习阶段无论操作哪一个?请确保另一个处于关闭状态。

​ 说明:不喜欢命令行的小伙伴可以使用任务管理器中的服务页,找到RabbitMQ服务,使用鼠标右键菜单控制服务的启停。

image-20220228170147193

访问web管理服务

​ RabbitMQ也提供有web控制台服务,但是此功能是一个插件,需要先启用才可以使用。

1
2
rabbitmq-plugins.bat list							# 查看当前所有插件的运行状态
rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件

​ 启动插件后可以在插件运行状态中查看是否运行,运行后通过浏览器即可打开服务后台管理界面

1
http://localhost:15672

​ web管理服务默认端口15672,访问后可以打开RabbitMQ的管理界面,如下:

image-20220228170504793

​ 首先输入访问用户名和密码,初始化用户名和密码相同,均为:guest,成功登录后进入管理后台界面,如下:

image-20220228170535261

整合(direct模型)

​ RabbitMQ满足AMQP协议,因此不同的消息模型对应的制作不同,先使用最简单的direct模型开发。

步骤①:导入springboot整合amqp的starter,amqp协议默认实现为rabbitmq方案

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

步骤②:配置RabbitMQ的服务器地址

1
2
3
4
spring:
rabbitmq:
host: localhost
port: 5672

步骤③:初始化直连模式系统设置

​ 由于RabbitMQ不同模型要使用不同的交换机,因此需要先初始化RabbitMQ相关的对象,例如队列,交换机等

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 RabbitConfigDirect {
@Bean
public Queue directQueue(){
return new Queue("direct_queue");
}
@Bean
public Queue directQueue2(){
return new Queue("direct_queue2");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding bindingDirect(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
}
@Bean
public Binding bindingDirect2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2");
}
}

​ 队列Queue与直连交换机DirectExchange创建后,还需要绑定他们之间的关系Binding,这样就可以通过交换机操作对应队列。

步骤④:使用AmqpTemplate操作RabbitMQ

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MessageServiceRabbitmqDirectImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq direct),id:"+id);
amqpTemplate.convertAndSend("directExchange","direct",id);
}
}

​ amqp协议中的操作API接口名称看上去和jms规范的操作API接口很相似,但是传递参数差异很大。

步骤⑤:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息

1
2
3
4
5
6
7
@Component
public class MessageListener {
@RabbitListener(queues = "direct_queue")
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq direct),id:"+id);
}
}

​ 使用注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列。

整合(topic模型)

步骤①:同上

步骤②:同上

步骤③:初始化主题模式系统设置

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 RabbitConfigTopic {
@Bean
public Queue topicQueue(){
return new Queue("topic_queue");
}
@Bean
public Queue topicQueue2(){
return new Queue("topic_queue2");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Binding bindingTopic(){
return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id");
}
@Bean
public Binding bindingTopic2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.orders.*");
}
}

​ 主题模式支持routingKey匹配模式,*表示匹配一个单词,#表示匹配任意内容,这样就可以通过主题交换机将消息分发到不同的队列中,详细内容请参看RabbitMQ系列课程。

匹配键topic.*.*topic.#
topic.order.idtruetrue
order.topic.idfalsefalse
topic.sm.order.idfalsetrue
topic.sm.idfalsetrue
topic.id.ordertruetrue
topic.idfalsetrue
topic.orderfalsetrue

步骤④:使用AmqpTemplate操作RabbitMQ

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MessageServiceRabbitmqTopicImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq topic),id:"+id);
amqpTemplate.convertAndSend("topicExchange","topic.orders.id",id);
}
}

​ 发送消息后,根据当前提供的routingKey与绑定交换机时设定的routingKey进行匹配,规则匹配成功消息才会进入到对应的队列中。

步骤⑤:使用消息监听器在服务器启动后,监听指定队列

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MessageListener {
@RabbitListener(queues = "topic_queue")
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq topic 1),id:"+id);
}
@RabbitListener(queues = "topic_queue2")
public void receive2(String id){
System.out.println("已完成短信发送业务(rabbitmq topic 22222222),id:"+id);
}
}

​ 使用注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列。

总结

  1. springboot整合RabbitMQ提供了AmqpTemplate对象作为客户端操作消息队列
  2. 操作ActiveMQ需要配置ActiveMQ服务器地址,默认端口5672
  3. 企业开发时通常使用监听器来处理消息队列中的消息,设置监听器使用注解@RabbitListener
  4. RabbitMQ有5种消息模型,使用的队列相同,但是交换机不同。交换机不同,对应的消息进入的策略也不同

SpringBoot整合RocketMQ

​ RocketMQ由阿里研发,后捐赠给apache基金会,目前是apache基金会顶级项目之一,也是目前市面上的MQ产品中较为流行的产品之一,它遵从AMQP协议。

安装

​ windows版安装包下载地址:https://rocketmq.apache.org/

​ 下载完毕后得到zip压缩文件,解压缩即可使用,解压后得到如下文件

image-20220228174453471

​ RocketMQ安装后需要配置环境变量,具体如下:

  • ROCKETMQ_HOME
  • PATH
  • NAMESRV_ADDR (建议): 127.0.0.1:9876

​ 关于NAMESRV_ADDR对于初学者来说建议配置此项,也可以通过命令设置对应值,操作略显繁琐,建议配置。系统学习RocketMQ知识后即可灵活控制该项。

RocketMQ工作模式

​ 在RocketMQ中,处理业务的服务器称为broker,生产者与消费者不是直接与broker联系的,而是通过命名服务器进行通信。broker启动后会通知命名服务器自己已经上线,这样命名服务器中就保存有所有的broker信息。当生产者与消费者需要连接broker时,通过命名服务器找到对应的处理业务的broker,因此命名服务器在整套结构中起到一个信息中心的作用。并且broker启动前必须保障命名服务器先启动。

image-20220228175123790

启动服务器

1
2
mqnamesrv		# 启动命名服务器
mqbroker # 启动broker

​ 运行bin目录下的mqnamesrv命令即可启动命名服务器,默认对外服务端口9876。

​ 运行bin目录下的mqbroker命令即可启动broker服务器,如果环境变量中没有设置NAMESRV_ADDR则需要在运行mqbroker指令前通过set指令设置NAMESRV_ADDR的值,并且每次开启均需要设置此项。

测试服务器启动状态

​ RocketMQ提供有一套测试服务器功能的测试程序,运行bin目录下的tools命令即可使用。

1
2
tools org.apache.rocketmq.example.quickstart.Producer		# 生产消息
tools org.apache.rocketmq.example.quickstart.Consumer # 消费消息
整合(异步消息)

步骤①:导入springboot整合RocketMQ的starter,此坐标不由springboot维护版本

1
2
3
4
5
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>

步骤②:配置RocketMQ的服务器地址

1
2
3
4
rocketmq:
name-server: localhost:9876
producer:
group: group_rocketmq

​ 设置默认的生产者消费者所属组group。

步骤③:使用RocketMQTemplate操作RocketMQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class MessageServiceRocketmqImpl implements MessageService {
@Autowired
private RocketMQTemplate rocketMQTemplate;

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rocketmq),id:"+id);
SendCallback callback = new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("消息发送成功");
}
@Override
public void onException(Throwable e) {
System.out.println("消息发送失败!!!!!");
}
};
rocketMQTemplate.asyncSend("order_id",id,callback);
}
}

​ 使用asyncSend方法发送异步消息。

步骤④:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息

1
2
3
4
5
6
7
8
@Component
@RocketMQMessageListener(topic = "order_id",consumerGroup = "group_rocketmq")
public class MessageListener implements RocketMQListener<String> {
@Override
public void onMessage(String id) {
System.out.println("已完成短信发送业务(rocketmq),id:"+id);
}
}

​ RocketMQ的监听器必须按照标准格式开发,实现RocketMQListener接口,泛型为消息类型。

​ 使用注解@RocketMQMessageListener定义当前类监听RabbitMQ中指定组、指定名称的消息队列。

总结

  1. springboot整合RocketMQ使用RocketMQTemplate对象作为客户端操作消息队列
  2. 操作RocketMQ需要配置RocketMQ服务器地址,默认端口9876
  3. 企业开发时通常使用监听器来处理消息队列中的消息,设置监听器使用注解@RocketMQMessageListener

SpringBoot整合Kafka

安装

​ windows版安装包下载地址:https://kafka.apache.org/downloads

​ 下载完毕后得到tgz压缩文件,使用解压缩软件解压缩即可使用,解压后得到如下文件

image-20220228181442155

​ 建议使用windows版2.8.1版本。

启动服务器

​ kafka服务器的功能相当于RocketMQ中的broker,kafka运行还需要一个类似于命名服务器的服务。在kafka安装目录中自带一个类似于命名服务器的工具,叫做zookeeper,它的作用是注册中心,相关知识请到对应课程中学习。

1
2
zookeeper-server-start.bat ..\..\config\zookeeper.properties		# 启动zookeeper
kafka-server-start.bat ..\..\config\server.properties # 启动kafka

​ 运行bin目录下的windows目录下的zookeeper-server-start命令即可启动注册中心,默认对外服务端口2181。

​ 运行bin目录下的windows目录下的kafka-server-start命令即可启动kafka服务器,默认对外服务端口9092。

创建主题

​ 和之前操作其他MQ产品相似,kakfa也是基于主题操作,操作之前需要先初始化topic。

1
2
3
4
5
6
# 创建topic
kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic itheima
# 查询topic
kafka-topics.bat --zookeeper 127.0.0.1:2181 --list
# 删除topic
kafka-topics.bat --delete --zookeeper localhost:2181 --topic itheima

测试服务器启动状态

​ Kafka提供有一套测试服务器功能的测试程序,运行bin目录下的windows目录下的命令即可使用。

1
2
kafka-console-producer.bat --broker-list localhost:9092 --topic itheima							# 测试生产消息
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic itheima --from-beginning # 测试消息消费
整合

步骤①:导入springboot整合Kafka的starter,此坐标由springboot维护版本

1
2
3
4
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

步骤②:配置Kafka的服务器地址

1
2
3
4
5
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: order

​ 设置默认的生产者消费者所属组id。

步骤③:使用KafkaTemplate操作Kafka

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MessageServiceKafkaImpl implements MessageService {
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;

@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(kafka),id:"+id);
kafkaTemplate.send("itheima2022",id);
}
}

​ 使用send方法发送消息,需要传入topic名称。

步骤④:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息

1
2
3
4
5
6
7
@Component
public class MessageListener {
@KafkaListener(topics = "itheima2022")
public void onMessage(ConsumerRecord<String,String> record){
System.out.println("已完成短信发送业务(kafka),id:"+record.value());
}
}

​ 使用注解@KafkaListener定义当前方法监听Kafka中指定topic的消息,接收到的消息封装在对象ConsumerRecord中,获取数据从ConsumerRecord对象中获取即可。

总结

  1. springboot整合Kafka使用KafkaTemplate对象作为客户端操作消息队列

  2. 操作Kafka需要配置Kafka服务器地址,默认端口9092

  3. 企业开发时通常使用监听器来处理消息队列中的消息,设置监听器使用注解@KafkaListener。接收消息保存在形参ConsumerRecord对象中

KF-6.监控

​ 在说监控之前,需要回顾一下软件业的发展史。最早的软件完成一些非常简单的功能,代码不多,错误也少。随着软件功能的逐步完善,软件的功能变得越来越复杂,功能不能得到有效的保障,这个阶段出现了针对软件功能的检测,也就是软件测试。伴随着计算机操作系统的逐步升级,软件的运行状态也变得开始让人捉摸不透,出现了不稳定的状况。伴随着计算机网络的发展,程序也从单机状态切换成基于计算机网络的程序,应用于网络的程序开始出现,由于网络的不稳定性,程序的运行状态让使用者更加堪忧。互联网的出现彻底打破了软件的思维模式,随之而来的互联网软件就更加凸显出应对各种各样复杂的网络情况之下的弱小。计算机软件的运行状况已经成为了软件运行的一个大话题,针对软件的运行状况就出现了全新的思维,建立起了初代的软件运行状态监控。

​ 什么是监控?就是通过软件的方式展示另一个软件的运行情况,运行的情况则通过各种各样的指标数据反馈给监控人员。例如网络是否顺畅、服务器是否在运行、程序的功能是否能够整百分百运行成功,内存是否够用,等等等等。

​ 本章要讲解的监控就是对软件的运行情况进行监督,但是springboot程序与非springboot程序的差异还是很大的,为了方便监控软件的开发,springboot提供了一套功能接口,为开发者加速开发过程。

KF-6-1.监控的意义

​ 对于现代的互联网程序来说,规模越来越大,功能越来越复杂,还要追求更好的客户体验,因此要监控的信息量也就比较大了。由于现在的互联网程序大部分都是基于微服务的程序,一个程序的运行需要若干个服务来保障,因此第一个要监控的指标就是服务是否正常运行,也就是监控服务状态是否处理宕机状态。一旦发现某个服务宕机了,必须马上给出对应的解决方案,避免整体应用功能受影响。其次,由于互联网程序服务的客户量是巨大的,当客户的请求在短时间内集中达到服务器后,就会出现各种程序运行指标的波动。比如内存占用严重,请求无法及时响应处理等,这就是第二个要监控的重要指标,监控服务运行指标。虽然软件是对外提供用户的访问需求,完成对应功能的,但是后台的运行是否平稳,是否出现了不影响客户使用的功能隐患,这些也是要密切监控的,此时就需要在不停机的情况下,监控系统运行情况,日志是一个不错的手段。如果在众多日志中找到开发者或运维人员所关注的日志信息,简单快速有效的过滤出要看的日志也是监控系统需要考虑的问题,这就是第三个要监控的指标,监控程序运行日志。虽然我们期望程序一直平稳运行,但是由于突发情况的出现,例如服务器被攻击、服务器内存溢出等情况造成了服务器宕机,此时当前服务不能满足使用需要,就要将其重启甚至关闭,如果快速控制服务器的启停也是程序运行过程中不可回避的问题,这就是第四个监控项,管理服务状态。以上这些仅仅是从大的方面来思考监控这个问题,还有很多的细节点,例如上线了一个新功能,定时提醒用户续费,这种功能不是上线后马上就运行的,但是当前功能是否真的启动,如果快速的查询到这个功能已经开启,这也是监控中要解决的问题,等等。看来监控真的是一项非常重要的工作。

​ 通过上述描述,可以看出监控很重要。那具体的监控要如何开展呢?还要从实际的程序运行角度出发。比如现在有3个服务支撑着一个程序的运行,每个服务都有自己的运行状态。

image-20220301093704396

​ 此时被监控的信息就要在三个不同的程序中去查询并展示,但是三个服务是服务于一个程序的运行的,如果不能合并到一个平台上展示,监控工作量巨大,而且信息对称性差,要不停的在三个监控端查看数据。如果将业务放大成30个,300个,3000个呢?看来必须有一个单独的平台,将多个被监控的服务对应的监控指标信息汇总在一起,这样更利于监控工作的开展。

image-20220301094001896

​ 新的程序专门用来监控,新的问题就出现了,是被监控程序主动上报信息还是监控程序主动获取信息?如果监控程序不能主动获取信息,这就意味着监控程序有可能看到的是很久之前被监控程序上报的信息,万一被监控程序宕机了,监控程序就无法区分究竟是好久没法信息了,还是已经下线了。所以监控程序必须具有主动发起请求获取被监控服务信息的能力。

image-20220301094259844

​ 如果监控程序要监控服务时,主动获取对方的信息。那监控程序如何知道哪些程序被自己监控呢?不可能在监控程序中设置我监控谁,这样互联网上的所有程序岂不是都可以被监控到,这样的话信息安全将无法得到保障。合理的做法只能是在被监控程序启动时上报监控程序,告诉监控程序你可以监控我了。看来需要在被监控程序端做主动上报的操作,这就要求被监控程序中配置对应的监控程序是谁。

image-20220301094547748

​ 被监控程序可以提供各种各样的指标数据给监控程序看,但是每一个指标都代表着公司的机密信息,并不是所有的指标都可以给任何人看的,乃至运维人员,所以对被监控指标的是否开放出来给监控系统看,也需要做详细的设定。

​ 以上描述的整个过程就是一个监控系统的基本流程。

总结

  1. 监控是一个非常重要的工作,是保障程序正常运行的基础手段
  2. 监控的过程通过一个监控程序进行,它汇总所有被监控的程序的信息集中统一展示
  3. 被监控程序需要主动上报自己被监控,同时要设置哪些指标被监控

思考

​ 下面就要开始做监控了,新的问题就来了,监控程序怎么做呢?难道要自己写吗?肯定是不现实的,如何进行监控,咱们下节再讲。

KF-6-2.可视化监控平台

​ springboot抽取了大部分监控系统的常用指标,提出了监控的总思想。然后就有好心的同志根据监控的总思想,制作了一个通用性很强的监控系统,因为是基于springboot监控的核心思想制作的,所以这个程序被命名为Spring Boot Admin

​ Spring Boot Admin,这是一个开源社区项目,用于管理和监控SpringBoot应用程序。这个项目中包含有客户端和服务端两部分,而监控平台指的就是服务端。我们做的程序如果需要被监控,将我们做的程序制作成客户端,然后配置服务端地址后,服务端就可以通过HTTP请求的方式从客户端获取对应的信息,并通过UI界面展示对应信息。

​ 下面就来开发这套监控程序,先制作服务端,其实服务端可以理解为是一个web程序,收到一些信息后展示这些信息。

服务端开发

步骤①:导入springboot admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.5.4</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

​ 上述过程可以通过创建项目时使用勾选的形式完成。

image-20220301102432817

步骤②:在引导类上添加注解@EnableAdminServer,声明当前应用启动后作为SpringBootAdmin的服务器使用

1
2
3
4
5
6
7
@SpringBootApplication
@EnableAdminServer
public class Springboot25AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot25AdminServerApplication.class, args);
}
}

​ 做到这里,这个服务器就开发好了,启动后就可以访问当前程序了,界面如下。

image-20220301103028468

​ 由于目前没有启动任何被监控的程序,所以里面什么信息都没有。下面制作一个被监控的客户端程序。

客户端开发

​ 客户端程序开发其实和服务端开发思路基本相似,多了一些配置而已。

步骤①:导入springboot admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.5.4</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

​ 上述过程也可以通过创建项目时使用勾选的形式完成,不过一定要小心,端口配置成不一样的,否则会冲突。

步骤②:设置当前客户端将信息上传到哪个服务器上,通过yml文件配置

1
2
3
4
5
spring:
boot:
admin:
client:
url: http://localhost:8080

​ 做到这里,这个客户端就可以启动了。启动后再次访问服务端程序,界面如下。

image-20220301103838079

​ 可以看到,当前监控了1个程序,点击进去查看详细信息。

image-20220301103936386

​ 由于当前没有设置开放哪些信息给监控服务器,所以目前看不到什么有效的信息。下面需要做两组配置就可以看到信息了。

  1. 开放指定信息给服务器看

  2. 允许服务器以HTTP请求的方式获取对应的信息

    配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 80
spring:
boot:
admin:
client:
url: http://localhost:8080
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"

​ 上述配置对于初学者来说比较容易混淆。简单解释一下,到下一节再做具体的讲解。springbootadmin的客户端默认开放了13组信息给服务器,但是这些信息除了一个之外,其他的信息都不让通过HTTP请求查看。所以你看到的信息基本上就没什么内容了,只能看到一个内容,就是下面的健康信息。

image-20220301104742563

​ 但是即便如此我们看到健康信息中也没什么内容,原因在于健康信息中有一些信息描述了你当前应用使用了什么技术等信息,如果无脑的对外暴露功能会有安全隐患。通过配置就可以开放所有的健康信息明细查看了。

1
2
3
4
management:
endpoint:
health:
show-details: always

​ 健康明细信息如下:

image-20220301105116554

​ 目前除了健康信息,其他信息都查阅不了。原因在于其他12种信息是默认不提供给服务器通过HTTP请求查阅的,所以需要开启查阅的内容项,使用*表示查阅全部。记得带引号。

1
2
3
4
endpoints:
web:
exposure:
include: "*"

​ 配置后再刷新服务器页面,就可以看到所有的信息了。

image-20220301105554494

​ 以上界面中展示的信息量就非常大了,包含了13组信息,有性能指标监控,加载的bean列表,加载的系统属性,日志的显示控制等等。

配置多个客户端

​ 可以通过配置客户端的方式在其他的springboot程序中添加客户端坐标,这样当前服务器就可以监控多个客户端程序了。每个客户端展示不同的监控信息。

image-20220301110352170

​ 进入监控面板,如果你加载的应用具有功能,在监控面板中可以看到3组信息展示的与之前加载的空工程不一样。

  • 类加载面板中可以查阅到开发者自定义的类,如左图

image-20220301161246835image-20220301161949431

  • 映射中可以查阅到当前应用配置的所有请求

image-20220301161418791image-20220301162008737

  • 性能指标中可以查阅当前应用独有的请求路径统计数据

image-20220301161906949image-20220301162040670

总结

  1. 开发监控服务端需要导入坐标,然后在引导类上添加注解@EnableAdminServer,并将其配置成web程序即可
  2. 开发被监控的客户端需要导入坐标,然后配置服务端服务器地址,并做开放指标的设定即可
  3. 在监控平台中可以查阅到各种各样被监控的指标,前提是客户端开放了被监控的指标

思考

​ 之前说过,服务端要想监控客户端,需要主动的获取到对应信息并展示出来。但是目前我们并没有在客户端开发任何新的功能,但是服务端确可以获取监控信息,谁帮我们做的这些功能呢?咱们下一节再讲。

KF-6-3.监控原理

​ 通过查阅监控中的映射指标,可以看到当前系统中可以运行的所有请求路径,其中大部分路径以/actuator开头

image-20220301170214076

​ 首先这些请求路径不是开发者自己编写的,其次这个路径代表什么含义呢?既然这个路径可以访问,就可以通过浏览器发送该请求看看究竟可以得到什么信息。

image-20220301170723057

​ 通过发送请求,可以得到一组json信息,如下

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
{
"_links": {
"self": {
"href": "http://localhost:81/actuator",
"templated": false
},
"beans": {
"href": "http://localhost:81/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:81/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://localhost:81/actuator/caches",
"templated": false
},
"health": {
"href": "http://localhost:81/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:81/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:81/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:81/actuator/conditions",
"templated": false
},
"shutdown": {
"href": "http://localhost:81/actuator/shutdown",
"templated": false
},
"configprops": {
"href": "http://localhost:81/actuator/configprops",
"templated": false
},
"configprops-prefix": {
"href": "http://localhost:81/actuator/configprops/{prefix}",
"templated": true
},
"env": {
"href": "http://localhost:81/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:81/actuator/env/{toMatch}",
"templated": true
},
"loggers": {
"href": "http://localhost:81/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://localhost:81/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://localhost:81/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:81/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:81/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:81/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:81/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:81/actuator/mappings",
"templated": false
}
}
}

​ 其中每一组数据都有一个请求路径,而在这里请求路径中有之前看到过的health,发送此请求又得到了一组信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": {
"total": 297042808832,
"free": 72284409856,
"threshold": 10485760,
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}

​ 当前信息与监控面板中的数据存在着对应关系

image-20220301171025615

​ 原来监控中显示的信息实际上是通过发送请求后得到json数据,然后展示出来。按照上述操作,可以发送更多的以/actuator开头的链接地址,获取更多的数据,这些数据汇总到一起组成了监控平台显示的所有数据。

​ 到这里我们得到了一个核心信息,监控平台中显示的信息实际上是通过对被监控的应用发送请求得到的。那这些请求谁开发的呢?打开被监控应用的pom文件,其中导入了springboot admin的对应的client,在这个资源中导入了一个名称叫做actuator的包。被监控的应用之所以可以对外提供上述请求路径,就是因为添加了这个包。

image-20220301171437817

​ 这个actuator是什么呢?这就是本节要讲的核心内容,监控的端点。

​ Actuator,可以称为端点,描述了一组监控信息,SpringBootAdmin提供了多个内置端点,通过访问端点就可以获取对应的监控信息,也可以根据需要自定义端点信息。通过发送请求路劲/actuator可以访问应用所有端点信息,如果端点中还有明细信息可以发送请求/actuator/端点名称来获取详细信息。以下列出了所有端点信息说明:

ID描述默认启用
auditevents暴露当前应用程序的审计事件信息。
beans显示应用程序中所有 Spring bean 的完整列表。
caches暴露可用的缓存。
conditions显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因。
configprops显示所有 @ConfigurationProperties 的校对清单。
env暴露 Spring ConfigurableEnvironment 中的属性。
flyway显示已应用的 Flyway 数据库迁移。
health显示应用程序健康信息
httptrace显示 HTTP 追踪信息(默认情况下,最后 100 个 HTTP 请求/响应交换)。
info显示应用程序信息。
integrationgraph显示 Spring Integration 图。
loggers显示和修改应用程序中日志记录器的配置。
liquibase显示已应用的 Liquibase 数据库迁移。
metrics显示当前应用程序的指标度量信息。
mappings显示所有 @RequestMapping 路径的整理清单。
scheduledtasks显示应用程序中的调度任务。
sessions允许从 Spring Session 支持的会话存储中检索和删除用户会话。当使用 Spring Session 的响应式 Web 应用程序支持时不可用。
shutdown正常关闭应用程序。
threaddump执行线程 dump。
heapdump返回一个 hprof 堆 dump 文件。
jolokia通过 HTTP 暴露 JMX bean(当 Jolokia 在 classpath 上时,不适用于 WebFlux)。
logfile返回日志文件的内容(如果已设置 logging.file 或 logging.path 属性)。支持使用 HTTP Range 头来检索部分日志文件的内容。
prometheus以可以由 Prometheus 服务器抓取的格式暴露指标。

​ 上述端点每一项代表被监控的指标,如果对外开放则监控平台可以查询到对应的端点信息,如果未开放则无法查询对应的端点信息。通过配置可以设置端点是否对外开放功能。使用enable属性控制端点是否对外开放。其中health端点为默认端点,不能关闭。

1
2
3
4
5
6
management:
endpoint:
health: # 端点名称
show-details: always
info: # 端点名称
enabled: true # 是否开放

​ 为了方便开发者快速配置端点,springboot admin设置了13个较为常用的端点作为默认开放的端点,如果需要控制默认开放的端点的开放状态,可以通过配置设置,如下:

1
2
3
management:
endpoints:
enabled-by-default: true # 是否开启默认端点,默认值true

​ 上述端点开启后,就可以通过端点对应的路径查看对应的信息了。但是此时还不能通过HTTP请求查询此信息,还需要开启通过HTTP请求查询的端点名称,使用“*”可以简化配置成开放所有端点的WEB端HTTP请求权限。

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: "*"

​ 整体上来说,对于端点的配置有两组信息,一组是endpoints开头的,对所有端点进行配置,一组是endpoint开头的,对具体端点进行配置。

1
2
3
4
5
6
7
8
9
10
11
management:
endpoint: # 具体端点的配置
health:
show-details: always
info:
enabled: true
endpoints: # 全部端点的配置
web:
exposure:
include: "*"
enabled-by-default: true

总结

  1. 被监控客户端通过添加actuator的坐标可以对外提供被访问的端点功能

  2. 端点功能的开放与关闭可以通过配置进行控制

  3. web端默认无法获取所有端点信息,通过配置开放端点功能

KF-6-4.自定义监控指标

​ 端点描述了被监控的信息,除了系统默认的指标,还可以自行添加显示的指标,下面就通过3种不同的端点的指标自定义方式来学习端点信息的二次开发。

INFO端点

​ info端点描述了当前应用的基本信息,可以通过两种形式快速配置info端点的信息

  • 配置形式

    在yml文件中通过设置info节点的信息就可以快速配置端点信息

    1
    2
    3
    4
    5
    info:
    appName: @project.artifactId@
    version: @project.version@
    company: 传智教育
    author: itheima

    配置完毕后,对应信息显示在监控平台上

    image-20220301174133248

    也可以通过请求端点信息路径获取对应json信息

    image-20220301174241310

  • 编程形式

    通过配置的形式只能添加固定的数据,如果需要动态数据还可以通过配置bean的方式为info端点添加信息,此信息与配置信息共存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class InfoConfig implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
    builder.withDetail("runTime",System.currentTimeMillis()); //添加单个信息
    Map infoMap = new HashMap();
    infoMap.put("buildTime","2006");
    builder.withDetails(infoMap); //添加一组信息
    }
    }

Health端点

​ health端点描述当前应用的运行健康指标,即应用的运行是否成功。通过编程的形式可以扩展指标信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class HealthConfig extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
boolean condition = true;
if(condition) {
builder.status(Status.UP); //设置运行状态为启动状态
builder.withDetail("runTime", System.currentTimeMillis());
Map infoMap = new HashMap();
infoMap.put("buildTime", "2006");
builder.withDetails(infoMap);
}else{
builder.status(Status.OUT_OF_SERVICE); //设置运行状态为不在服务状态
builder.withDetail("上线了吗?","你做梦");
}
}
}

​ 当任意一个组件状态不为UP时,整体应用对外服务状态为非UP状态。

image-20220301174751845

Metrics端点

​ metrics端点描述了性能指标,除了系统自带的监控性能指标,还可以自定义性能指标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
@Autowired
private BookDao bookDao;

private Counter counter;

public BookServiceImpl(MeterRegistry meterRegistry){
counter = meterRegistry.counter("用户付费操作次数:");
}

@Override
public boolean delete(Integer id) {
//每次执行删除业务等同于执行了付费业务
counter.increment();
return bookDao.deleteById(id) > 0;
}
}

​ 在性能指标中就出现了自定义的性能指标监控项

image-20220301175101812

自定义端点

​ 可以根据业务需要自定义端点,方便业务监控

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Endpoint(id="pay",enableByDefault = true)
public class PayEndpoint {
@ReadOperation
public Object getPay(){
Map payMap = new HashMap();
payMap.put("level 1","300");
payMap.put("level 2","291");
payMap.put("level 3","666");
return payMap;
}
}

​ 由于此端点数据spirng boot admin无法预知该如何展示,所以通过界面无法看到此数据,通过HTTP请求路径可以获取到当前端点的信息,但是需要先开启当前端点对外功能,或者设置当前端点为默认开发的端点。

image-20220301175355482

总结

  1. 端点的指标可以自定义,但是每种不同的指标根据其功能不同,自定义方式不同
  2. info端点通过配置和编程的方式都可以添加端点指标
  3. health端点通过编程的方式添加端点指标,需要注意要为对应指标添加启动状态的逻辑设定
  4. metrics指标通过在业务中添加监控操作设置指标
  5. 可以自定义端点添加更多的指标

开发实用篇完结

​ 开发实用篇到这里就暂时完结了,在开发实用篇中我们讲解了大量的第三方技术的整合方案,选择的方案都是市面上比较流行的常用方案,还有一些国内流行度较低的方案目前还没讲,留到番外篇中慢慢讲吧。

​ 整体开发实用篇中讲解的内容可以分为两大类知识:实用性知识与经验性知识。

​ 实用性知识就是新知识了,springboot整合各种技术,每种技术整合中都有一些特殊操作,整体来说其实就是三句话。加坐标做配置调接口。经验性知识是对前面两篇中出现的一些知识的补充,在学习基础篇时如果将精力放在这些东西上就有点学偏了,容易钻牛角尖,放到实用开发篇中结合实际开发说一些