《SpringBoot 3:入门与应用实战》第 6 章 Spring Boot 最佳实践 阅读笔记 11

《SpringBoot 3:入门与应用实战》第 6 章 Spring Boot 最佳实践 阅读笔记 11

在启动 Spring Boot 应用时,我们会在控制台中看到一个图案,这种特殊的启动图案被称为 "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 后该功能被移除。

在线 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 工程,此时再编写配置属性时就发现有了文档注释的提示。到此,与自定义场景启动器相关的基本编写知识讲解完毕。

相关推荐
mlidongfeng1 小时前
[AI][昇腾950]TP(Transport Layer,传输层)学习笔记
笔记·学习
九硕智慧建筑一体化厂家1 小时前
楼宇智能优选!KNX总线系统打造稳定高效智能控制体系
运维·人工智能·笔记·智慧城市
对象存储与RustFS1 小时前
用 rclone 把现有 S3/MinIO 数据同步到 RustFS
后端·rust·开源
魔兽大山哥1 小时前
【NL2SQL 实战 05】sqlglot 这把刀:把 SQL 当结构处理,安全校验才不靠碰运气
后端
有来技术1 小时前
youlai-boot 实战:MinIO 停止维护,Docker 迁移 RustFS 完整记录
java·后端·docker
Lyy1 小时前
DevOps平台 — 第七篇:我的项目与表格组件抽取
后端·devops
唐青枫1 小时前
别只会写 fn:Zig 函数、错误处理、泛型与回调实战
后端
Rain的Java大神之路2 小时前
Docker搭建Redis集群完全指南
java·运维·redis·后端·docker·容器·架构
苏子寒2 小时前
Nano-VLLM全代码解析笔记(8)-qwen3与qwen3_moe
笔记·python·深度学习·ai·性能优化·vllm