《SpringBoot 3:入门与应用实战》第 6 章 Spring Boot 最佳实践 阅读笔记 11
6.3 Banner 机制
在启动 Spring Boot 应用时,我们会在控制台中看到一个图案,这种特殊的启动图案被称为 "Banner"。

6.3.1 Banner 的变更
默认情况下,Spring Boot会自动加载classpath下的 banner.txt 文件,如果加载成功,则会将 banner.txt 中的内容打印在控制台上,用这种方式就可以覆盖 Spring Boot 原有的 Banner。
另外,我们可以改变 spring.banner.location 的配置值来修改 Banner 文件的路径,如 spring.banner.location=classpath:mybanner.txt 代表加载Banner 文件的规则是从 classpath 下加载 mybanner.txt 文件。
制作自己的 Banner 图案有 3 种方式,
第一种是手工制作,不过这种方式比较耗时费神;
第二种是从网络中下载别人制作的成品 Banner,这种 Banner 通常比较有趣;
第三种比较常用的方式是借助一些 Banner 制作网站,导出 banner.txt 文件并放入 src/main/resources 目录中,重启 Spring Boot 应用即可看到效果。
在 Spring Boot 2.x 及之前版本,Spring Boot 还支持图片 Banner,但随着 Spring Boot 升级到 3.0 后该功能被移除。


6.3.2 Banner 的输出模式
默认情况下 Spring Boot 会将 Banner 打印到控制台中,我们可以编写配置 spring.main.banner-mode=log 将 Banner 打印到日志文件中,也可以声明 spring.main.banner-mode=off 关闭 Banner 的打印功能。除了使用配置文件指定的方式,我们还可以在主启动类的 main 方法中指定Banner 的输出模式。

java
package com.yangjunbo.springboot.practice;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Properties;
@SpringBootApplication
public class SpringbootPracticeBApplication {
//@Value("${person.name}")
//private String name;
// public static void main(String[] args) {
//
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// //System.out.println(ctx.getBean(SpringbootPracticeBApplication.class).name);
// System.out.println(ctx.getBean(Person.class));
//
// }
// public static void main(String[] args) {
//
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication. class);
// Properties properties = new Properties();
// properties.setProperty("server.port", "9999");
// springApplication.setDefaultProperties(properties);
// springApplication.run(args);
//
// }
// public static void main(String[] args) {
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
// springApplication.setAdditionalProfiles("sky");
/* 等同于
SpringApplication springApplication = new SpringApplicationBuilder(
SpringBootPracticeApplication.class).profiles("sky").build();
*/
// var ctx = springApplication.run(args);
// System.out.println("rabbit是否存在:" + ctx.containsBeanDefinition("rabbit"));
// System.out.println("turtle是否存在:" + ctx.containsBeanDefinition("turtle"));
// System.out.println("fish是否存在:" + ctx.containsBeanDefinition("fish"));
// System.out.println("bird是否存在:" + ctx.containsBeanDefinition("bird"));
// System.out.println("bat是否存在:" + ctx.containsBeanDefinition("bat"));
// }
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
springApplication.setAdditionalProfiles("sky");
springApplication.setBannerMode(Banner.Mode.OFF);
springApplication.run(args);
}
}
6.4 日志的使用
日志是每个企业级项目开发中必备的组件,即使我们不主动打印日志,仅由底层框架本身也会输出非常多的日志,前面我们使用 Spring Boot 运行的测试示例中就是如此。合理的日志输出可以辅助开发者有效地定位和排查问题,对开发和运维阶段而言是非常重要的。
6.4.1 日志门面与实现
在讲解 Spring Boot 内部的日志使用之前,先了解一个概念:日志也有类似于规范和实现的区分 。读者可能在初学 Java Web 时了解 Log4j 或Logback 等日志框架,这些都属于具体的实现。设计模式的依赖倒转原则指出,应当使用抽象(规范)而不是具体的框架实现,以避免因切换底层框架实现而导致项目代码无法正确编译的问题(类似于 JDBC 规范与具体数据库的实现)。日志的顶层有一个 "日志门面" Slf4j(Simple Logging Facade for Java),它就是日志的抽象层规范之一,也是 Spring Boot 默认依赖的日志抽象层门面。
具体到日志的实现中,Spring Boot 提供了 3 种整合的框架:JUL (Java Util Logging) 、Logback、Log4j2,默认使用的是 Logback 。Spring Boot 的底层是 Spring Framework,而 Spring Framework 使用的日志是 Apache 开源的 commons-logging,这个日志框架本身也是一个门面,底层会根据项目中引入的其他门面或具体实现来动态选择。
正如我们从前面的测试示例中所见,我们没有显式声明导入日志相关的依赖,Spring Boot 默认就会打印很多日志,这是因为日志的标准启动器spring-boot-starter-logging 已经被整合在 spring-boot-starter-web 的父依赖 spring-boot-starter 中,根据传递依赖的原则,项目中自然也就有了日志相关的依赖。
spring-boot-starter 是 Spring Boot 绝大多数官方场景启动器的基础依赖,这就意味着几乎所有的场景启动器被引入项目中时底层会有一个spring-boot-starter 依赖,而 spring- boot-starter 中又导入了 spring-boot-starter-logging,所以我们使用 Spring Boot 的官方场景启动器时,通常不需要再显式导入 spring-boot-starter-logging。
6.4.2 使用日志打印
使用日志打印的方式非常简单,通常情况下我们只需要按照一个标准编写,这种写法适合所有可以创建对象的类,要用到哪个类,直接将其复制粘贴即可;如果是静态类或者工具类等无法创建对象的,则可以使用静态成员指定。
java
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingService.class);
注意,导入包的时候一定要选择 org.slf4j 包下的类,不要选 Logback 或其他包下的类(依赖门面而不是实现)。
实际使用时,我们可以调用 Logger 的方法来打印日志。

java
package com.yangjunbo.springboot.practice.examplec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class LoggingService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public void printLog() {
logger.debug("这是一条debug日志");
logger.info("这是一条info带参数的日志:{}", "我是参数");
logger.warn("这是一条warn日志");
logger.error("这是一条error带异常信息的日志", new NullPointerException());
}
}
编写完毕后,我们在主启动类获取 LoggingService 并执行 printLog 方法,控制台打印了 3 条日志,说明默认情况下 Spring Boot 打印的等级是INFO 级。

java
package com.yangjunbo.springboot.practice;
import com.yangjunbo.springboot.practice.examplec.LoggingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.Properties;
@SpringBootApplication
public class SpringbootPracticeBApplication {
//@Value("${person.name}")
//private String name;
// public static void main(String[] args) {
//
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// //System.out.println(ctx.getBean(SpringbootPracticeBApplication.class).name);
// System.out.println(ctx.getBean(Person.class));
//
// }
// public static void main(String[] args) {
//
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication. class);
// Properties properties = new Properties();
// properties.setProperty("server.port", "9999");
// springApplication.setDefaultProperties(properties);
// springApplication.run(args);
//
// }
// public static void main(String[] args) {
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
// springApplication.setAdditionalProfiles("sky");
/* 等同于
SpringApplication springApplication = new SpringApplicationBuilder(
SpringBootPracticeApplication.class).profiles("sky").build();
*/
// var ctx = springApplication.run(args);
// System.out.println("rabbit是否存在:" + ctx.containsBeanDefinition("rabbit"));
// System.out.println("turtle是否存在:" + ctx.containsBeanDefinition("turtle"));
// System.out.println("fish是否存在:" + ctx.containsBeanDefinition("fish"));
// System.out.println("bird是否存在:" + ctx.containsBeanDefinition("bird"));
// System.out.println("bat是否存在:" + ctx.containsBeanDefinition("bat"));
// }
// public static void main(String[] args) {
//
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
// springApplication.setAdditionalProfiles("sky");
// springApplication.setBannerMode(Banner.Mode.OFF);
// springApplication.run(args);
//
// }
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
LoggingService loggingService = ctx.getBean(LoggingService.class);
loggingService.printLog();
}
}
6.4.3 日志格式
Spring Boot默认输出的日志样例。
log
2026-08-24T16:48:37.095+08:00 INFO 21608 --- [springboot-practice-b] [ main] c.y.s.practice.examplec.LoggingService : 这是一条info带参数的日志:我是参数
输出内容主要包含以下信息。
- 日志输出的时间和日期,精度为毫秒级。
- 日志级别,包含 ERROR、WARN、INFO、DEBUG、TRACE( Logback 中没有 FATAL)。
- 当前应用的进程 ID。
- ---(三条短横线),代表这是分隔符,分隔符后面的内容是日志的主体信息。
- 打印当前日志的线程名称,使用方括号括起来。
- 打印当前日志的所在类名/日志组件名(通常是一个全限定类名)。
- 日志的具体内容。
如果需要改变默认的日志格式,我们可以修改 application.properties 或 application.yml 文件的内容,比如把日志的内容稍做精简,这样输出的日志时间里没有毫秒值,中间的三条短横线被替换为箭头。

yaml
spring:
application:
name: springboot-practice-b
profiles:
active: dev
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} %p [%t] ----> %c{50} %m%n"
# profiles:
# group:
# excludeland:
# - ocean
# - sky
# all:
# - land
# - ocean
# - sky
# active: all
# profiles:
# default: land
# active: land
# include: sky
#person:
# name: '小\t帅'
# name:
# |
# 123
# 345
# 567
# name:
# >
# abc
# cde
# efg
person:
name: 小帅
age: 20
# 默认的日期格式为yyyy/MM/dd HH:mm:ss
birthday: 2000/01/01 10:00:00
# 数组/集合,以下两种方式均可
alias:
- 张三
- 三三来迟
tels: [ 88881234, 12345678 ]
# 对象数组/集合
cats:
- name: 咪咪
age: 2
- name: 喵喵
age: 3
# Map/嵌套对象
events:
eight: 起床
nine: 撸猫
twenty: 睡觉
# Map<String, Object> 对象可以直接用JSON形式
dogs:
wang:
name: 旺旺
age: 4
wuwu: { name: 呜呜, age: 5 }
具体的可编写项如下,可根据偏好定制日志格式。
- %m:输出代码中指定的日志信息。
- %p:输出优先级。
- %n:换行符。
- %r:输出应用启动到产生该日志信息所耗费的毫秒数。
- %c:输出打印语句所属的类的全名。
- %t:输出产生该日志的线程全名。
- %d:输出服务器当前时间,默认遵循 ISO 8601 标准,也可以指定格式,如 %d{yyyy-MM- ddHH:mm:ss.SSS}。
- %F:输出日志消息产生时所在的文件名称。
- %L:输出代码中的行号。
- %%:输出一个 % 符号。
6.4.4 日志级别
与传统的控制台打印不同,日志的打印可以根据日志内容划分重要性级别。Spring Boot 支持的所有日志级别如下。
- OFF:关闭日志,不打印。
- ALL:打印所有日志,只要代码中包含的都会打印。
- TRACE:追踪日志,可用于追踪底层框架的代码执行流程,几乎不使用。
- DEBUG:调试日志,打印调试细节相关的日志(调试时常用)。
- INFO:信息日志,打印关键的信息(Spring Boot默认级别)。
- WARN:警告日志,打印一些警告但不至于产生错误的信息。
- ERROR:业务错误日志,打印此类信息时通常会伴随异常的产生和抛出。
- FATAL:致命日志,打印此类日志时通常伴随应用崩溃等问题(Logback不支持,若配置FATAL会降级到ERROR)。
注意一点,当指定日志级别后,最终打印的日志内容的级别包含当前级别及更高的级别(例如指定 INFO,会打印 INFO、WARN、ERROR 和FATAL 级别的日志)。
指定应用全局日志级别的方式,可以在 application.properties 中配置 logging.level.root=debug,如此配置后当前应用的全部日志都会打印DEBUG 及以上的日志。如果需要指定某一个类或者某一个包下所有类的日志级别,则可以直接声明对应的类名或包名。
logging.level.root=info
logging.level.org.springframework.boot=error
logging.level.org.springframework.boot.SpringApplication=debug
6.4.5 日志分组
如果像 6.4.4 节的方式逐个配置日志级别,当配置内容规模扩大后,万一需要批量修改,改动量会很大且费时费力。为此,Spring Boot 提供了日志分组功能,与 Profile 的环境分组类似,我们可以把相同类型的包或类配置成一个日志组,再通过指定日志组的方式设置日志级别。
例如,指定所有与 Tomcat 相关的日志都为 DEBUG 级别。重启工程,可以发现控制台中多了很多 Tomcat 相关的 Debug 日志打印,证明日志分组的配置生效了。
logging.group.tomcat=org.apache.tomcat,org.apache.catalina,org.apache.coyote
logging.level.tomcat=debug
Spring Boot 默认提供了两个内置的分组,可以直接声明该内置分组的级别。除了使用这些内置分组,还可以自行定义日志分组并加以使用。
| 分组名称 | 包含的 Logger (日志记录器) |
|---|---|
| web | org.springframework.core.codec org.springframework.http org.springframework.web org.springframework.boot.actuate.endpoint.web org.springframework.boot.web.servlet.ServletContextInitializerBeans |
| sql | org.springframework.jdbc.core org.hibernate.SQL org.jooq.tools.LoggerListener |
6.4.6 日志输出与归档
Spring Boot 默认将日志打印到控制台,在项目生产环境中这种做法显然不合理,因为我们需要将日志输出到文件中,以便我们后续查看。Spring Boot 给我们提供了两个配置项 logging.file.name 和 logging.file.path,分别用于指定日志的输出名称与位置。
# 指定输出的日志文件名
logging.file.name=boot.log
# 指定输出的日志文件名及路径
logging.file.name=E:/boot.log
# 指定输出的日志文件名及路径
logging.file.path=E:/boot.log
# 只指定输出的日志文件路径,此时日志文件名为spring.log
logging.file.path=E:/
logging.file.name 的优先级高于l ogging.file.path(配置 logging.file.name 后 logging.file.path 会失效),实际项目开发中更推荐使用logging.file.name。
日志全部输出到文件后,随着时间的推移,日志文件所占空间必定会逐渐庞大,到时如果想下载保存就会很麻烦。为此,Spring Boot 提供了日志的归档和切分功能,我们只需要指定一个日志文件的最大大小,Spring Boot 会帮我们把日志按日期切分(每天的日志保存在一起,而不存在一个文件中出现连续两天的日志内容)。另外,Spring Boot 默认按 10MB 切分日志,只要日志文件大小达到 10MB,Spring Boot 就会把之前的日志文件进行切分,后续产生的日志会输出到新的文件中。
Spring Boot 默认整合的日志框架实现是 Logback,如果需要变更相关配置,我们可以直接修改 application.properties 或 application.yml 配置文件;如果使用的是其他日志框架,则需要编写单独的配置文件(如 log4j2.xml)。
Spring Boot 支持的日志归档配置。
| 配置项 | 说明 | 默认值 |
|---|---|---|
logging.file.name |
日志文件的完整路径和名称。 | 无 (仅输出到控制台) |
logging.file.path |
日志文件存放的目录。 | 无 |
logging.logback.rollingpolicy.max-file-size |
单个日志文件的最大大小。达到此大小后会触发归档。 | 10MB |
logging.logback.rollingpolicy.max-history |
保留的归档日志文件的最大天数(或个数)。 | 7 |
logging.logback.rollingpolicy.total-size-cap |
所有归档日志文件的总大小上限。超过此限制后,最旧的归档文件会被删除。 | 无限制 |
logging.logback.rollingpolicy.file-name-pattern |
归档文件的命名模式。 | spring.log.%d{yyyy-MM-dd}.%i.gz |
logging.logback.rollingpolicy.clean-history-on-start |
是否在应用启动时清理旧的归档日志。 | false |
6.4.7 切换日志实现
Spring Boot 默认使用 Logback 作为日志框架的具体实现,如果需要切换到其他的日志框架(比如 Log4j2),则可以显式声明导入 spring-boot-starter 依赖,并将其中的 spring-boot-starter-logging 依赖去除,导入新的依赖 spring-boot-starter-log4j2,之后如果需要对 Log4j2 进行配置,则直接创建名为 log4j2-spring.xml 的配置文件,并在其中配置即可。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
可能有读者对上述的去除依赖做法感到疑惑,为什么要显式声明导入 spring-boot-starter 之后再去除?这是因为 spring-boot-starter-logging 依赖在 spring-boot-starter 中,我们完全可以直接在 spring-boot-starter-web 中去除,但这样做的前提是 spring-boot-starter-web 必须位于整个 标签的第一个依赖,否则可能会出现去除失效的异常情况,为保证去除效果,笔者更推荐显式导入 spring-boot-starter 依赖,并在这个依赖中去除 spring- boot-starter-logging 坐标。
6.5 启动过程的简单扩展点
Spring Boot 在 Spring Framework 原有的基础上新增了许多扩展点。
6.5.1 启动过程简单概述
Spring Boot 应用的整体启动过程可以大致分为以下 3 个阶段。
- 准备 Spring Boot 应用,这个阶段主要完成的工作包含识别 Web 环境、设置一些内置的组件等。
- 初始化 IOC 容器并启动(刷新),这个阶段的核心工作是创建和刷新 ApplicationContext,即 IOC 容器,应用中绝大部分的组件在这个阶段被初始化。
- IOC 容器启动完毕后的处理,包括广播事件、回调扩展的组件等。
ApplicationContext 的主要生命周期中包含创建、刷新、关闭三个环节,注意创建环节中 IOC 容器中的组件大多没有被创建,而是在刷新环节才被创建,这就意味着 IOC 容器的刷新环节是应用启动的最重要环节。
6.5.2 启动容器前的扩展
启动容器前,即准备 Spring Boot 应用的阶段,这个阶段中介绍一个重要的组件:ApplicationContextInitializer,这个组件可以在 IOC 容器创建后但尚未刷新的环节切入扩展逻辑。上面的小提示中已经提到,ApplicationContext 有一个刷新动作,在这个刷新动作之前是创建动作,那么这两个动作之间就可以插入新的扩展逻辑,其中的一个实现方式即本节介绍的 ApplicationContextInitializer,它可以操纵 ApplicationContext 对象并执行一些额外的逻辑,不过这个阶段中扩展的逻辑相对抽象,读者仅有一个初步印象即可。
6.5.3 启动容器时的扩展
在 4.3.4 节中提到了 ApplicationContext 的几个内置事件,其中在启动阶段触发的事件包含 ContextRefreshedEvent 和 ContextStartedEvent。如果我们在启动容器时扩展逻辑,一个经典的实现方式是编写一个监听器并监听 ContextRefreshedEvent 事件,只要 ContextRefreshedEvent 被广播,就意味着 IOC 容器已经刷新完毕,容器中绝大多数 bean 对象均已创建,此时切入扩展逻辑即可获取 ApplicationContext 本身,或者可以直接在监听器的内部使用 @Autowired 等注解注入需要扩展逻辑来处理的 bean 对象,操纵这些对象完成后续的扩展逻辑。
相关的代码编写方式与 Spring Framework 的完全一致,在 4.3 节中已经讲解了相关内容,本节不再重复讲解
6.5.4 启动完成后的扩展
当 IOC 容器彻底初始化完毕后,Spring Boot 还给我们扩展了两个新的接口,包括 ApplicationRunner 和 CommandLineRunner,用于扩展启动完成后的逻辑,这两个接口的使用方式相对简单,本节将讲解这两个接口的使用方式。
ApplicationRunner 与 CommandLineRunner 的具体使用方式都是编写接口的实现类(或匿名内部类),之后注册到 IOC 容器即可,由于是注册到 IOC 容器的 Bean,所以它们也支持使用 @Autowired 等注解注入其他 Bean。另外,ApplicationRunner 与 CommandLineRunner 的类上都被标注了 @FunctionalInterface 注解,我们可以使用 Lambda 表达式来快速编写匿名内部类,通过注解配置类 + @Bean 注解的方式快速注册。
以两种不同的方式注册后,下面我们可以直接重新启动当前工程,观察控制台可以输出如下内容,证明已经正确运行。

java
package com.yangjunbo.springboot.practice.exampled;
import com.yangjunbo.springboot.practice.examplea.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class TestApplicationRunner implements ApplicationRunner {
@Autowired
private Person person;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("TestApplicationRunner run ......");
System.out.println(person);
}
}

java
package com.yangjunbo.springboot.practice.exampled;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RunnerConfiguration {
@Bean
public CommandLineRunner runner() {
return args -> {
System.out.println("CommandLineRunner run ......");
};
}
}

6.6 场景启动器与自动装配
Spring Boot 的强大,在底层离不开场景启动器 (starter) 和与之搭配的自动装配机制。Spring Boot 官方本身提供了很多封装好的场景启动器,但即便再多也很难覆盖所有的应用开发场景。每个大型项目中通常会有一些通用抽取的组件和配置,其他模块都需要依赖这些组件和配置,这就需要制作自定义的场景启动器。
6.6.1 场景启动器的结构
Spring Boot 官方提供的场景启动器大多遵循一个标准规范,即场景启动器本身没有代码,仅用来组织和维护该场景启动器所需导入的所有依赖。
6.6.2 自定义场景启动器
本节的示例中选择使用简化结构制作场景启动器。先拟定一个简单需求:制作一个场景启动器,当其他项目依赖该场景启动器时,自动向 IOC 容器中注入一个 Cat 和一个 Dog 对象。
1.搭建工程
场景启动器本身也是一个工程,所以我们再新建一个工程,本节的所有代码均在该工程下编写。关于 pom.xml 文件中的依赖,我们只需要导入spring-boot- starter-web。
2.编写组件
接下来简单编写两个组件,在具体的具有功能的场景启动器中,这些组件通常具备业务和功能上的含义,本节的组件仅用于演示。
分别创建一个 Cat 和 Dog 类,并指定 Cat 类绑定全局配置文件中 "animal.cat" 开头的配置属性,Dog 类则不予声明。

java
package com.yangjunbo.springboot.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "animal.cat")
public class Cat {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

java
package com.yangjunbo.springboot.starter;
public class Dog {
private String name;
private Integer sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", sex=" + sex +
'}';
}
}
3.编写自动配置类
我们编写的 Cat 和 Dog 类上都没有标注模式注解,所以只能通过注解配置类 + @Bean 的方式,将编写好的组件注册到 IOC 容器中。自动配置类在具体的代码编写上与常规的注解配置类大体没有区别,只是标注配置类的注解由 @Configuration 变为 @AutoConfiguration,并且可以在类上声明当前类生效的条件等。

java
package com.yangjunbo.springboot.starter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(prefix = "animal", name = "enable", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(Cat.class)
public class AnimalAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "animal.dog")
public Dog dog() {
return new Dog();
}
}
整个 AnimalAutoConfiguration 中包含 3 个要素。
(1) 通过使用 @ConditionalOnProperty 注解,可以实现仅当配置属性中包含一个 animal.enable 属性且值为 true 时,配置类才会生效。此外,若声明 matchIfMissing=true,则当该配置属性没有配置时,认定该条件也生效。
(2) 通过使用 @EnableConfigurationProperties 注解,可以直接向 IOC 容器中注册标注了 @ConfigurationProperties 注解的 Bean。
(3) 通过使用 @Bean + @ConfigurationProperties 注解,可以实现与 @EnableConfigurationProperties 注解相似的效果。
4.注册自动配置类
编写自动配置类后,要想让其发挥作用,就需要遵循 Spring Boot 的规范,将自动配置类注册到 Spring Boot 可以读取到的位置,也就是 5.4.4 节中提到的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中,所以接下来在 resources 目录中新建相应的文件,并将 AnimalAutoConfiguration 的全限定类名填写到文件中。
再次提醒读者,Spring Boot 3.0 以后只支持使用 imports 文件的方式注册自动配置类,而且这个文件的格式不是 properties,而是普通文本文档,只需要将所需注册的自动配置类每行一个罗列在文档中。

5.使用 starter 测试效果
为了测试 starter 的效果,我们可以在 springboot-practice-b 工程中导入该场景启动器,并获取对应的 Cat 和 Dog 对象检验场景启动器是否生效。

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yangjunbo</groupId>
<artifactId>springboot-practice-b</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-practice-b</name>
<description>springboot-practice-b</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.yangjunbo</groupId>
<artifactId>springboot-starter-c</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

java
package com.yangjunbo.springboot.practice;
import com.yangjunbo.springboot.practice.examplec.LoggingService;
import com.yangjunbo.springboot.starter.Cat;
import com.yangjunbo.springboot.starter.Dog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.Properties;
@SpringBootApplication
public class SpringbootPracticeBApplication {
//@Value("${person.name}")
//private String name;
// public static void main(String[] args) {
//
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// //System.out.println(ctx.getBean(SpringbootPracticeBApplication.class).name);
// System.out.println(ctx.getBean(Person.class));
//
// }
// public static void main(String[] args) {
//
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication. class);
// Properties properties = new Properties();
// properties.setProperty("server.port", "9999");
// springApplication.setDefaultProperties(properties);
// springApplication.run(args);
//
// }
// public static void main(String[] args) {
// var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
// springApplication.setAdditionalProfiles("sky");
/* 等同于
SpringApplication springApplication = new SpringApplicationBuilder(
SpringBootPracticeApplication.class).profiles("sky").build();
*/
// var ctx = springApplication.run(args);
// System.out.println("rabbit是否存在:" + ctx.containsBeanDefinition("rabbit"));
// System.out.println("turtle是否存在:" + ctx.containsBeanDefinition("turtle"));
// System.out.println("fish是否存在:" + ctx.containsBeanDefinition("fish"));
// System.out.println("bird是否存在:" + ctx.containsBeanDefinition("bird"));
// System.out.println("bat是否存在:" + ctx.containsBeanDefinition("bat"));
// }
// public static void main(String[] args) {
//
// SpringApplication springApplication = new SpringApplication(SpringbootPracticeBApplication.class);
// springApplication.setAdditionalProfiles("sky");
// springApplication.setBannerMode(Banner.Mode.OFF);
// springApplication.run(args);
//
// }
// public static void main(String[] args) {
// ConfigurableApplicationContext ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
// LoggingService loggingService = ctx.getBean(LoggingService.class);
// loggingService.printLog();
// }
public static void main(String[] args) {
var ctx = SpringApplication.run(SpringbootPracticeBApplication.class, args);
System.out.println(ctx.getBean(Cat.class));
System.out.println(ctx.getBean(Dog.class));
}
}
重新运行 SpringBootPracticeApplication 的 main 方法,观察控制台中打印了 Cat 和 Dog 对象的信息,但是内部的所有属性都为 null。如果我们在 application.yaml 文件中配置一些属性信息,则在重启应用后的控制台输出中可以看到相应的属性值。

yaml
spring:
application:
name: springboot-practice-b
profiles:
active: dev
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} %p [%t] ----> %c{50} %m%n"
# profiles:
# group:
# excludeland:
# - ocean
# - sky
# all:
# - land
# - ocean
# - sky
# active: all
# profiles:
# default: land
# active: land
# include: sky
#person:
# name: '小\t帅'
# name:
# |
# 123
# 345
# 567
# name:
# >
# abc
# cde
# efg
person:
name: 小帅
age: 20
# 默认的日期格式为yyyy/MM/dd HH:mm:ss
birthday: 2000/01/01 10:00:00
# 数组/集合,以下两种方式均可
alias:
- 张三
- 三三来迟
tels: [ 88881234, 12345678 ]
# 对象数组/集合
cats:
- name: 咪咪
age: 2
- name: 喵喵
age: 3
# Map/嵌套对象
events:
eight: 起床
nine: 撸猫
twenty: 睡觉
# Map<String, Object> 对象可以直接用JSON形式
dogs:
wang:
name: 旺旺
age: 4
wuwu: { name: 呜呜, age: 5 }
animal:
cat:
name: mimi
age: 2
dog:
name: wangwang
6.配置属性提示
手动声明的配置属性,在 IDE 中只有 animal.cat 开头的属性有提示,animal.dog 开头的配置没有提示;另外,即便是 animal.cat 开头的配置属性有提示,也没有任何含义提示,只有 Spring Boot 内部相关的配置属性有完备的提示信息。
想要让我们自定义的配置属性也能够有提示,需要我们在场景启动器的工程中再添加一个 spring-boot-configuration-processor 坐标,通过这个坐标可以将我们编写的配置属性信息和注释,在编译时生成一个可以让 IDE 解析的提示文件。

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yangjunbo</groupId>
<artifactId>springboot-starter-c</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-starter-c</name>
<description>springboot-starter-c</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意,只有类上标注了 @ConfigurationProperties 注解的类才能被 spring-boot-configuration-processor 处理,所以上面 animal.dog 开头的属性无法显示。对此,我们可以在 Dog 类上也标注 @ConfigurationProperties 注解,并取消 AnimalAutoConfiguration 中的 @Bean 注解标注,添加@EnableConfigurationProperties 的 Dog 注册。

java
package com.yangjunbo.springboot.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "animal.dog")
public class Dog {
private String name;
private Integer sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", sex=" + sex +
'}';
}
}

java
package com.yangjunbo.springboot.starter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(prefix = "animal", name = "enable", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties({Cat.class, Dog.class})
public class AnimalAutoConfiguration {
}
如此改造之后,重新编译 springboot-starter-c 工程,再编写 application.yaml 中的配置时就可以发现 animal.dog 开头的属性也得以显示。
虽然属性已经有提示,但这些属性的含义是什么?我们可以在这些属性上补充文档注释,比如在 Dog 类中补充文档注释。

java
package com.yangjunbo.springboot.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "animal.dog")
public class Dog {
/**
* 狗狗的名字
*/
private String name;
/**
* 狗狗的性别 1雄性 0雌性
*/
private Integer sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", sex=" + sex +
'}';
}
}
补充完文档注释后,再重新编译 springboot-starter-c 工程,此时再编写配置属性时就发现有了文档注释的提示。到此,与自定义场景启动器相关的基本编写知识讲解完毕。

