SpringBoot-实用篇


实用篇包含运维实用篇和开发实用篇,运维实用篇的定位是玩转配置;开发实用篇包含热部署、高级配置、测试进阶、内置数据层、监控等。

小简从 0 开始学 Java 知识之 Java-学习路线 中的《SpringBoot-实用篇》,不定期更新所学笔记,期待一年后的蜕变吧!<有同样想法的小伙伴,可以联系我一起交流学习哦!>

  • 🚩时间安排:预计5天更新完

  • 🎯开始时间:09-11

  • 🎉结束时间:09-14

  • 🍀总结:

  • 学习目标

章节 学习目标
应用篇 能够掌握SpringBoot程序多环境开发
能够基于Linux系统发布SpringBoot工程
能够解决线上灵活配置SpringBoot工程的需求

运维实用篇

1.打包与运行

①程序打包

SpringBoot程序是基于Maven创建的,在Maven中提供有打包的指令,叫做package。

mvn package

本操作也可以在IDEA环境下执行。打包后会产生一个与工程名类似的jar文件,其名称是由模块名+版本号+.jar组成的。如果你写的测试类中有操作数据库的功能,打包时可以在IDEA->Maven中点击跳过测试按钮,再打包,来避免测试类影响数据库。

特别关注:在使用向导创建SpringBoot工程时,pom.xml文件中会有如下配置,这一段配置千万不能删除,否则打包后无法正常执行程序。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

②程序运行

  • 1.安装JDK,且版本不低于打包时使用的JDK版本

  • 2.程序包保存在/usr/local/自定义目录中或$HOME下

  • 3.执行运行指令就可以了

前台运行

java -jar 工程包名.jar

后台运行

$ nohup java -jar 工程包名.jar > server.log 2>&1 &

#命令解释
# nohup 英文全称 no hang up(不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
# 最后一个&表示把条命令放到后台执行
# 2>&1表示把标准错误输出和标准输出都定向到log中
名称 代码 操作符 Java中表示
标准输入(stdin) 0 < 或 << System.in
标准输出(stdout) 1 >, >>, 1> 或 1>> System.out
标准错误输出(stderr) 2 2> 或 2>> System.err

参考:深入理解Linux shell中2>&1的含义

总结

  1. SpringBoot工程可以基于java环境下独立运行jar文件启动服务
  2. SpringBoot工程执行mvn命令package进行打包
  3. 执行jar命令:java –jar 工程名.jar

③jar运行机制

  • jar包结构
$ tree -L 2
├── BOOT-INF
│   ├── classes
│   ├── classpath.idx
│   ├── layers.idx
│   └── lib
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
└── org
    └── springframework
  • MANIFEST.MF 文件
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.jianjian.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
  • 运作机制
  1. spring-boot-maven-plug会打出一个特殊的包,包含Spring框架部分功能,原始工程内容,原始工程依赖的jar包
  2. 首先读取MANIFEST.MF文件中的Main-Class属性,用来标记执行java -jar命令后运行的类JarLauncher
  3. JarLauncher类执行时会找到Start-Class属性,也就是启动类类名
  4. 运行启动类时会运行当前工程的内容
  5. 运行当前工程时会使用依赖的jar包,从lib目录中查找

④异常排查

启动SpringBoot工程时,可能会遇到端口占用的问题。

  • windows版
# 查询指定端口
$ netstat -ano |findstr "端口号"
# 根据进程PID查询进程名称
$ tasklist |findstr "进程PID号"
# 根据PID杀死任务
$ taskkill /F /PID "进程PID号"
# 根据进程名称杀死任务
$ taskkill -f -t -im "进程名称"
  • Linux版
# 查询指定端口
$ ps -ef | grep "端口号"
# 根据进程PID查询进程名称
$ ps -ef | grep "进程PID号"
# 根据PID杀死任务
$ kill -9 进程PID号
# 根据进程名称杀死任务
$ kill -9 进程名称

2.配置进阶

①临时属性

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

a.服务器中使用临时属性

SpringBoot提供了灵活的配置方式,如果你发现你的项目中有个别属性需要重新配置,可以使用临时属性的方式快速修改某些配置。方法也特别简单,在启动的时候添加上对应参数就可以了。

$ java –jar springboot.jar –-server.port=80

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

$ java –jar springboot.jar –-server.port=80 --logging.level.root=debug

b.开发环境中使用临时属性

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

做到这里其实可以产生一个思考了,如果对java编程熟悉的小伙伴应该知道,我们运行main方法的时候,如果想使用main方法的参数,也就是下面的args参数,就是在上面这个位置添加的参数。

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

原来是这样,通过这个args就可以获取到参数。再来看我们的引导类是如何书写的

public static void main(String[] args) {
    SpringApplication.run(SSMPApplication.class,args);
}

这个args参数居然传递给了run方法,看来在Idea中配置的临时参数就是通过这个位置传递到我们的程序中的。言外之意,这里如果不用这个args是不是就断开了外部传递临时属性的入口呢?是这样的,我们可以使用下面的调用方式,这样外部临时属性就无法进入到SpringBoot程序中了。

public static void main(String[] args) {
    SpringApplication.run(SSMPApplication.class);
}

或者还可以使用如下格式来玩这个操作,就是将配置不写在配置文件中,直接写成一个字符串数组,传递给程序入口。当然,这种做法并没有什么实际开发意义。

public static void main(String[] args) {
    String[] arg = new String[1];
    arg[0] = "--server.port=8082";
    SpringApplication.run(SSMPApplication.class, arg);
}

属性加载优先级

现在我们的程序配置受两个地方控制了,第一配置文件,第二临时属性。并且我们发现临时属性的加载优先级要高于配置文件的。那是否还有其他的配置方式呢?有的

打开官方文档中对应的内容,就可以查看配置读取的优先顺序。地址:属性加载优先级 (spring.io)

有14种配置的位置,而我们现在使用的是这里面的2个。

  • 第3条Config data说的就是使用配置文件,

  • 第11条Command line arguments说的就是使用命令行临时参数。

这14种配置的顺序就是SpringBoot加载配置的顺序,这个列表上面的优先级低,下面的优先级高

总结

  1. 使用jar命令启动SpringBoot工程时可以使用临时属性替换配置文件中的属性
  2. 临时属性添加方式:java –jar 工程名.jar –-属性名=值
  3. 多个临时属性之间使用空格分隔
  4. 临时属性必须是当前boot工程支持的属性,否则设置无效

③配置文件级别

SpringBoot提供4个级别配置文件。

  • 1.程序包所在目录中配置文件

  • 2.程序包所在目录中config目录下配置文件

  • 3.类路径下config目录下配置文件

  • 4.类路径下配置文件(一直使用的是这个,也就是resources目录中的application.yml文件)

其实就是提供给了4种配置文件书写的位置。上面4个文件的加载优先顺序为

  • 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。你继续写你的代码,你发现你原来写的配置文件被改成测试服务器的内容了,你要再改回来。现在明白了不?场景B中把你的内容都改掉了,你现在要重新改回来,以后呢?改来改去吗?

解决方案很简单,用上面第3这个级别的配置文件就可以快速解决这个问题,再写一个配置就行了。两个配置文件共存,因为config目录中的配置加载优先级比你的高,所以配置项如果和级别4里面的内容相同就覆盖了,这样是不是很简单?

级别1和2什么时候使用呢?程序打包以后就要用这个级别了,管你程序里面配置写的是什么?我的级别高,可以轻松覆盖你,就不用考虑这些配置冲突的问题了。

总结

  1. 配置文件分为4种

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

④自定义配置文件

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

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

  • 方式一:使用临时属性设置配置文件名,注意仅仅是名称,不要带扩展名
--spring.config.name=不带后缀的配置文件名字

  • 方式二:使用临时属性设置配置文件路径,这个是全路径名
--spring.config.location=classpath:/带后缀的配置文件名字

#也可以设置加载多个配置文件
--spring.config.location=classpath:/带后缀的配置文件名字,classpath:/带后缀的配置文件名字

温馨提示

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

总结

  1. 配置文件可以修改名称,通过启动参数设定
  2. 配置文件可以修改路径,通过启动参数设定
  3. 微服务开发中配置文件通过配置中心进行设置

3.多环境开发

程序最终要放到服务器上去运行。每个计算机环境不一样,这就是多环境。常见的多环境开发主要兼顾3种环境设置。

  • 开发环境——自己用的

  • 测试环境——自己公司用的

  • 生产环境——甲方爸爸用的

因为这是绝对不同的三台电脑,所以环境肯定有所不同,比如连接的数据库不一样,设置的访问端口不一样等等。

①yaml单一文件版

多环境开发就是针对不同的环境设置不同的配置属性即可。比如你自己开发时,配置你的端口如下:

server:
  port: 80

如何设计两组环境呢?中间使用三个减号分隔开

server:
  port: 80
---
server:
  port: 81

如何区分两种环境呢?起名字呗

spring:  # 环境1
    profiles: pro   
server:
    port: 80
---
spring:  # 环境2
    profiles: dev  
server:
    port: 81

那用哪一个呢?设置默认启动哪个就可以了

spring:
    profiles:
        active: pro		# 启动pro
---
spring:
    profiles: pro
server:
    port: 80
---
spring:
    profiles: dev
server:
    port: 81

其中关于环境名称定义上述格式是过时格式,标准格式如下

spring:
    config:
        activate:
            on-profile: pro

总结

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

②yaml多文件版

将所有的配置都放在一个配置文件中,尤其是每一个配置应用场景都不一样,这显然不合理,于是就有了将一个配置文件拆分成多个配置文件的想法。拆分后,每个配置文件中写自己的配置,主配置文件中写清楚用哪一个配置文件就好了。

主配置文件

spring:
    profiles:
        active: pro		# 启动pro

环境配置文件

server:
    port: 80

环境配置文件因为每一个都是配置自己的项,所以连名字都不用写里面了。

那如何区分这是哪一组配置呢?使用文件名区分。文件的命名规则为:application-环境名.yml。

application-pro.yaml

server:
    port: 80

application-dev.yaml

server:
    port: 81

在配置文件中,如果某些配置项所有环境都一样,可以将这些项写入到主配置中,只有哪些有区别的项才写入到环境配置文件中。

  • 主配置文件中设置公共配置(全局)
  • 环境配置文件中设置冲突属性(局部)

总结

  1. 可以使用独立配置文件定义环境属性

  2. 独立配置文件便于线上系统维护更新并保障系统安全性

③properties多文件版

properties文件多环境配置仅支持多文件格式

主配置文件

spring.profiles.active=pro

环境配置文件

application-pro.properties

server.port=80

application-dev.properties

server.port=81

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

④配置文件书写技巧

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

a.准备工作

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

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

b.使用

使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔

spring:
    profiles:
        active: dev
        include: devDB,devRedis,devMVC

现在相当于加载dev配置时,再加载对应的3组配置,从结构上就很清晰,用了什么,对应的名称是什么

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

c.改良

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

spring:
    profiles:
        active: dev
        group:
            "dev": devDB,devRedis,devMVC
              "pro": proDB,proRedis,proMVC
              "test": testDB,testRedis,testMVC

⑤多环境开发控制

如果maven和SpringBoot同时设置了多环境的话怎么搞。要想处理这个冲突问题,你要先理清一个关系,究竟谁在多环境开发中其主导地位。maven是做什么的?项目构建管理的,最终生成代码包的,SpringBoot是干什么的?简化开发的。简化,又不是其主导作用。最终还是要靠maven来管理整个工程,所以SpringBoot应该听maven的。大体思想如下:

  • 先在maven环境中设置用什么具体的环境
  • 在SpringBoot中读取maven设置的环境即可

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

<profiles>
    <profile>     <!--环境1-->
        <id>env_dev</id>
        <properties>
            <profile.active>dev</profile.active>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>		<!--默认启动环境-->
        </activation>
    </profile>
  
    <profile>     <!--环境2-->
        <id>env_pro</id>
        <properties>
            <profile.active>pro</profile.active>
        </properties>
    </profile>
</profiles>

b.SpringBoot中读取maven设置值

spring:
    profiles:
        active: @profile.active@

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

总结

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

4.日志

日志就是记录程序日常运行的信息,主要作用如下:

  • 编程期调试代码
  • 运营期记录信息
    • 记录日常运营重要信息(峰值流量、平均响应时长……)
    • 记录应用报错信息(错误堆栈)
    • 记录运维过程数据(扩容、宕机、报警……)

①开启日志

步骤①:添加日志记录操作

@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...";
    }
}

上述代码中log对象就是用来记录日志的对象,下面的log.debug,log.info这些操作就是写日志的API了。

步骤②:设置日志输出级别

日志设置好以后可以根据设置选择哪些参与记录。这里是根据日志的级别来设置的。日志的级别分为6种,分别是:

  • TRACE:运行堆栈信息,使用率低
  • DEBUG:程序员调试代码使用
  • INFO:记录运维过程数据
  • WARN:记录运维过程报警数据
  • ERROR:记录错误堆栈信息
  • FATAL:灾难信息,合并计入ERROR

一般情况下,开发时候使用DEBUG,上线后使用INFO,运维信息记录使用WARN即可。

下面就设置一下日志级别:

# 开启debug模式,输出调试信息,常用于检查系统运行状况
debug: true

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

# 开启debug模式,输出调试信息,常用于检查系统运行状况
debug: true

# 设置日志级别,root表示根节点,即整体应用日志级别
logging:
    level:
        root: debug

还可以再设置更细粒度的控制,可以直接控制指定包对应的日志输出级别

logging:
    level:
        root: warn
        # 设置某包包设置日志级别
      com.jianjian.config: debug

步骤③:设置日志组,控制指定包对应的日志输出级别

logging:
    # 设置日志组
    group:
        # 自定义组名,设置当前组中所包含的包
        book: com.jianjian.controller
        test: com.jianjian.dao,com.jianjian.service
    level:
        root: warn
      # 为对应组设置日志级别
      book: debug
      test: info

总结

  1. 日志用于记录开发调试与运维过程消息
  2. 日志的级别共6种,通常使用4种即可,分别是DEBUG,INFO,WARN,ERROR
  3. 可以通过日志组或代码包的形式进行日志显示级别的控制

②@Slf4j

每个类都要写创建日志记录对象,太繁琐,可以使用lombok给我们提供的工具类。

@RestController
@RequestMapping("/books")
public class BookController extends BaseClass{
    private static final Logger log = LoggerFactory.getLogger(BookController.class);	//这一句可以不写了
}

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

@Slf4j		//这个注解替代了上面那一行
@RestController
@RequestMapping("/books")
public class BookController extends BaseClass{
    @GetMapping
    public String getById(){
        log.debug("debug...");
        log.info("info...");
        log.warn("warn...");
        log.error("error...");
        return "springboot is running...";
    }
}

③日志输出格式

目前记录的格式是SpringBoot给我们提供的,如果想自定义控制就需要自己设置了。先分析一下当前日志的记录格式。

  • 级别用于做筛选过滤,PID与线程名用于做精准分析。

  • 所属类/接口名:当名称过长时,简化包名书写为首字母,甚至直接删除

下面是模拟上方图片中日志格式的配置方式

logging:
    pattern:
        console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"
  • %d: 时间
  • %clr:带颜色显示
  • %p: PID
  • %16t:%t代表线程 ,16代码显示长度为16
  • %-40.40c: %c代表所属类,-40代表左对齐显示长度40,.40长度超过40开始截断
  • {cyan}:自定义显示颜色
  • %m:消息
  • %n:换行

详细可参考:log4j 日志格式详解

④日志文件

如何把日志记录到文件中,方便后期维护查阅。设置日志文件名即可。

logging:
    file:
        name: server.log

为了便于维护,可以限制每个日志文件的大小,产生多个日志文件。

logging:
    logback:
        rollingpolicy:
            max-file-size: 3KB  # 限制每个日志文件大小
            file-name-pattern: server.%d{yyyy-MM-dd}.%i.log #日志文件命名规则

以上格式是基于logback日志技术设置每日日志文件的设置格式,要求容量到达3KB以后就转存信息到第二个文件中。文件命名规则中的%d标识日期,%i是一个递增变量,用于区分日志文件。

开发实用篇

1.热部署

①什么是热部署?

简单说就是你程序改了,现在要重新启动服务器,嫌麻烦?不用重启,服务器会自己悄悄的把更新后的程序给重新加载一遍,这就是热部署。

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

非springboot项目热部署实现原理

开发非springboot项目时,我们要制作一个web工程并通过tomcat启动,通常需要先安装tomcat服务器到磁盘中,开发的程序配置发布到安装的tomcat服务器上。如果想实现热部署的效果有两种做法,一种是在tomcat服务器的配置文件中进行配置。另一种做法是通过IDE工具进行配置。核心思想是一样的,就是使用服务器去监控其中加载的应用,发现产生了变化就重新加载一次。

springboot项目热部署实现原理

​ 基于springboot开发的web工程其实有一个显著的特征,就是tomcat服务器内置了。服务器是以一个对象的形式在spring容器中运行的。本来我们期望于tomcat服务器加载程序后由tomcat服务器盯着程序,你变化后我就重新启动重新加载,但是现在tomcat和我们的程序是平级的了,都是spring容器中的组件,这下就麻烦了,缺乏了一个直接的管理权,那该怎么做呢?简单,再搞一个程序X在spring容器中盯着你原始开发的程序A不就行了吗?确实,搞一个盯着程序A的程序X就行了,如果你自己开发的程序A变化了,那么程序X就命令tomcat容器重新加载程序A就OK了。并且这样做有一个好处,spring容器中东西不用全部重新加载一遍,只需要重新加载你开发的程序那一部分就可以了,这下效率又高了,挺好。

​ 下面就说说,怎么搞出来这么一个程序X,肯定不是我们自己手写了,springboot早就做好了,搞一个坐标导入进去就行了。

②手动启动热部署

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

步骤②:构建项目,可以使用快捷键(ctrl+F9或者command+F9)激活此功能

底层的工作工程原理:重启与重载

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

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

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

③自动启动热部署

上述过程每次进行热部署都需要开发者手工操作,不管是点击按钮还是快捷键都需要开发者手工执行。

自动热部署其实就是设计一个开关,打开这个开关后,IDE工具就可以自动热部署。因此这个操作和IDE工具有关,以下以idea为例设置idea中启动热部署

步骤①:设置自动构建项目

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

步骤②:允许在程序运行时进行自动构建

  • 老版IDEA操作方法:

使用快捷键【Ctrl+Alt+Shift+/ 或者 command+option+shift+/】打开维护面板,选择第1项【Registry…】

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

  • 新版本IDEA操作方法

关注:如果你每敲一个字母,服务器就重新构建一次,这未免有点太频繁了,所以idea设置当idea工具失去焦点5秒后进行热部署。其实就是你从idea工具中切换到其他工具时进行热部署,比如改完程序需要到浏览器上去调试,这个时候idea就自动进行热部署操作。

④热部署范围配置

其实并不是所有的文件修改都会激活热部署的,原因在于在开发者工具中有一组配置,当满足了配置中的条件后,才会启动热部署,配置中默认不参与热部署的目录信息如下

  • /META-INF/maven
  • /META-INF/resources
  • /resources
  • /static
  • /public
  • /templates

以上目录中的文件如果发生变化,是不参与热部署的。如果想修改配置,可以通过application.yml文件进行设定哪些文件不参与热部署操作

spring:
  devtools:
    restart:
      # 设置不参与热部署的文件或文件夹
      exclude: static/**,public/**,config/application.yml

⑤关闭热部署

线上环境运行时是不可能使用热部署功能的,所以需要强制关闭此功能,通过配置可以关闭此功能。

spring:
  devtools:
    restart:
      enabled: false

如果当心配置文件层级过多导致相符覆盖最终引起配置失效,可以提高配置的层级,在更高层级中配置关闭热部署。例如在启动容器前通过系统属性设置关闭热部署功能。

@SpringBootApplication
public class SSMPApplication {
    public static void main(String[] args) {
        System.setProperty("spring.devtools.restart.enabled","false");
        SpringApplication.run(SSMPApplication.class);
    }
}

2.配置进阶

①@ConfigurationProperties

在基础篇学习了@ConfigurationProperties注解,此注解的作用是用来为bean绑定属性的。

**a.**开发者可以在yml配置文件中以对象的格式添加若干属性

servers:
  ip-address: 192.168.0.1 
  port: 2345
  timeout: -1

**b.**然后再开发一个用来封装数据的实体类,注意要提供属性对应的setter方法

@Component
@Data
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}

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

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}

这样加载对应bean的时候就可以直接加载配置属性值了。

**d.**使用@ConfigurationProperties注解时,会出现一个提示信息

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

但是目前我们学的都是给自定义的bean使用这种形式加载属性值,如果是第三方的bean呢?能不能用这种形式加载属性值呢?为什么会提出这个疑问?原因就在于当前@ConfigurationProperties注解是写在类定义的上方,而第三方开发的bean源代码不是你自己书写的,你也不可能到源代码中去添加@ConfigurationProperties注解,这种问题该怎么解决呢?下面就来说说这个问题。

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

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

@Bean
public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
}

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

datasource:
  driverClassName: com.mysql.jdbc.Driver

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

@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
}

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

②@EnableConfigurationProperties

目前我们定义bean不是通过@Component定义就是通过@Bean定义,使用@ConfigurationProperties注解可以为bean进行属性绑定,那在一个业务系统中,哪些bean通过注解@ConfigurationProperties去绑定属性了呢?因为这个注解不仅可以写在类上,还可以写在方法上,所以找起来就比较麻烦了。

为了解决这个问题,spring给我们提供了一个全新的注解,专门标注使用@ConfigurationProperties注解绑定属性的bean是哪些。这个注解叫做@EnableConfigurationProperties。具体如何使用呢?

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

@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class SpringbootConfigurationApplication {
}

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

@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}

有人感觉这没区别啊?注意观察,现在绑定属性的ServerConfig类并没有声明@Component注解。当使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean,因此无需再次声明@Component注解了。

总结

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

③松散绑定

在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将属性名严格按照驼峰命名法书写,在yml配置文件中将datasource修改为dataSource,此时程序可以正常运行

dataSource:
  driverClassName: com.mysql.jdbc.Driver

然后又将代码中的前缀datasource修改为dataSource

@Bean
@ConfigurationProperties(prefix = "dataSource")
public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
}

此时就发生了编译错误,而且并不是idea工具导致的,运行后依然会出现问题,配置属性名dataSource是无效的

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进行属性绑定时的一个重要知识点了,有关属性名称的宽松绑定,也可以称为宽松绑定。

什么是宽松绑定?实际上是springboot进行编程时人性化设计的一种体现,即配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容。兼容到什么程度呢?几乎主流的命名格式都支持,例如:

在ServerConfig中的ipAddress属性名

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
}

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

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

也可以说,以上4种模式最终都可以匹配到ipAddress这个属性名。为什么这样呢?原因就是在进行匹配时,配置中的名称要去掉中划线和下划线后,忽略大小写的情况下去与java代码中的属性名进行忽略大小写的等值匹配,以上4种命名去掉下划线中划线忽略大小写后都是一个词ipaddress,java代码中的属性名忽略大小写后也是ipaddress,这样就可以进行等值匹配了,这就是为什么这4种格式都能匹配成功的原因。不过springboot官方推荐使用烤肉串模式,也就是中划线模式。

到这里我们掌握了一个知识点,就是命名的规范问题。再来看开始出现的编程错误信息

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描述了报错的原因,规范的名称应该是烤肉串(kebab)模式(case),即使用-分隔,使用小写字母数字作为标准字符,且必须以字母开头。然后再看我们写的名称dataSource,就不满足上述要求。闹了半天,在书写前缀时,这个词不是随意支持的,必须使用上述标准。

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

总结

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

④计量单位绑定

在配置中,我们书写如下配置值,其中第三项超时时间timeout描述了服务器操作超时时间,当前值是-1表示永不超时。

servers:
  ip-address: 192.168.0.1 
  port: 2345
  timeout: -1

但是每个人对这个值的理解会产生不同,比如线上服务器完成一次主从备份,配置超时时间240,这个240如果单位是秒就是超时时间4分钟,如果单位是分钟就是超时时间4小时。这个时候问题就来了,怎么解决这个误会?

除了加强约定之外,springboot充分利用了JDK8中提供的全新的计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个JDK8中新增的类,分别是Duration和DataSize

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    @DurationUnit(ChronoUnit.HOURS)
    private Duration serverTimeOut;
    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize dataSize;
}
  • Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)

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

​ 使用上述两个单位就可以有效避免因沟通不同步或文档不健全导致的信息不对称问题,从根本上解决了问题,避免产生误读。

⑤属性校验

在yml文件中书写配置时由于无法感知模型类中的数据类型,就会出现类型不匹配的问题,比如代码中需要int类型,配置中给了非法的数值,例如写一个“a”,这种数据肯定无法有效的绑定,还会引发错误。

SpringBoot给出了强大的数据校验功能,可以有效的避免此类问题的发生。在javaEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用Hibernate提供的校验框架来作为实现进行数据校验。

步骤①:开启校验框架

<!--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开启校验功能

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

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

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

通过设置数据格式校验,就可以有效避免非法数据加载。

⑥属性注入问题

先把问题描述一下,开发者连接数据库操作,但是运行程序后显示的信息是密码错误。

java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)

这是用户名和密码不匹配,就是密码输入错了,但是问题就在于密码并没有输入错误。如果是初学者,估计这会心态就崩了,我密码没错啊,你怎么能说我有错误呢?来看看用户名密码的配置是如何写的:

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

这名开发者的生日是1月27日,所以密码就使用了0127,其实问题就出在这里了。

在整数相关知识中有这么一句话,支持二进制,八进制,十六进制

这个问题就出在这里了,因为0127在开发者眼中是一个字符串“0127”,但是在springboot看来,这就是一个数字,而且是一个八进制的数字。当后台使用String类型接收数据时,如果配置文件中配置了一个整数值,他是先按照整数进行处理,读取后再转换成字符串。巧了,0127撞上了八进制的格式,所以后台先转换为十进制数字87再读取为“87”,这就导致密码错误的报错。解决方法为加上引号

这里提两个注意点,第一,字符串标准书写加上引号包裹,养成习惯,第二,遇到0开头的数据多注意吧。

3.测试进阶

①加载测试专用属性

测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,这个时候我们能不能每次测试的时候都去修改源码 application.yml 中的配置进行测试呢?显然是不行的。每次测试前改过来,每次测试后改回去,这太麻烦了。于是我们就想,需要在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试,这样就方便多了。

临时属性

SpringBoot已经为我们开发者早就想好了这种问题该如何解决,并且提供了对应的功能入口。在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:

//properties属性可以为当前测试用例添加临时的属性配置
@SpringBootTest(properties = {"test.prop=testValue1"})
public class PropertiesAndArgsTest {

    @Value("${test.prop}")
    private String msg;
    
    @Test
    void testProperties(){
        System.out.println(msg);
    }
}

使用注解@SpringBootTest的properties属性就可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。

临时参数

通过命令行参数也可以设置属性值。而且线上启动程序时,通常都会添加一些专用的配置信息。

//args属性可以为当前测试用例添加临时的命令行参数
@SpringBootTest(args={"--test.prop=testValue2"})
public class PropertiesAndArgsTest {
    
    @Value("${test.prop}")
    private String msg;
    
    @Test
    void testProperties(){
        System.out.println(msg);
    }
}

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

总结

加载测试临时属性可以通过注解@SpringBootTest的properties和args属性进行设定,此设定应用范围仅适用于当前测试用例

②加载测试专用配置

临时配置一些专用于测试环境的bean的需求,现在我们的需求其实就是在测试环境中再添加一个配置类,然后启动测试环境时,生效此配置就行了。其实做法和spring环境中加载多个配置信息的方式完全一样。具体操作步骤如下:

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

@Configuration
public class MsgConfig {
    @Bean
    public String msg(){
        return "bean msg";
    }
}

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

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

@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {

    @Autowired
    private String msg;

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

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

总结

定义测试环境专用的配置类,然后通过@Import注解在具体的测试中导入临时的配置,例如测试用例,方便测试过程,且上述配置不影响其他的测试类环境

③Web环境模拟测试

当前我们已经可以实现业务层和数据层的测试,并且通过临时配置,控制每个测试用例加载不同的测试数据。但是实际企业开发不仅要保障业务层与数据层的功能安全有效,也要保障控制层的功能正常。那么如何对控制层进行测试呢?

对控制层功能进行测试有三个要点:

  • 1.运行测试程序时,必须启动web环境,不然没法测试web功能。
  • 2.必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。
  • 3.测试过程必须出现预计值与真实值的比对结果才能确认测试结果是否通过。

因此测试控制层接口这项工作就转换成了三件事

  • 1.如何在测试类中启动web测试
  • 2.如何在测试类中发送web请求
  • 3.如何在测试类进行请求结果比对

测试类中启动web环境

@SpringBootTest注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境,具体如下:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {	
}

可以指定启动的Web环境对应的端口,springboot提供了4种设置值,分别如下:

  • MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
  • DEFINED_PORT:使用自定义的端口作为web服务器端口
  • RANDOM_PORT:使用随机端口作为web服务器端口
  • NONE:不启动web环境

建议大家测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。

测试类中发送请求

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

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

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc //开启虚拟MVC调用
public class WebTest {
    @Test
    void testWeb(@Autowired MockMvc mvc) {
    }
}

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc //开启虚拟MVC调用
public class WebTest {
    @Test
    void testWeb(@Autowired MockMvc mvc) throws Exception {
         //http://localhost:8080/books
        //创建虚拟请求,当前访问/books
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        //执行对应的请求
        mvc.perform(builder);
    }
}

web环境请求结果比对

  • 响应状态匹配

    @Test
    void testStatus(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
        //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
        //定义本次调用的预期值
        StatusResultMatchers status = MockMvcResultMatchers.status();
        ResultMatcher ok = status.isOk(); //预计本次调用时成功的:状态200
        //添加预计值到本次调用过程中进行匹配
        action.andExpect(ok);
    }
    
  • 响应体匹配(非json数据格式)

    @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);
    }
    
  • 响应体匹配(json数据格式,开发中的主流使用方式)

    @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);
    }
    
  • 响应头信息匹配

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

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

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

总结

  1. 在测试类中测试web层接口要保障测试类启动时启动web容器,使用@SpringBootTest注解的webEnvironment属性可以虚拟web环境用于测试
  2. 为测试方法注入MockMvc对象,通过MockMvc对象可以发送虚拟请求,模拟web请求调用过程
  3. web虚拟调用可以对本地虚拟请求的返回响应信息进行比对,分为响应头信息比对、响应体信息比对、响应状态信息比对

④数据层测试回滚

测试用例开发完成后,在打包的阶段由于test生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,

作为开发者希望测试用例正常运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?

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

@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「@RollBack(false)」即可正常提交事务。

总结

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

⑤测试用例数据设定

测试用例的数据如果固定书写肯定是不合理的,springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:

testcase:
  book:
    id: ${random.int}           # 随机整数
    id2: ${random.int(10)}      # 10以内随机整数
    type: ${random.int(5,10)}   #  10到20随机整数
    name: ${random.value}       #  随机字符串,MD5字符,32位
    uuid: ${random.uuid}        #  随机uuid
    publishTime: ${random.long} #  随机整数(long范围)

当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可

@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;
}

总结

使用随机数据可以替换测试用例中书写的固定数据,提高测试用例中的测试数据有效性

4.内置数据层

基础篇中用到的数据层解决方案是 Mysql+Druid+MyBatisPlus。而三个技术分别对应了数据层操作的三个层面:

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

那么 SpringBoot 有内置的数据解决方案吗?

①数据源技术

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

  • HikariCP

    • springboot官方推荐的数据源技术,作为默认内置数据源使用。
  • Tomcat提供DataSource

    • Tomcat提供的DataSource,如果不想用HikartCP,并且使用tomcat作为web服务器进行web程序的开发,使用这个。使用时把HikartCP技术的坐标排除掉就OK了。
  • Commons DBCP

    • 既不使用HikartCP也不使用tomcat的DataSource时,默认给你用这个。

我们之前配置druid时使用druid的starter对应的配置如下:

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删掉就行了,如下:

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地址要单独配置,如下:

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做进一步的配置,可以继续配置其独立的属性。例如:

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。

步骤①:导入jdbc对应的starter

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

步骤②:自动装配JdbcTemplate对象

@SpringBootTest
class Springboot15SqlApplicationTests {
    @Test
    void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
    }
}

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

@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实现查询操作(实体类封装数据的查询操作)

@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 book = new Book();
            book.setId(rs.getInt("id"));
            book.setName(rs.getString("name"));
            book.setType(rs.getString("type"));
            book.setDescription(rs.getString("description"));
            return book;
        }
    };
    List<Book> list = jdbcTemplate.query(sql, rm);
    System.out.println(list);
}

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

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

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

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

③数据库技术

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

  • H2
  • HSQL
  • Derby

我们一直使用MySQL数据库就挺好的,为什么有需求用这个呢?原因就在于这三个数据库都可以采用内嵌容器的形式运行,在应用程序运行后,如果我们进行测试工作,此时测试的数据无需存储在磁盘上,但是又要测试使用,内嵌数据库就方便了,运行在内存中,该测试测试,该运行运行,等服务器关闭后,一切烟消云散,多好,省得你维护外部数据库了。这也是内嵌数据库的最大优点,方便进行功能测试。三款数据库还可以独立安装。

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

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

<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数据库

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

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

spring:
  h2:
    console:
      enabled: true
      path: /h2

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

datasource:
  url: jdbc:h2:~/test
  hikari:
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456

步骤④:使用JdbcTemplate或MyBatisPlus技术操作数据库

其实我们只是换了一个数据库而已,其他的东西都不受影响。一个重要提醒,别忘了,上线时,把内存级数据库关闭,采用MySQL数据库,关闭方式就是设置enabled属性为false即可。

总结

现在的可选技术就丰富多了,开发程序时就可以在技术中任选一套数据库解决方案了。

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

5.监控

监控的意义

  • 监控服务状态是否处理宕机状态

  • 监控服务运行指标

  • 监控程序运行日志

  • 管理服务状态

①可视化监控平台

Spring Boot Admin,这是一个开源社区项目,用于管理和监控SpringBoot应用程序。这个项目中包含有客户端和服务端两部分,而监控平台指的就是服务端。我们做的程序如果需要被监控,将我们做的程序制作成客户端,然后配置服务端地址后,服务端就可以通过HTTP请求的方式从客户端获取对应的信息,并通过UI界面展示对应信息。

服务端开发

步骤①:导入spring-boot-admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程,也可通过创建项目时使用勾选的形式完成(Ops/Codecentric’s Spring Boot Admin (Server))。

<dependency>
  <groupId>de.codecentric</groupId>
  <artifactId>spring-boot-admin-starter-server</artifactId>
  <version>2.7.3</version>
</dependency>

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

步骤②:在引导类上添加注解@EnableAdminServer,声明当前应用启动后作为SpringBootAdmin的服务器使用

@SpringBootApplication
@EnableAdminServer
public class SpringbootAdminServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootAdminServerApplication.class, args);
    }
}

步骤③:运行程序,启动后就可以访问 http://127.0.0.1:8080

客户端开发

步骤①:导入springboot admin对应的starter,版本与当前使用的springboot版本保持一致,并将其配置成web工程

上述过程也可以通过创建项目时使用勾选的形式完成,不过一定要小心,端口配置成与服务端不一样的,否则会冲突。

<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文件配置

spring:
  boot:
    admin:
      client:
        url: http://localhost:8080  #服务端url

步骤③:运行程序,启动后再次访问服务端程序。

当前监控了1个程序,点击进去查看详细信息。由于当前没有设置开放哪些信息给监控服务器,所以目前看不到什么有效的信息。下面需要做两组配置就可以看到信息了。

  1. 开放指定信息给服务器看

  2. 允许服务器以HTTP请求的方式获取对应的信息

spring:
  boot:
    admin:
      client:
        url: http://localhost:8080
management:
  endpoint:
    health:
      show-details: always # 开放所有的健康信息
  endpoints:
    web:
      exposure:
        include: "*" # 使用*表示查阅全部。记得带引号

配置后再刷新服务器页面,就可以看到所有的信息了。

以上界面中展示的信息量就非常大了,包含了13组信息,有性能指标监控,加载的bean列表,加载的系统属性,日志的显示控制等等。还可以配置多个客户端,通过配置客户端的方式在其他的springboot程序中添加服务端坐标,这样当前服务器就可以监控多个客户端程序了。

进入监控面板,如果你加载的应用具有功能,在监控面板中可以看到3组信息展示的与之前加载的空工程不一样。

  • 类加载面板中可以查阅到开发者自定义的类

  • 映射中可以查阅到当前应用配置的所有请求

  • 性能指标中可以查阅当前应用独有的请求路径统计数据

总结

  1. 开发监控服务端需要导入坐标,然后在引导类上添加注解@EnableAdminServer,并将其配置成web程序即可
  2. 开发被监控的客户端需要导入坐标,然后配置服务端服务器地址,并做开放指标的设定即可
  3. 在监控平台中可以查阅到各种各样被监控的指标,前提是客户端开放了被监控的指标

②监控原理

通过查阅监控中的映射指标,可以看到当前系统中可以运行的所有请求路径,其中大部分路径以/actuator开头

首先这些请求路径不是开发者自己编写的,其次这个路径代表什么含义呢?既然这个路径可以访问,就可以通过浏览器发送该请求看看究竟可以得到什么信息。

通过发送请求,可以得到上面json信息,其中每一组数据都有一个请求路径,而在这里请求路径中有之前看到过的health,发送此请求又得到了一组信息

{
    "status": "UP",
    "components": {
        "diskSpace": {
            "status": "UP",
            "details": {
                "total": 297042808832,
                "free": 72284409856,
                "threshold": 10485760,
                "exists": true
            }
        },
        "ping": {
            "status": "UP"
        }
    }
}

当前信息与监控面板中的数据存在着对应关系

原来监控中显示的信息实际上是通过发送请求后得到json数据,然后展示出来。按照上述操作,可以发送更多的以/actuator开头的链接地址,获取更多的数据,这些数据汇总到一起组成了监控平台显示的所有数据。

到这里我们得到了一个核心信息,监控平台中显示的信息实际上是通过对被监控的应用发送请求得到的。那这些请求谁开发的呢?打开被监控应用的pom文件,其中导入了springboot admin的对应的client,在这个资源中导入了一个名称叫做actuator的包。被监控的应用之所以可以对外提供上述请求路径,就是因为添加了这个包。

这个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端点为默认端点,不能关闭。

management:
  endpoint:
    health:						# 端点名称
      show-details: always
    info:						  # 端点名称
      enabled: true				# 是否开放

为了方便开发者快速配置端点,springboot admin设置了13个较为常用的端点作为默认开放的端点,如果需要控制默认开放的端点的开放状态,可以通过配置设置,如下:

management:
  endpoints:
    enabled-by-default: true	# 是否开启默认端点,默认值true

上述端点开启后,就可以通过端点对应的路径查看对应的信息了。但是此时还不能通过HTTP请求查询此信息,还需要开启通过HTTP请求查询的端点名称,使用“*”可以简化配置成开放所有端点的WEB端HTTP请求权限。

management:
  endpoints:
    web:
      exposure:
        include: "*"

整体上来说,对于端点的配置有两组信息,一组是endpoints开头的,对所有端点进行配置,一组是endpoint开头的,对具体端点进行配置。

management:
  endpoint:		# 具体端点的配置
    health:
      show-details: always
    info:
      enabled: true
  endpoints:	# 全部端点的配置
    web:
      exposure:
        include: "*"
    enabled-by-default: true

总结

  1. 被监控客户端通过添加actuator的坐标可以对外提供被访问的端点功能

  2. 端点功能的开放与关闭可以通过配置进行控制

  3. web端默认无法获取所有端点信息,通过配置开放端点功能

③自定义监控指标

端点描述了被监控的信息,除了系统默认的指标,还可以自行添加显示的指标,下面就通过3种不同的端点的指标自定义方式来学习端点信息的二次开发。

INFO端点

info端点描述了当前应用的基本信息,可以通过两种形式快速配置info端点的信息

  • 配置形式

    在yml文件中通过设置info节点的信息就可以快速配置端点信息

    management:
      info:
        env:
          enabled: true
    info:
      appName: @project.artifactId@
      version: @project.version@
      company: jianjian
      author: xiaojian
    

    配置完毕后,对应信息显示在监控平台上

  • 编程形式

    通过配置的形式只能添加固定的数据,如果需要动态数据还可以通过配置bean的方式为info端点添加信息,此信息与配置信息共存

    @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端点描述当前应用的运行健康指标,即应用的运行是否成功。通过编程的形式可以扩展指标信息。

@Component
public class HealthConfig extends AbstractHealthIndicator {
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        boolean condition = false;
        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状态。

Metrics端点

metrics端点描述了性能指标,除了系统自带的监控性能指标,还可以自定义性能指标。

@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;
    }
}

在性能指标中就出现了自定义的性能指标监控项

自定义端点

可以根据业务需要自定义端点,方便业务监控

@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请求路径可以获取到当前端点的信息,但是需要先开启当前端点对外功能,或者设置当前端点为默认开发的端点。

总结

  1. 端点的指标可以自定义,但是每种不同的指标根据其功能不同,自定义方式不同
  2. info端点通过配置和编程的方式都可以添加端点指标
  3. health端点通过编程的方式添加端点指标,需要注意要为对应指标添加启动状态的逻辑设定
  4. metrics指标通过在业务中添加监控操作设置指标
  5. 可以自定义端点添加更多的指标

Sponsor❤️

您的支持是我不断前进的动力,如果您感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰

支付宝 微信

文章作者: 简简
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 简简 !
评论
填上邮箱会收到评论回复提醒哦!!!
 上一篇
SpringBoot-原理篇 SpringBoot-原理篇
原理篇包含自动配置工作流程、自定义starter开发、springboot程序启动流程、掌握SpringBoot内部工作流程、理解SpringBoot整合第三方技术的原理 小简从 0 开始学 Java 知识之 Java-学习路线 中的《
2022-09-15
下一篇 
SpringBoot-基础篇 SpringBoot-基础篇
基础篇包含如何创建一个SpringBoot工程、SpringBoot的基础配置语法格式、常见实用技术整合、整合综合应用。 小简从 0 开始学 Java 知识之 Java-学习路线 中的《SpringBoot-基础篇》,不定期更新所学笔记
2022-09-06
  目录