SpringBoot3 基础教程之五基础特性

大纲

前言

本章节所需的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-06,除特别说明外。

SpringApplication

自定义 banner

推荐使用 Spring Boot Banner 在线生成工具,制作并下载英文 banner.txt 文件,然后将它放到项目的 /src/main/resources 目录下,这样就可以实现应用的个性化启动。

自定义 SpringApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
// 关闭Banner(此方式的配置优先级较低,低于配置文件)
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}

}

上面的代码等同于以下配置内容,值得一提的是,配置文件的优先级更高

1
2
# 关闭Banner
spring.main.banner-mode=off

FluentBuilder API

SpringBoot 支持以 Builder 方式构建 SpringApplication,通过 FluentBuilder API 设置应用属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class MainApplication {

public static void main(String[] args) {
new SpringApplicationBuilder()
.main(MainApplication.class)
.sources(MainApplication.class)
.bannerMode(Banner.Mode.OFF) // 关闭Banner(此方式的配置优先级较低,低于配置文件)
.run(args);
}

}

Profiles 环境隔离

简单介绍

Profiles 提供环境隔离能力,支持快速切换开发、测试、生产环境,使用步骤如下:

  • 1、指定环境:指定哪些组件、配置在哪个环境生效
  • 2、激活环境:这个环境对应的所有组件和配置就应该生效

基础使用

指定环境

  • Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效
  • 任何 @Component@Configuration@ConfigurationProperties 都可以使用 @Profile 注解,来指定自身何时被加载。值得一提的是,Spring 容器中的组件都可以被 @Profile 注解标记。
1
2
3
4
5
@Profile({"dev"})
@Configuration
public class WebConfiguration {

}
1
2
3
4
5
@Profile({"prod", "test"})
@Configuration
public class SecurityConfiguration {

}

环境激活

application.properties 配置文件中,指定需要激活的环境

1
spring.profiles.active=dev

或者同时激活多个环境

1
spring.profiles.active=dev,test

提示

  • 也可以使用命令行激活环境,如 java -jar xxxx.jar --spring.profiles.active=dev,test
  • 还可以配置默认环境,即不标注 @Profile 注解的组件永远都会生效
  • 以前默认环境叫 default
  • 自定义默认环境 spring.profiles.default=test
  • 不推荐使用自定义默认环境的方式,而是推荐使用激活方式激活指定环境

环境包含

包含指定的环境,即不管激活哪个环境,包含指定的环境都会生效

1
spring.profiles.include=dev

最佳实践

  • 生效的环境 = 激活的环境 / 默认环境 + 包含的环境
  • 企业项目里面的使用规则
    • 基础的配置内容,如 MyBatis、Log 写到包含环境中
    • 需要动态切换变化的配置内容,如 DataBase、Redis 写到激活的环境中

Profiles 分组

创建 prod 分组,指定包含的 dbmq 配置。当使用命令行激活 java -jar xxx.jar --spring.profiles.active=prod ,就会激活 prod 分组,包括激活 dbmq 的配置文件。

1
spring.profiles.group.prod=db,mq

或者使用数组的写法

1
2
spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq

Profiles 配置文件

  • application-{profile}.properties 可以作为指定环境的配置文件
  • 激活这个环境,对应的配置文件就会生效,最终生效的所有配置如下
    • application.properties 主配置文件,任意时候都会生效
    • application-{profile}.properties 指定环境配置文件,激活指定环境时会生效
    • 如果发生了配置冲突,默认以激活的环境配置文件为准,即 application-{profile}.properties 的优先级高于 application.properties

特别注意

spring.profiles.defaultspring.profiles.activespring.profiles.include 的配置信息只能写在 application.properties 中,如果写在 application-{profile}.properties 是无效的。

外部化配置

线上应用如何快速修改配置,并应用最新配置?

  • SpringBoot 使用 配置优先级 + 外部配置,可以简化配置更新、简化运维。
  • 只需要往 Jar 应用所在的文件夹放一个 application.properties 最新配置文件,重启项目后就能自动应用最新的配置信息,无需重新编译打包代码。

配置优先级

SpringBoot 允许将配置外部化,以便可以在不同的环境中使用相同的应用程序代码。支持使用各种外部配置源,包括 Properties 文件、YAML 文件、环境变量和命令行参数。使用 @Value 注解可以获取到配置参数的值,也可以用 @ConfigurationProperties 注解将所有属性绑定到 POJO 中。

  • 以下是 SpringBoot 属性源的加载顺序,后面的会覆盖前面的值。由低到高,高优先级配置会覆盖低优先级配置。
    • 默认属性(通过 SpringApplication.setDefaultProperties() 指定的)
    • @PropertySource 加载指定的配置(需要写在 @Configuration 类上才可生效)
    • 配置文件(application.properties/yml 等)
    • RandomValuePropertySource 支持的 random.* 配置(如 @Value("${random.int}")
    • 系统环境变量
    • Java 系统属性(System.getProperties()
    • JNDI 属性(来自 java:comp/env
    • ServletContext 初始化参数
    • ServletConfig 初始化参数
    • SPRING_APPLICATION_JSON 属性(内置在环境变量或系统属性中的 JSON)
    • 命令行参数
    • 测试属性 (@SpringBootTest 进行测试时指定的属性)
    • 测试类 @TestPropertySource 注解
    • Devtools 设置的全局属性($HOME/.config/spring-boot

总结

配置信息可以写在很多位置,常见的优先级顺序: 命令行 > 配置文件 > SpringApplication 配置

  • 配置文件的优先级如下(越往后优先级越高)
    • Jar 包内的 application.properties/yml
    • Jar 包内的 application-{profile}.properties/yml
    • Jar 包外的 application.properties/yml
    • Jar 包外的 application-{profile}.properties/yml

提示

  • 优先级顺序:包外 > 包内; 同级情况:Profiles 配置 > Application 配置
  • 建议使用同一种格式的配置文件。如果 xxx.propertiesxxx.yml 同时存在,则 xxx.properties 的优先级更高
  • 所有参数均可由命令行传入,使用 --参数项=参数值 格式,参数将会被添加到环境变量中,且优先级大于配置文件。比如 java -jar app.jar --name="Spring",可以使用 @Value("${name}") 获取参数值。
  • 实际应用场景
    • 包内: application.properties server.port=8000
    • 包内: application-dev.properties server.port=9000
    • 包外: application.properties server.port=8001
    • 包外: application-dev.properties server.port=9001
    • 应用启动后的端口: 命令行 > 9001 > 8001 > 9000 > 8000

外部配置说明

SpringBoot 应用在启动的时候,会自动寻找 application.propertiesapplication.yaml 配置文件,然后进行加载。配置文件的加载顺序如下(越往后优先级越高):

  • 类路径(内部)

    • 类根路径(classpath
    • 类根路径(classpath)下的 /config 子目录
  • 当前路径(Jar 应用所在的位置)

    • 当前路径
    • 当前路径下的 /config 子目录
    • /config 目录的直接子目录

外部配置总结

  • 最终效果:优先级由高到低,前面的覆盖后面的

    • 命令行 > 包外 config 直接子目录 > 包外 config 目录 > 包外根目录 > 包内目录
    • 同级比较
      • Profiles 配置 > Application 配置
      • Properties 配置 > YAML 配置
  • 规律总结:最外层的最优先

    • 命令行 > 所有方式
    • 包外 > 包内
    • /config 子目录 > 根目录
    • Profiles 配置 > Application 配置
    • 配置不同就都生效(互补),配置相同则高优先级会覆盖低优先级的配置

导入配置使用

使用 spring.config.import 可以导入额外的配置。

1
2
spring.config.import=classpath:/my.properties
my.property=value

无论以上写法的先后顺序是怎样,my.properties 的值总是优先于直接在文件中编写的 my.property

属性占位符使用

配置文件中可以使用 ${name:default} 形式取出之前配置过的值。

1
2
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}

JUnit 5 单元测试

整合单元测试

SpringBoot 提供一系列测试工具集及注解方便开发者进行测试。spring-boot-test 提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。值得一提的是,一般只需要导入 spring-boot-starter-test 即可整合单元测试。

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

spring-boot-starter-test 默认提供了以下库供开发者进行单元测试使用:

使用单元测试

组件测试

直接通过 @Autowired 注入容器中的组件即可进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assumptions.assumeTrue;

@SpringBootTest
public class StandardTests {

@Autowired
public User userService;

@Test
public void getUserById() {
User user = userService.getById(1L);
assumeTrue(user != null);
}

}

特别注意

单元测试类所在包的名称,必须与主启动类所在的包或者其子包的名称相同;否则单元测试类需要通过 @SpringBootTest 注解的 class 属性指定主启动类。

常用注解

提示

JUnit5 的注解与 JUnit4 的注解有所区别,详细说明请参考 官方文档

  • @Test: 表示方法是测试方法。但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试
  • @ParameterizedTest: 表示方法是参数化测试,下面会有详细介绍
  • @RepeatedTest: 表示方法可重复执行,下面会有详细介绍
  • @DisplayName: 为测试类或者测试方法设置展示名称
  • @BeforeEach: 表示在每个单元测试之前执行
  • @AfterEach: 表示在每个单元测试之后执行
  • @BeforeAll: 表示在所有单元测试之前执行
  • @AfterAll: 表示在所有单元测试之后执行
  • @Tag: 表示单元测试类别,类似于 JUnit4 中的 @Categories
  • @Disabled: 表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore
  • @Timeout: 表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith: 为测试类或测试方法提供扩展类引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class StandardTests {

@BeforeAll
static void initAll() {
}

@BeforeEach
void init() {
}

@DisplayName("😱")
@Test
void succeedingTest() {
}

@Test
void failingTest() {
fail("a failing test");
}

@Test
@Disabled("for demonstration purposes")
void skippedTest() {
}

@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}

@AfterEach
void tearDown() {
}

@AfterAll
static void tearDownAll() {
}

}

常用断言

方法说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
assertArrayEquals 数组断言
assertAll 组合断言
assertThrows 异常断言
assertTimeout 超时断言
fail 快速失败

嵌套测试

JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach@AfterEach 注解,而且嵌套的层次没有限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.clay.boot;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.EmptyStackException;
import java.util.Stack;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
@DisplayName("A stack")
public class TestingAStack {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}

}

参数化测试

参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为单元测试带来许多便利。利用 @ValueSource 等注解,指定传入的参数,将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及 String 类型,Class 类型
  • @NullSource: 表示为参数化测试提供一个 null 的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法的返回值需要是一个流)
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
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.commons.util.StringUtils;

import java.util.stream.Stream;

@SpringBootTest
@DisplayName("Input Param")
public class TestingInputParam {

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}

@ParameterizedTest
@MethodSource("method")
@DisplayName("方法来源参数测试")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}

}

SpringBoot 事件驱动开发

SpringBoot 提供了基于事件驱动的编程模型,允许开发者将代码以应用程序与 IOC 容器之间的事件进行的方式来组织,实现松散耦合和高内聚。在 SpringBoot 事件驱动的过程中,当事件被触发时,IOC 容器会通过 ApplicationEventPublisher 发布事件。事件监听器通过实现 ApplicationListener 接口,并指定其感兴趣的事件类型来响应此事件。SpringBoot 事件驱动支持异步监听,可以在异步环境中执行事件响应函数,达到事件和响应函数的解耦。例如可以基于 SimpleAsyncTaskExecutor 执行器实现简单的异步处理。本节完整的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-08

事件驱动使用介绍

SpringBoot 事件驱动开发,依赖应用启动过程生命周期事件感知(9 大事件)、应用运行中事件感知(无数种),详细介绍请看 这里

  • 事件发布:实现 ApplicationEventPublisherAware 接口,或者注入 ApplicationEventMulticaster
  • 事件监听:实现 ApplicationListener 接口,或者使用 @EventListener 注解

事件驱动使用场景

SpringBoot 事件驱动的常见使用场景

  • 系统级别:例如在应用启动或关闭时执行一些任务。
  • 业务处理:例如用户完成注册时,在后台发送电子邮件或短信等。
  • 更改数据或状态:例如在用户完成订单时,将订单数据写入数据库,并发送通知邮件。

SpringBoot 事件监听的常见使用场景

  • 对事件进行处理:如发送邮件,写日志,执行业务逻辑等。
  • 事件发布 / 订阅:多个组件可以监听同一事件,每个组件可以以自己独特的方式响应事件,例如使用事件获取数据等。
  • 事件模型:自定义 SpringBoot 事件可以成为一个轻量级的消息传递系统。

事件驱动使用案例

这里将模拟用户成功登录后,通过 SpringBoot 的事件驱动模型,分别执行增加用户积分、发送优惠券、记录系统日志等操作。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {

private String username;

private String password;

}
  • 控制器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
@RequestMapping("/user")
public class LoginController {

@Autowired
private EventPublisher eventPublisher;

@ResponseBody
@PostMapping("/login")
public String login(@RequestBody User user) {

// 处理用户登录业务

// 发送用户登录成功的事件
LoginSuccessEvent event = new LoginSuccessEvent(user);
eventPublisher.sendEvent(event);

return "Login Success";
}

}
  • 自定义事件,继承 ApplicationEvent
1
2
3
4
5
6
7
public class LoginSuccessEvent extends ApplicationEvent {

public LoginSuccessEvent(User user) {
super(user);
}

}
  • 定义事件发送器,实现 ApplicationEventPublisherAware 接口,获取 IOC 容器中的 ApplicationEventPublisher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class EventPublisher implements ApplicationEventPublisherAware {

private ApplicationEventPublisher eventPublisher;

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}

/**
* 发送事件
* @param event
*/
public void sendEvent(ApplicationEvent event) {
this.eventPublisher.publishEvent(event);
}

}
  • 接收事件,实现 ApplicationListener 接口(第一种方式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Service
public class AccountService implements ApplicationListener<LoginSuccessEvent> {

/**
* 接收事件
*
* @param event
*/
@Override
public void onApplicationEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
addAccountScore(user);
}

/**
* 增加用户积分
*
* @param user
*/
public void addAccountScore(User user) {
log.info("{} 加了 1 积分", user.getUsername());
}

}
  • 接收事件,使用 @EventListener 注解(第二种方式),可以通过 @Order 注解指定接收事件的顺序
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
@Slf4j
@Service
public class CouponService {

/**
* 接收事件
*
* @param event
*/
@Order(1)
@EventListener
public void onEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
sendCoupon(user);
}

/**
* 发送优惠券
*
* @param user
*/
public void sendCoupon(User user) {
log.info("{} 得到 1 张优惠券", user.getUsername());
}

}
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
@Slf4j
@Service
public class SysLogService {

/**
* 接收事件
*
* @param event
*/
@Order(2)
@EventListener
public void onEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
sendCoupon(user);
}

/**
* 记录系统日志
*
* @param user
*/
public void sendCoupon(User user) {
log.info("{} 成功登录系统", user.getUsername());
}

}