SpringBoot-基础篇

  • 学习目标
章节 学习目标
基础篇 能够创建SpringBoot工程 基于SpringBoot实现ssm/ssmp整合
  • 前置知识
章节 前置知识 要求
基础篇 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请求,如何获取响应数据,如何进行数据模型双向绑定

一、快速入门

1.手动创建项目

①创建Maven工程

img

②配置pom文件

继承spring-boot-starter-parent;添加依赖spring-boot-starter-web

1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>

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

③创建引导类

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

@SpringBootApplication 注解相当于使用 @Configuration@EnableAutoConfiguration@ComponentScan 及他们的默认属性

  • @Configuration作为配置类替代xml配置文件
  • @EnableAutoConfiguration启用 SpringBoot 的自动配置机制
  • @ComponentScan 自动扫描所有 Spring 组件

④编写业务

1
2
3
4
5
6
7
@RestController
public class HelloController {
@GetMapping
public String home(){
return "Hello, Spring Boot 2!";
}
}

⑤运行项目

直接运行主程序 MainApplication 中的 main 方法,访问 http://127.0.0.1:8080,将会输出Hello, Spring Boot 2!

⑥简化部署

Spring Boot 包括了一个 Maven 插件,它可以将项目打包成一个可执行 jar

1
2
3
4
5
6
7
8
9
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.0</version>
</plugin>
</plugins>
</build>

点击 IDEA 右侧的 Maven ⇒ 点击生命周期 ⇒ 点击 clean ⇒ 点击 package,target 目录下就会生成 jar 包,执行如下命令即可运行 jar 包

1
java -jar springBoot-learn-1.0-SNAPSHOT.jar

2.脚手架创建项目

Spring Initailizr是创建 Spring Boot 工程的脚手架,以后就创建 SpringBoot 项目就不用创建空白Maven项目,而是创建 Spring Initailizr 项目

Spirng程序相比,SpringBoot程序在开发的过程中各个层面均具有优势

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

①创建Spring Initializr工程

创建新模块,选择Spring Initializr,并配置模块相关基础信息

创建工程时,可以切换 starter 服务路径为阿里云提供给的使用地址。

地址:http://start.aliyun.com或https://start.aliyun.com

img

②选择技术集

img

③编写业务

1
2
3
4
5
6
7
@RestController
public class HelloController {
@GetMapping
public String home(){
return "Hello, Spring Boot 2!";
}
}

④运行项目

直接运行主程序 MainApplication 中的 main 方法,访问 http://127.0.0.1:8080,将会输出Hello, Spring Boot 2!

二、工作流程

SpringBoot带来的好处:

  • 起步依赖(简化依赖配置)
    • 依赖配置的书写简化就是靠这个起步依赖达成的
  • 自动配置(简化常用工程相关配置)
    • 配置过于繁琐,使用自动配置就可以做响应的简化,但是内部还是很复杂的
  • 辅助功能(内置服务器,……)
    • 比如我们没有配置Tomcat服务器,但是能正常运行。

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

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

1.parent

SpringBoot 关注到开发者在进行开发时,往往对依赖版本的选择具有固定的搭配格式,并且这些依赖版本的选择还不能乱搭配。比如 A 技术的 2.0 版与 B 技术的 3.5 版可以合作在一起,但是和 B 技术的 3.7 版合并使用时就有冲突。于是 SpringBoot 做了无数个最合理的技术版本搭配列表,这个技术搭配列表的名字叫做parent

一句话总结:使用parent可以帮助开发者进行版本的统一管理

那SpringBoot又是如何做到这一点的呢?可以查阅SpringBoot的配置源码,看到这些定义

  • 项目中的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
      <properties>
      <activemq.version>5.16.3</activemq.version>
      <aspectj.version>1.9.7</aspectj.version>
      <assertj.version>3.19.0</assertj.version>
      <javax-json.version>1.1.4</javax-json.version>
      <javax-websocket.version>1.1</javax-websocket.version>
      <jetty-el.version>9.0.48</jetty-el.version>
      <junit.version>4.13.2</junit.version>
      ......
      </properties>
    • 第二组是各式各样的的依赖坐标信息。可以看出依赖坐标定义中没有具体的依赖版本号,而是引用了第一组信息中定义的依赖版本属性值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <dependencyManagement>
      <dependencies>
      <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
      </dependency>
      <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      </dependency>
      </dependencies>
      </dependencyManagement>

关注:上面的依赖坐标定义是出现在标签中的,其实是对引用坐标的依赖管理,并不是实际使用的坐标。因此当你的项目中继承了这组parent信息后,在不使用对应坐标的情况下,这组定义是不会具体导入某个依赖的

关注:因为在maven中继承机会只有一次,上述继承的格式还可以切换成导入的形式进行,并且在阿里云的starter创建工程时就使用了此种形式

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

总结

  1. 开发SpringBoot程序要继承spring-boot-starter-parent
  2. spring-boot-starter-parent中定义了若干个依赖管理
  3. 继承parent模块可以避免多个依赖使用相同技术时出现依赖版本冲突
  4. 继承parent的形式也可以采用引入依赖的形式实现效果

2.starter

SpringBoot 关注到开发者在实际开发时,对于依赖坐标的使用往往都有一些固定的组合方式,比如使用 spring-webmvc 就一定要使用 spring-web。每次都要固定搭配着写,非常繁琐。于是 SpringBoot 把所有的技术使用固定搭配格式都给开发出来,以后你用某个技术,就不用一次写一堆依赖了,直接用固定搭配格式就好了。这个固定技术搭配的名字叫做starter

一句话总结:使用starter可以帮助开发者减少依赖配置

那SpringBoot又是如何做到这一点的呢?可以查阅SpringBoot的配置源码,看到这些定义

  • 项目中的pom.xml定义了使用SpringMVC技术,但是并没有写SpringMVC的坐标,而是添加了一个名字中包含starter的依赖

command + shift + option + U:以图的方式显示项目中依赖之间的关系。

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>

发现里面有个 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提供给我们的配置管理

    • 方式一:直接写坐标
    • 方式二:覆盖中定义的版本号,在当前项目里面重写配置,如下面的代码
    1
    2
    3
    <properties>
    <mysql.version>5.1.43</mysql.version>
    </properties>

温馨提示

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

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

那非官方定义的也有吗?有的,而且名称都是如下格式

1
2
命名规则:技术名称-spring-boot-starter 
或者: 技术名称-boot-starter(技术名称过长,简化命名)

总结

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

3.引导类

程序是如何运行的。目前程序运行的入口就是SpringBoot工程创建时自带的那个类了,带有main方法的那个类,运行这个类就可以启动SpringBoot工程的运行

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}

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

总结

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

4.内嵌tomcat

当前我们做的SpringBoot入门案例勾选了Spirng-web的功能,并且导入了对应的starter。

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

SpringBoot发现,既然你要做web程序,肯定离不开使用web服务器,我再帮你搞一个web服务器,你要愿意用的,直接使用就好了,干脆我再多给你几种选择,你随便切换。万一你不想用我给你提供的,也行,你可以自己搞。

由于这个功能不属于程序的主体功能,可用可不用,于是乎SpringBoot将其定位成辅助功能。

下面就围绕着这个内置的web服务器,也可以说是内置的tomcat服务器来研究几个问题

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

内嵌Tomcat定义位置

说到定义的位置,我们就想,如果我们不开发web程序,用的着web服务器吗?肯定用不着啊。那如果这个东西被加入到你的程序中,伴随着什么技术进来的呢?肯定是web相关的功能啊,没错,就是前面导入的web相关的starter做的这件事。

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引入到程序中的?spring-boot-starter-web中的spring-boot-starter-tomcat做的。

内嵌Tomcat运行原理

Tomcat服务器是一款软件,而且是一款使用java语言开发的软件下面的问题来了,既然是使用java语言开发的,运行的时候肯定符合java程序运行的原理,java程序运行靠的是什么?对象呀,一切皆对象,万物皆对象。那tomcat运行起来呢?也是对象。

如果是对象,那Spring容器是用来管理对象的,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服务器的功能呢?是这样的,通过依赖排除可以去掉这个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>

重新启动程序可以观察到程序运行了,但是并没有像之前那样运行后会等着用户发请求,而是直接停掉了。

更换内嵌Tomcat

根据SpringBoot的工作机制,用什么技术,加入什么依赖就行了。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. 变更内嵌服务器思想是去除现有服务器,添加全新的服务器

三、基础配置

1.属性配置

SpringBoot 通过 resources 目录下的配置文件 application.properties 就可以修改默认的配置

例如 将 tomcat 的默认端口8080改成80,输入port后,自带提示带提示

1
server.port=80

重新运行,浏览器输入http://localhost即可

这个配置项和什么有关?在pom中注释掉导入的spring-boot-starter-web,然后刷新工程,你会发现配置的提示消失了。闹了半天是设定使用了什么技术才能做什么配置。

更多配置项请参考:Common Application Properties

总结

  1. SpringBoot中导入对应starter后,提供对应配置属性
  2. 书写SpringBoot配置采用关键字+提示形式书写

2.配置文件分类

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

  • properties格式
1
server.port=80
  • yml格式(推荐使用
1
2
server:
port: 81
  • yaml格式
1
2
server:
port: 82

yml 格式和 yaml 格式除了文件名后缀不一样,格式完全一样,所以可以合并成一种格式来看。

  • 三种格式中推荐使用 yml 格式
  • 配置文件间的加载优先级 properties(最高)> yml > yaml(最低)
  • 不同配置文件中相同配置按照加载优先级相互覆盖,不同配置文件中不同配置全部保留

3.yaml文件格式

YAML(YAML Ain’t Markup Language) 是一种较为人性化的数据序列化语言 ,语法比较简洁直观,特点是使用空格来表达层次结构,其最大优势在于数据结构方面的表达,所以 YAML 更多应用于编写配置文件,其文件一般以 .yml 为后缀。

yaml 非常适合用来做以数据为中心的配置文件。所以我们可以用 yaml 代替之前的 properties

①基本语法

  • key: value
    • :”后有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用 tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • 字符串无需加引号,如果要加:
    • 双引号表示字符串内容会被转义
    • 单引号表示字符串内容不会被转义

数据类型

  • 普通数据:单个的、不可再分的值。date、boolean、string、number、null
1
k: v
  • 对象:键值对的集合。map、hash、set、object
1
2
3
4
5
6
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
  • 数组:一组按次序排列的值。array、list、queue
1
2
3
4
5
6
7
8
9
10
11
12
13
行内写法:  k: [v1,v2,v3]
#或
k:
- v1
- v2
- v3

users: #对象数组格式
- name: Tom
age: 4
- name: Jerry
age: 5
users2: [ { name:Tom , age:4 } , { name:Jerry , age:5 } ] #对象数组缩略格式

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class Person {

private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
private String name;
private Double weight;
}
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
# yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

②数据引用

如果你在书写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. 如果属性中出现特殊字符,可以使用双引号包裹起来作为字符解析

4.yaml数据读取

①读取单一数据

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

img

总结

  1. 使用@Value读取单个数据
  2. 如果数据存在多层级,依次书写层级名称即可

②读取全部数据

读取单一数据可以解决读取数据的问题,但是如果定义的数据量过大,这么一个一个书写肯定会累死人的,SpringBoot提供了一个对象,能够把所有的数据都封装到这一个对象中,这个对象叫做Environment,使用自动装配注解 @Autowired 可以将所有的yaml数据封装到这个对象中

img

数据封装到了Environment对象中,通过方法 getProperty(“属性名”) 获取数据

总结

  1. 使用Environment对象封装全部配置信息
  2. 使用@Autowired自动装配数据到Environment对象中

③读取对象数据

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

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

img

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

img

总结

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

温馨提示

细心的小伙伴会发现一个问题,自定义的这种数据在yaml文件中书写时没有弹出提示,解决方法如下。

④配置提示效果

配置提示 configuration-processor

自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<build>
<plugins>
<plugin>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

再在自定义的类上添加配置注解

1
2
@ConfigurationProperties(prefix = "xxx")//读取文件中所有以xxx开头的属性,并和bean中的字段进行匹配
@Component //标注类为Spring容器的Bean

测试

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = "user")
@Component //标注类为Spring容器的Bean
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
@Data
@EqualsAndHashCode
public class User {
private String name;
private Integer age;
}

在配置文件 application.yml 中输入 user 就会得到 name 和 age 的输入提示

四、整合技术

整合第三方技术通用方式

  • 导入对应的starter
  • 配置非默认值对应的配置项

1.整合JUnit

  • 导入测试对应的starter,初始化项目时此项是默认导入的
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  • 在测试类上添加注解 @SpringBootTest ,在方法上添加注解 @Test
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class SpringbootJunitTests {
//注入你要测试的对象
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
//执行要测试的对象对应的方法
bookDao.save();
System.out.println("two...");
}
}
  • 测试类如果存在于引导类所在包或子包中无需指定引导类,测试类如果不存在于引导类所在的包或子包中需要通过classes属性指定引导类

第一种方式使用属性的形式进行,在注解 @SpringBootTest 中添加 classes 属性指定配置类

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

第二种方式回归原始配置方式,仍然使用 @ContextConfiguration 注解进行,效果是一样的

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

总结

  1. 导入测试对应的starter
  2. 测试类使用@SpringBootTest修饰
  3. 使用自动装配的形式添加要测试的对象
  4. 测试类如果存在于引导类所在包或子包中无需指定引导类
  5. 测试类如果不存在于引导类所在的包或子包中需要通过classes属性指定引导类

2.整合MyBatis

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

img

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

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>

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

步骤②:配置数据源相关信息,没有这个信息你连接哪个数据库都不知道

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

测试

  • 测试表
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
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tbl_book
-- ----------------------------
DROP TABLE IF EXISTS `tbl_book`;
CREATE TABLE `tbl_book` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`description` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tbl_book
-- ----------------------------
INSERT INTO `tbl_book` VALUES (1, '三体', '科幻', '大刘的巅峰之作,将中国科幻推向世界舞台。总共分为三部曲:《地球往事》、《黑暗森林》、《死神永生》。');
INSERT INTO `tbl_book` VALUES (2, '格林童话', '童话', '睡前故事。');
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核心技术卷|基础知识(原书第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+');
INSERT INTO `tbl_book` VALUES (13, 'Spring实战第5版', '计算机理论', 'Spring入门经典教程,深入理解Spring原理技术内幕');
INSERT INTO `tbl_book` VALUES (14, 'Spring 5核心原理与30个类手写实战', '计算机理论', '十年沉淀之作,写Spring精华思想');

SET FOREIGN_KEY_CHECKS = 1;
  • 实体类
1
2
3
4
5
6
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
  • 映射接口(Dao)
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));
}
}

总结

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

3.整合MyBatis-Plus

步骤①:导入对应的 starter 和数据库驱动

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>

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

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

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

步骤③:配置表名的通用前缀名

数据库的表名定义规则是tbl_模块名称,为了能和实体类相对应,需要设置所有表名的通用前缀名

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

步骤④:配置运行日志

使用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 #配置日志

测试

  • 实体类
1
2
3
4
5
6
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
  • 映射接口(Dao)
1
2
3
@Mapper
public interface BookDao extends BaseMapper<Book> {
}

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

  • 测试类
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
class SpringbootApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
System.out.println(bookDao.selectById(2));
System.out.println(bookDao.selectList(null));
}
}

总结

  1. 手工添加MyBatis-Plus对应的starter
  2. 数据层接口使用BaseMapper简化开发
  3. 借助MyBatis-Plus日志可以查阅执行SQL语句
  4. 需要使用的第三方技术无法通过勾选确定时,需要手工添加坐标

4.整合Durid

前面整合 MyBatis 和 MyBatis-Plus 的时候,使用的数据源对象都是SpringBoot默认的数据源对象 HiKari,下面我们手工控制一下,自己指定了一个数据源对象 Druid。

步骤①:导入对应的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
8
9
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root

#除了这4个常规配置外,还有druid专用的其他配置。通过提示功能可以打开druid相关的配置查阅

总结

  1. 整合Druid需要导入Druid对应的starter
  2. 根据Druid提供的配置方式进行配置

5.整合Redis

步骤①:导入对应的starter 和依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

步骤②:修改配置

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: 127.0.0.1 #指定redis所在的host
port: 6379 #指定redis的端口
password: 123456 #设置redis密码
lettuce:
pool:
max-active: 8 #最大连接数
max-idle: 8 #最大空闲数
min-idle: 0 #最小空闲数
max-wait: 100ms #连接等待时间

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class SpringRedisApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Test
void testString() {
// 1.通过RedisTemplate获取操作String类型的ValueOperations对象
ValueOperations ops = redisTemplate.opsForValue();
// 2.插入一条数据
ops.set("name","jianjian");
// 3.获取数据
String name = (String) ops.get("name");
System.out.println("name = " + name);
}
}

五、整合案例

本章将实现一个基于 SpringBoot -Vue 的 CRUD 整合 Demo

项目地址:https://github.com/jwt1399/SpringBoot-Vue-CRUD

0.项目简介

①.项目层次

  • entity层

entity层即数据库实体层,也称为 POJO 层(Plain Ordinary Java Object,即简单普通的java对象)或 model 层。 entity层的作用为存放实体类Bean。 一般数据库一张表对应一个实体类,类属性同表字段一一对应。

  • dao层

dao层即数据持久层,也称为mapper层。 dao层的作用为访问数据库,向数据库发送sql语句,完成数据的增删改查任务。

  • service层

service层即业务逻辑层。 service层的作用为完成功能设计。 service层调用dao层接口,接收dao层返回的数据,完成项目的基本功能设计。

  • controller层

controller层即控制层。 controller层的功能为请求和响应控制。 controller层负责前后端交互,接受前端请求,调用service层,接收service层返回的数据,最后返回具体的页面和数据到客户端。

img

②.相关技术

  • Bean开发————使用Lombok快速制作实体类
  • Dao开发————整合MyBatis-Plus,制作数据层测试
  • Service开发————基于MyBatis-Plus进行增量开发,制作业务层测试类
  • Controller开发————基于Restful开发,使用PostMan测试接口功能
  • 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理

③.项目展示

主页面

img

增/改

img

删除

img

查询

img

1.项目创建

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

pom.xml

1
2
3
4
5
6
7
8
9
<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>

application.yml

1
2
server:
port: 80

2.实体层开发

数据表

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+');

实体类

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

实体类的开发可以手工生成get/set方法,然后覆盖toString()方法。不过这一套操作书写很繁琐,可以使用 Lombok简化JavaBean开发。

引入 Lombok,用注解代替构造器、getter/setter、toString()等代码,新版 IDEA 已集成 Lombok 插件,无需下载

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  • @Data : 注在类上,提供类的get、set、equals、hashCode、toString等方法
  • @AllArgsConstructor :注在类上,提供类的全参构造
  • @NoArgsConstructor :注在类上,提供类的无参构造
  • @Setter :注在属性上,提供 set 方法
  • @Getter :注在属性上,提供 get 方法
  • @EqualsAndHashCode :注在类上,提供对应的 equals 和 hashCode 方法
  • @Log4j/@Slf4j :注在类上,提供对应的 Logger 对象,变量名为 log
1
2
3
4
5
6
7
8
9
10
//简化JavaBean开发
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}

Lombok 还可以简化日志开发,例如下面代码

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}

3.数据层开发

①基础CRUD

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

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

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

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=Asia/Shanghai
username: root
password: root

步骤③:配置MP相关配置

  • 表名通用前缀
  • 主键生成策略
    • MP技术默认的主键生成策略为雪花算法,生成的主键ID长度较大,和目前的数据库设定规则不相符,需要配置一下使MP使用数据库的主键生成策略
  • 配置MP日志
    • 设置日志输出方式为标准输出即可查阅SQL执行日志
1
2
3
4
5
6
7
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_ #设置表名通用前缀
id-type: auto #设置主键id字段的生成策略为参照数据库设定的策略,当前数据库设置id生成策略为自增
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置标准输出日志

步骤④:使用MP的标准通用接口BaseMapper加速开发,别忘了@Mapper和泛型的指定

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

步骤⑤:制作测试类测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.jianjian.dao;

@SpringBootTest
public class BookDaoTests {

@Autowired
private BookDao bookDao;

@Test
void testGetById(){
System.out.println(bookDao.selectById(1));
}

@Test
void testSave(){
Book book = new Book();
book.setType("测试数据123");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookDao.insert(book);
}

@Test
void testUpdate(){
Book book = new Book();
book.setId(17);
book.setType("测试数据abcdefg");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookDao.updateById(book);
}

@Test
void testDelete(){
bookDao.deleteById(16);
}

@Test
void testGetAll(){
bookDao.selectList(null);
}
}

总结

  1. 手工导入starter坐标(2个),mysql驱动(1个)
  2. 配置数据源与MyBatisPlus对应的配置
  3. 配置MP相关配置
  4. 开发Dao接口(继承BaseMapper)
  5. 制作测试类测试Dao功能是否有效

②分页功能

MyBatis-Plus自带分页插件,只要需要定义MP拦截器并将其设置为Spring管控的bean即可实现分页功能

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MPConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//创建MP的拦截器栈
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//初始化了分页拦截器,并添加到拦截器栈中
//如果后期开发其他功能,需要添加全新的拦截器,按照此行的格式继续add进去新的拦截器就可以了。
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}

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方法需要传入一个封装分页数据的IPage对象,可以通过new的形式创建这个对象。创建此对象时就需要指定分页的两个基本数据

  • 当前显示第几页
  • 每页显示几条数据

可以通过创建Page对象时利用构造方法初始化这两个数据

1
IPage page = new Page(2,5);

将该对象传入到查询方法selectPage后,可以得到查询结果,但是我们会发现当前操作查询结果返回值仍然是一个IPage对象,这又是怎么回事?

1
IPage page = bookDao.selectPage(page, null);

原来这个IPage对象中封装了若干个数据,而查询的结果作为IPage对象封装的一个数据存在的,可以理解为查询结果得到后,又塞到了这个IPage对象中,其实还是为了高度的封装。

总结

  1. 分页操作依赖MyBatisPlus分页拦截器实现功能
  2. 使用IPage封装分页数据

③条件查询

以往我们写条件查询要自己动态拼写复杂的SQL语句,现在简单了,MP将这些操作都制作成API接口,调用一个又一个的方法就可以实现各种套件的拼装。

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

1
2
3
4
5
6
7
8
@Test
void testGetBy(){
//封装查询条件的对象
QueryWrapper<Book> qw = new QueryWrapper<>();
//查询条件
qw.like("name","Spring");
bookDao.selectList(qw);
}

由于属性字段名的书写存在着安全隐患,比如查询字段name,当前是以字符串的形态书写的,万一写错,编译器还没有办法发现,只能将问题抛到运行器通过异常堆栈告诉开发者,不太友好。

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,"Spring");
bookDao.selectList(lqw);
}

为了便于开发者动态拼写SQL,防止将null数据作为条件使用,MP还提供了动态拼装SQL的快捷书写方式

1
2
3
4
5
6
7
8
@Test
void testGetBy3(){
String name = "Spring";
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. 使用QueryWrapper对象封装查询条件
  2. 推荐使用LambdaQueryWrapper对象
  3. 所有查询操作封装成方法调用
  4. 查询条件支持动态条件拼装

4.业务层开发

①简介

初学者认为业务层就是调用数据层,更精准的说法应该是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用

一个常识性的知识普及一下,业务层的方法名定义一定要与业务有关,例如登录操作

1
login(String username,String password);

而数据层的方法名定义一定与业务无关,是一定,不是可能,也不是有可能,例如根据用户名密码查询

1
selectByUserNameAndPassword(String username,String password);

我们在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。

比如开发数据层的团队,拿到的需求文档要求可能是这样的

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

但是开发业务层功能团队,拿到的需求文档要求差别就很大

1
接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,824位,对喵喵喵字段做特殊字符校验,不允许存在空格,查询结果为对象。如果为null,返回BusinessException,封装消息码INFO_LOGON_USERNAME_PASSWORD_ERROR

所以说业务层方法定义与数据层方法定义差异化很大

②普通开发

步骤①:业务层接口定义如下:

1
2
3
4
5
6
7
8
public interface BookService {
Boolean save(Book book);
Boolean update(Book book);
Boolean delete(Integer id);
Book getById(Integer id);
List<Book> getAll();
IPage<Book> getPage(int currentPage,int pageSize);
}

步骤②:业务层实现类如下,转调数据层即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class BookServiceImpl implements BookService {

@Autowired
private BookDao bookDao;

@Override
public Boolean save(Book book) {
return bookDao.insert(book) > 0;
}

@Override
public Boolean update(Book book) {
return bookDao.updateById(book) > 0;
}

@Override
public Boolean delete(Integer id) {
return bookDao.deleteById(id) > 0;
}

@Override
public Book getById(Integer id) {
return bookDao.selectById(id);
}

@Override
public List<Book> getAll() {
return bookDao.selectList(null);
}

@Override
public IPage<Book> getPage(int currentPage, int pageSize) {
IPage page = new Page(currentPage,pageSize);
return bookDao.selectPage(page,null);
}
}

步骤③:对业务层接口进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@SpringBootTest
public class BookServiceTests {
@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();
}
@Test
void testGetPage(){
IPage<Book> page = bookService.getPage(2, 5);
System.out.println(page.getCurrent());
System.out.println(page.getSize());
System.out.println(page.getTotal());
System.out.println(page.getPages());
System.out.println(page.getRecords());
}
}

总结

  1. Service接口名称定义成业务名称,并与Dao接口名称进行区分
  2. 制作测试类测试Service功能是否有效

③快速开发

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

步骤①:业务层接口快速开发

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
}

如果感觉MP提供的功能不足以支撑你的使用需要,可以在原始接口基础上接着定义新的API接口就行了

总结

  1. 使用通用接口(ISerivce)快速开发Service
  2. 使用通用实现类(ServiceImpl<M,T>)快速开发ServiceImpl
  3. 可以在通用接口基础上做功能重载或功能追加
  4. 注意重载时不要覆盖原始操作,避免原始提供的功能丢失

5.控制层开发

①普通Restful开发

步骤①:控制层接口如下,基于Restful开发

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 BookService 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, null);
}
}

步骤②:Postman测试,关注提交类型,对应上即可,不然就会报405的错误码了

  • 普通GET请求

img

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

img

  • PUT请求传递json数据,后台使用@RequestBody接收数据

img

总结

  1. 基于Restful制作表现层接口
    • 新增:@PostMapping
    • 删除:@DeleteMapping
    • 修改:@PutMapping
    • 查询:@GetMapping
  2. 接收参数
    • 实体数据:@RequestBody
    • 路径变量:@PathVariable

②消息一致性开发

为何要返回统一格式。为方便使用已封装好,导入项目即可使用:https://jwt1399.lanzouv.com/iINu80bfh1sd

现在大多数web项目基本都是前后端分离模式,这种模式会涉及到一个前后端对接问题,所以一套完善且规范的接口是非常有必要的,不仅能够提高对接效率,也可以让我的代码看起来更加简洁优雅。本节将解决如何返回统一的标准格式以及处理全局异常。

如果SpringBoot不使用统一返回格式,默认会有如下三种返回情况。

  • 返回字符串
1
2
3
4
5
6
@GetMapping("/getUserName")
public String getUserName(){
return "Hello";
}

// 返回结果:Hello
  • 返回实体类对象
1
2
3
4
5
6
7
8
9
10
@GetMapping("/getUserName")
public User getUserName(){
return new User("Hello",18);
}

// 返回结果:
{
"name": "Hello",
"age": "18"
}
  • 返回异常
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/getUserName")
public static String getUserName(){
HashMap hashMap = new HashMap();
return hashMap.get(0).toString(); // 模拟一个空指针异常
}

// 返回结果:
{
"timestamp": "2022-09-09T12:56:06.549+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/getUserName"
}

对于上面这几种情况,如果整个项目没有定义统一的返回格式,不同开发人员可能会定义不同的返回格式,这样会使前后端对接出现一些问题。

a. 定义返回标准

一个标准的返回格式至少包含3部分:

1
2
3
code: 状态码
message: 接口调用的提示信息
data: 返回数据

步骤①:定义数据返回格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Data
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;

/**
* 成功
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<T>();
result.setCode(ResultMsgEnum.SUCCESS.getCode());
result.setMessage(ResultMsgEnum.SUCCESS.getMessage());
result.setData(data);
return result;
}

/**
* 失败
*/
public static <T> Result<T> fail(ResultMsgEnum resultMsgEnum) {
Result<T> result = new Result<T>();
result.setCode(resultMsgEnum.getCode());
result.setMessage(resultMsgEnum.getMessage());
return result;
}
}

步骤②:定义状态码

1
2
3
4
5
6
7
8
9
10
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ResultMsgEnum {
SUCCESS(0, "成功"),
FAIL(-1, "失败"),
TEST_ERROR(400, "发生错误啦!");
private int code;
private String message;
}

步骤③:使用

上面定义了数据返回格式和状态码,下面在接口中使用。

1
2
3
4
5
6
7
8
9
10
11
12
//示例1
@GetMapping("/getUserName")
public Result getUserName(){
return Result.success("Hello");
}

//示例2
@GetMapping("/getUserName")
public static Result getUserName(){
HashMap hashMap = new HashMap();
return Result.success(hashMap.get(0).toString()); // 模拟一个空指针异常
}

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 示例1结果
{
"code": 0,
"message": "成功",
"data": "Hello"
}

# 示例2结果
{
"timestamp": "2022-09-09T14:45:21.875+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/getUserName"
}

通过在Controller层调用Result.success()对返回结果进行包装后返回给前端,虽然能够满足日常需求,但是当有大量的接口时,每一个接口中都使用Result.success()来包装返回信息就会增加很多重复代码,而且遇到异常数据格式无法统一。

c .统一接口返回

前面步骤不够优雅,可以继续改进,用 @RestControllerAdvice 注解,拦截下后端返回的数据,实现 ResponseBodyAdvice 接口对数据做一层包装再返回给前端。

ResponseBodyAdvice: 该接口是SpringMVC 4.1提供的,它允许在 执行@ResponseBody后自定义返回数据,用来封装统一数据格式返回;拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等

@RestControllerAdvice: 该注解是Controller的增强版,可以全局捕获抛出的异常,全局数据绑定,全局数据预处理。

步骤①:新建ResponseAdvice类,该类用于统一封装controller中接口的返回结果。实现ResponseBodyAdvice接口,实现supports、beforeBodyWrite方法。

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
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;

/**
* 是否开启功能 true:开启
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}

/**
* 处理返回结果
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
//处理字符串类型数据
if(o instanceof String){
try {
return objectMapper.writeValueAsString(Result.success(o));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//返回类型是否已经封装
if(o instanceof Result){
return o;
}
return Result.success(o);
}
}

步骤②:测试,无需转换格式,直接使用

1
2
3
4
5
@GetMapping("/getUserName")
public static String getUserName(){
HashMap hashMap = new HashMap();
return hashMap.get(0).toString(); // 模拟一个空指针异常
}

返回结果如下,可以看到返回结果与在Result中定义的参数类型相同。

1
2
3
4
5
6
7
8
9
10
{
"code": 0,
"message": "成功",
"data": {
"timestamp": "2022-09-09T14:37:19.902+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/getUserName"
}
}

d.全局异常处理

遇到异常时,第一时间想到的应该是try…catch,不过这种方式会导致大量代码重复,维护困难等问题,这里不用手写try…catch,由全局异常处理器统一捕获;对于自定义异常,只能通过全局异常处理器来处理,使用全局异常处理器最大的便利就是程序员在写代码时不再需要手写 try…catch了。

步骤①:首先新增一个类,增加@RestControllerAdvice注解,如果我们有想要拦截的异常类型,就新增一个方法,使用@ExceptionHandler注解修饰,注解参数为目标异常类型。

例如:controller中接口发生Exception异常时,就会进入到Execption方法中进行捕获,将杂乱的异常信息,转换成指定格式后交给ResponseAdvice方法进行统一格式封装并返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {

@ExceptionHandler(Exception.class)
public Result Execption(Exception e) {
log.error("未知异常!", e);
//e.printStackTrace();
return Result.fail(ResultMsgEnum.TEST_ERROR);
}
//还可定义其它拦截异常方法
//。。。。。。
}

步骤②:测试,再次调用接口getUserName查看返回结果

1
2
3
4
5
@GetMapping("/getUserName")
public static String getUserName(){
HashMap hashMap = new HashMap();
return hashMap.get(0).toString(); // 模拟一个空指针异常
}

返回结果

1
2
3
4
5
{
"code": 400,
"message": "发生错误啦!",
"data": null
}

e.快速使用

前面实现了消息一致性,为了方便使用作如下封装,只需引用到项目中即可

步骤①:定义数据返回格式

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
@Data
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;

/**
* 成功
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<T>();
result.setCode(ResultMsgEnum.SUCCESS.getCode());
result.setMessage(ResultMsgEnum.SUCCESS.getMessage());
result.setData(data);
return result;
}

/**
* 失败
*/
public static <T> Result<T> fail(ResultMsgEnum resultMsgEnum) {
Result<T> result = new Result<T>();
result.setCode(resultMsgEnum.getCode());
result.setMessage(resultMsgEnum.getMessage());
return result;
}
}

步骤②:定义状态码

1
2
3
4
5
6
7
8
9
10
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ResultMsgEnum {
SUCCESS(0, "成功"),
FAIL(-1, "失败"),
TEST_ERROR(400, "发生错误啦!");
private int code;
private String message;
}

**步骤③:**统一接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;

/**
* 是否开启功能 true:开启
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}

/**
* 处理返回结果
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
//处理字符串类型数据
if(o instanceof String){
try {
return objectMapper.writeValueAsString(Result.success(o));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//返回类型是否已经封装
if(o instanceof Result){
return o;
}
return Result.success(o);
}
}

步骤④:全局异常

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {

@ExceptionHandler(Exception.class)
public Result Execption(Exception e) {
log.error("未知异常!", e);
//e.printStackTrace();
return Result.fail(ResultMsgEnum.TEST_ERROR);
}
//还可定义其它异常方法
//。。。。。。
}

总结

  1. 设计统一的返回值结果类型便于前端开发读取数据
  2. 返回值结果类型可以根据需求自行设定,没有固定格式
  3. 返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议

参考

6.页面功能开发

⓪前端页面

前端页面下载:https://jwt1399.lanzouv.com/ibQNG0bet3yh

将页面保存到 resources/static目录中,建议执行maven的clean生命周期,避免缓存的问题出现。

如果成功访问:http://127.0.0.1/pages/books.html,即表明接入成功

在进行具体的功能开发之前,先做前后端联通性的测试,通过页面发送异步提交(axios)

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

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

总结

  1. 单体项目中页面放置在resources/static目录下
  2. created钩子函数用于初始化页面时发起调用
  3. 页面使用axios发送异步请求获取数据后确认前后端是否联通

①列表功能

列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可

a.页面数据模型定义

1
2
3
4
data:{
dataList: [],//当前页要展示的列表数据
...
},

b.异步请求获取数据

1
2
3
4
5
6
//列表
getAll() {
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
},

c.钩子函数调用请求

1
2
3
4
//钩子函数,VUE对象初始化完成后自动执行
created() {
this.getAll();
},

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

②添加功能

添加功能首先要进行数据收集窗口弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示时切换状态即可

a.表单窗口默认状态

1
2
3
4
data:{
dialogFormVisible: false,//添加表单是否可见
...
},

b.切换为显示状态

1
2
3
4
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
},

c.定义清理数据操作

由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹

1
2
3
4
//重置表单
resetForm() {
this.formData = {};
},

d.切换弹窗状态时清理数据

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

e.添加操作

  1. 将要保存的数据传递到后台,通过post请求的第二个参数传递json数据到后台
  2. 根据返回的操作结果决定下一步操作
    • 如果是状态码是成功就关闭添加窗口,显示添加成功的消息
    • 反之保留添加窗口,显示添加失败的消息
  3. 无论添加是否成功,页面均进行刷新,动态加载数据(对getAll操作发起调用)
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.code == 0){
this.dialogFormVisible = false;
this.$message.success("添加成功");
}else {
this.$message.error("添加失败");
}
}).finally(()=>{
this.getAll();//重载数据,避免添加后数据不显示
});
},

f.取消添加操作

1
2
3
4
5
//取消
cancel(){
this.dialogFormVisible = false;
this.$message.info("操作取消");
},

总结

  1. 请求方式使用POST调用后台对应操作
  2. 添加操作结束后动态刷新页面加载数据
  3. 根据操作结果不同,显示对应的提示信息
  4. 弹出添加表单时清除上次表单数据

③删除功能

模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据id到后台即可

a.删除操作

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();
});
},

b.删除操作提示信息

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

总结

  1. 请求方式使用Delete调用后台对应操作
  2. 删除操作需要传递当前行数据对应的id值到后台
  3. 删除操作结束后动态刷新页面加载数据
  4. 根据操作结果不同,显示对应的提示信息
  5. 删除操作前弹出提示框避免误操作

④修改功能

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

  1. 页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
  2. 弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询相同,都是要查数据
  3. 查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
  4. 查询得到数据后需要展示到弹窗中,这一点与查询相同,都是要通过数据模型绑定展示数据
  5. 修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据

a.查询并展示数据

1
2
3
4
5
6
7
8
9
10
11
12
//弹出编辑窗口
handleUpdate(row) {
axios.get("/books/"+row.id).then((res)=>{
if(res.data.code == 0){
//展示弹层,加载数据
this.formData = res.data.data;
this.dialogFormVisibleEdit = true;
}else {
this.$message.error("数据同步失败");
}
});
},

b.修改操作

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.code == 0){
this.dialogFormVisibleEdit = false;
this.$message.success("修改成功");
}else {
this.$message.error("修改失败");
}
}).finally(()=>{
this.getAll();
});
},

c.取消修改操作

1
2
3
4
5
6
//取消
cancel(){
this.dialogFormVisible = false;
this.dialogFormVisibleEdit = false;
this.$message.info("操作取消");
},

总结

  1. 加载要修改数据通过传递当前行数据对应的id值到后台查询数据
  2. 利用前端双向数据绑定将查询到的数据进行回显
  3. 请求方式使用PUT调用后台对应操作
  4. 修改操作结束后动态刷新页面加载数据
  5. 根据操作结果不同,显示对应的提示信息

⑤分页功能

分页功能的制作用于替换前面的查询全部,

a.ElementUI分页组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--分页组件-->
<div class="pagination-container">
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total,sizes, prev, pager, next, jumper"
:page-sizes="[5, 10, 15, 20]"
:total="pagination.total">
</el-pagination>
</div>

b.配合分页组件,封装分页对应的数据模型

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

c.修改查询全部功能为分页查询,通过路径变量传递页码信息参数

1
2
3
4
5
6
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize)
.then((res)=>{
console.log(res.data.data);
});
},

d.页面根据分页操作结果读取对应数据,并进行数据模型绑定

1
2
3
4
5
6
7
8
9
10
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize)
.then((res)=>{
//console.log(res.data.data);
this.dataList = res.data.data.records;
this.pagination.currentPage = res.data.data.current;
this.pagination.pageSize = res.data.data.size;
this.pagination.total = res.data.data.total;
})
},

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

1
2
3
4
5
//切换页码
handleCurrentChange(currentPage) {
this.pagination.currentPage = currentPage;
this.getAll();
},

g.对切换每页显示条数操作设置调用当前记录数操作

1
2
3
4
5
//切换每页条数
handleSizeChange(pageSize){
this.pagination.pageSize = pageSize;
this.getAll();
}

总结

  1. 使用el分页组件
  2. 定义分页组件绑定的数据模型
  3. 异步调用获取分页数据
  4. 分页数据页面回显

⑥删除功能维护

由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。其实这个问题解决方案很多,这里给出比较简单的一种处理方案

1
2
3
4
5
6
7
8
9
@GetMapping("{currentPage}/{pageSize}")
public IPage<Book> 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 page;
}

删除一条数据之后,主键id不连续的问题解决

1
2
3
4
#删除id列
alter table tbl_book drop id;
#新增id列,设为主键,并自增
ALTER TABLE tbl_book ADD id INT NOT NULL PRIMARY KEY AUTO_INCREMENT FIRST;

⑦条件查询

条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。

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

a.页面封装查询条件字段

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

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

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>

c.将查询条件组织成url参数,添加到请求url地址中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)=>{
//console.log(res.data.data);
this.pagination.currentPage = res.data.data.current;
this.pagination.pageSize = res.data.data.size;
this.pagination.total = res.data.data.total;
this.dataList = res.data.data.records;
})
},

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

1
2
3
4
5
6
7
8
9
10
@GetMapping("{currentPage}/{pageSize}")
public IPage<Book> getPage(@PathVariable int currentPage, @PathVariable int pageSize,Book book){
System.out.println("参数=====>"+book);
IPage<Book> page = bookService.getPage(currentPage, pageSize, book);
//如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize, book);
}
return page;
}

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

1
2
3
public interface BookService {
IPage<Book> getPage(int currentPage,int pageSize,Book book);
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class BookServiceImpl implements BookService {
@Override
public IPage<Book> getPage(int currentPage, int pageSize, Book book) {
IPage page = new Page(currentPage,pageSize);
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
lqw.like(Strings.isNotEmpty(book.getName()),Book::getName,book.getName());
lqw.like(Strings.isNotEmpty(book.getType()),Book::getType,book.getType());
lqw.like(Strings.isNotEmpty(book.getDescription()),Book::getDescription,book.getDescription());
return bookDao.selectPage(page,lqw);
}
}

总结

  1. 定义查询条件数据模型(当前封装到分页数据模型中)
  2. 异步调用分页功能并通过请求参数传递数据到后台