第 4 章:从 Spring Framework 到 Spring Boot

通过前面几个章节的介绍,相信大家已经对 Spring Framework 有了一个基本的认识,相比早期那些没有 Spring Framework 加持的项目而言,它让生产力产生了质的飞跃。但人们的追求是无止境的,这也驱动着技术的发展。开发者认为 Spring 还可以更好,于是 Spring Boot 诞生了。本章我们将一起了解一下 Spring Boot 的基础知识,还有它的两个重要功能——起步依赖与自动配置。

4.1 Spring Boot 基础知识

Spring Framework 提供了 IoC 容器、AOP、MVC 等众多功能,让开发者可以从烦琐的工作中抽离出来,只关注在自己的业务逻辑上。Perl 语言发明人 Larry Wal 说过一句名言:“懒惰,是程序员的第一大美德。” 当我们得到了一样东西,总会想着去追求更好的。而这个更好的东西就是 Spring Boot。有了 Spring Framework,为什么还需要搞出一个 Spring Boot?Spring Boot 又包含哪些东西呢?本节的内容将会回答这些问题。

4.1.1 为什么需要 Spring Boot

随着时间的推移,什么是烦琐的工作,这个定义也在发生变化。原先的参照物是 EJB 1. x 和 EJB 2. x,是徒手开发的 JSP 甚至是 CGI 程序;现在,创建一个基于 Spring 的项目本身变成了一件麻烦事——无论使用 Maven 还是 Gradle,要管理清楚这一堆依赖,避免出现冲突,已经是一场灾难了,我们永远都不知道哪个包里的同名类会带来什么“惊喜”。好不容易搞定了依赖,Spring Framework 的配置又该让人抓狂了,等到 Bean 的自动扫描和自动织入稍稍安抚了一下大家几近奔溃的内心,一大堆与业务逻辑没有太多关系的“模板”配置又“补了一刀”。当这些东西耗费的心智和开发业务逻辑相当,甚至超过业务逻辑时,开发者就该做点什么了。

就在广大开发者们快要接受这个事实,打算认命的时候,Spring 团队推出了一款代码生成器,它就是 Spring Roo 项目,官方介绍它是新一代的 Java 快速应用开发工具,在几分钟内就能构建一个完整的 Java 应用。但现实情况是大家不太买账,Spring Roo 一直都没能成为主流,截至本书写作之时,它的最新版本还是末次修改停留在 2017 年的 2.0.0 版本。虽然 Spring Roo 能帮忙生成各种代码和配置,但它们的数量并未减少。后来在笔者与 Spring 团队的 Josh Long 的一次交流过程中,他一语道破了真相,大意是:“如果一个东西可以生成出来,那为什么还要生成它呢?”

另一方面,Spring Framework 虽然解决了开发和测试的问题,但在整个系统的生命周期中,上线后的运维也占据了很大的比重,怎么样让系统具备更好的可运维性也是个重要的任务。怎么配置、怎么监控、怎么部署,都是要考虑的事情。

出于这些原因,Spring Boot 横空出世了,它解决了上面说到的各种痛点,再一次将生产力提升了一个台阶。正如 Spring Boot 项目首页上写的那样,Spring Boot 可以轻松创建独立的生产级 Spring 应用程序,拿来就能用了。这次,Spring Boot 站到了聚光灯下,成了新的主角。

4.1.2 Spring Boot 的组成部分

Spring Boot 提供了大量的功能,但其本身的核心主要是以下几点:

  • 起步依赖
  • 自动配置
  • Spring Boot Actuator
  • 命令行 CLI

在实际使用时,最后那项命令行 CLI 用得相对较少,因此本书并不会介绍它。此外,Spring Boot 同时支持 Java 与 Groovy,但在本书中,我们也不会涉及 Groovy 的内容。

  1. 起步依赖

    起步依赖(starter dependency)的目的就是解决 4.1.1 节中提到的依赖管理难题:针对一个功能,需要引入哪些依赖、它们的版本又是什么、互相之间是否存在冲突、它们的间接依赖项之间是否存在冲突……现在我们可以把这些麻烦都交给 Spring Boot 的起步依赖来解决。

    以我们在 1.2 节中创建的 HelloWorld 为例(即代码示例 1-1),我们只需在 Maven 的 POM 文件中引入 org.springframework.boot:spring-boot-starter-web 这个依赖,Spring Boot 就知道我们的项目需要 Web 这个功能,它实际上为我们引入了大量相关的依赖项。通过 mvn dependency:tree 可以查看 Maven 的依赖信息 ,其中就有如下的内容:

    +- org.springframework.boot:spring-boot-starter-web:jar:2.6.3:compile
    |  +- org.springframework.boot:spring-boot-starter:jar:2.6.3:compile
    |  |  +- org.springframework.boot:spring-boot:jar:2.6.3:compile
    |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.6.3:compile
    |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.6.3:compile
    |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.10:compile
    |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.10:compile
    |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.1:compile
    |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.17.1:compile
    |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.33:compile
    |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
    |  |  \- org.yaml:snakeyaml:jar:1.29:compile
    |  +- org.springframework.boot:spring-boot-starter-json:jar:2.6.3:compile
    |  |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.1:compile
    |  |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.1:compile
    |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.13.1:compile
    |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.1:compile
    |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.1:compile
    |  |  \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.1:compile
    |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.6.3:compile
    |  |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.56:compile
    |  |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.56:compile
    |  |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.56:compile
    |  +- org.springframework:spring-web:jar:5.3.15:compile
    |  |  \- org.springframework:spring-beans:jar:5.3.15:compile
    |  \- org.springframework:spring-webmvc:jar:5.3.15:compile
    |     +- org.springframework:spring-aop:jar:5.3.15:compile
    |     +- org.springframework:spring-context:jar:5.3.15:compile
    |     \- org.springframework:spring-expression:jar:5.3.15:compile
    

    可以看到,起步依赖是以功能为单位来组织依赖的。要实现某个功能,一共需要哪些依赖,我们自己不清楚,但 Spring Boot 知道。我们会在 4.2 节详细讲解起步依赖。

  2. 自动配置

    在没有使用 Spring Boot 时,对于一个 Web 项目,我们需要配置 DispatcherServlet 来处理请求,需要配置 Jackson JSON 来处理 JSON 的序列化,需要配置 Log4j2 或者 Logback 来打印日志……而在 1.2 节的 HelloWorld 例子中,我们并没有配置这些东西,Spring Boot 自己完成了所有的配置,我们只需要编写 REST 接口的逻辑就好了。

    这就是 Spring Boot 的自动配置,它能根据 CLASSPATH 中存在的类判断出引入了哪些依赖,并为这些依赖提供常规的默认配置,以此来消除模板化的配置。与此同时,Spring Boot 仍然给我们留下了很大的自由度,可以对配置进行各种定制,甚至能够排除自动配置。我们会在 4.3 节详细讲解自动配置。

  3. Spring Boot Actuator

    如果说前两项的目的是简化 Spring 项目的开发,那 Spring Boot Actuator 的目的则是提供一系列在生产环境运行时所需的特性,帮助大家监控并管理应用程序。通过 HTTP 端点或者 JMX,Spring Boot Actuator 可以实现健康检查、度量收集、审计、配置管理等功能。我们会在 5.1 节和 5.2 节详细讲解 Spring Boot Actuator。

4.1.3 解析 Spring Boot 工程

一个使用了 Spring Boot 的项目工程,本质上来说和只使用 Spring Framework 的工程是一样的,如果使用 Maven 来管理,那它就是个标准的 Maven 工程,大概的结构就像下面这样。

|-pom.xml
|-src
  |-main
    |-java
    |-resources
  |-test
    |-java
    |-resources

具体内容如下:

  • pom.xml 中管理了整个项目的依赖和构建相关的信息;
  • src/main 中是生产的 Java 代码和相关资源文件;
  • src/test 中是测试的 Java 代码和相关资源文件。

如果通过 Spring Initializr 来生成工程,它还会为我们生成用来启动项目的启动类,比如 HelloWorld 中的 Application 类,以及测试用的 ApplicationTest 类(这是个空的 JUnit 测试类)。其中 Application 类上加了 @SpringBootApplication 注解,表示这是应用的主类,在打包成可执行 Jar 包后,运行 Jar 包时会去调用这个主类的 main() 方法。

这里需要展开说明一下 POM 文件的内容,分为以下几个部分:

  • 工程自身的 GroupId、ArtifactId 与 Version 等内容定义;
  • 工程继承的 org.springframework.boot:spring-boot-starter-parent 定义;
  • <dependencies/> 依赖项定义;
  • <build/> 构建相关的配置定义。

org.springframework.boot:spring-boot-starter-parent 又继承了 org.springframework.boot:spring-boot-dependencies,它通过 <dependencyManagement/> 定义了大量的依赖项,有了 <dependencyManagement/> 的加持,在我们自己的工程中,只需要在 <dependencies/> 中写入依赖项的 <groupId><artifactId> 就好了,无须指定版本,有冲突的依赖项也在 <dependencyManagement/> 中排除了,无须重复排除。

<build/> 中的 org.springframework.boot:spring-boot-maven-plugin 在打包时能够生成可执行 Jar 包,它也是在 org.springframework.boot:spring-boot-starter-parent 中定义的。

在一些特殊情况下,我们的工程无法直接继承 org.springframework.boot:spring-boot-starter-parent,这时就可能失去 Spring Boot 的很多便利之处。为此,我们需要自己在 pom.xml 中做些额外的工作。

首先,增加 <dependencyManagement/>,导入 org.springframework.boot:spring-boot-dependencies 中的依赖项,这样就能利用其中定义的依赖了:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.6.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

接着,在 <build/> 中增加 org.springframework.boot:spring-boot-maven-plugin,这样打包时就能用上 Spring Boot 的插件,打出可执行的 Jar 包:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.6.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

通过上述修改,我们就能在不继承 org.springframework.boot:spring-boot-starter-parent 的情况下继续让 Spring Boot 替我们管理依赖并构建可执行 Jar 包了。

4.2 起步依赖

在自己管理依赖时,我们要为工程引入 Web 相关的支持,需要配置一堆依赖,但我们常常会搞不清楚哪些是必需的,哪些是多余的,最后只能不管三七二十一从某个现在能跑的工程里胡乱复制一通。

有了 Spring Boot,情况就不一样了。Spring Boot 按照功能划分了很多起步依赖,大家只需要知道自己要什么功能,比如要实现 Web 功能、需要 JPA 支持等,具体引入什么依赖、分别是什么版本,都可以交给起步依赖来管理。

此外,管理依赖时不仅要避免出现 GroupId 和 ArtifactId 相同但 Version 不同的依赖,还要注意同一个依赖项因为版本升级替换了 GroupId 或 ArtifactId 的情况。对于前者 Maven 会仅保留一个依赖,但它未必是你想要的那个,而对于后者则更糟糕,Maven 会认为这是两个不同的依赖,它们都会被保留下来。但用了 Spring Boot 的起步依赖之后,此类问题就能得到缓解,同一版本的 Spring Boot 中的各个起步依赖所引入的依赖不会产生冲突,因为官方对这些依赖进行了严格的测试。

所以说起步依赖是帮助大家摆脱依赖管理困局的一大利器,这节就让我们来了解一下 Spring Boot 都提供了哪些起步依赖,它背后的实现原理又是什么样的。

4.2.1 Spring Boot 内置的起步依赖

Spring Boot 官方的起步依赖都遵循一样的命名规范,即都以 spring-boot-starter- 开头,其他第三方的起步依赖都应该 避免使用这个前缀,以免引起混淆。

Spring Boot 内置了超过 50 个不同的起步依赖,表 4-1 罗列了其中 10 个常用的起步依赖。

表 4-1 一些常用的 Spring Boot 起步依赖

名称描述
spring-boot-starterSpring Boot 的核心功能,比如自动配置、配置加载等
spring-boot-starter-actuatorSpring Boot 提供的各种生产级特性
spring-boot-starter-aopSpring AOP 相关支持
spring-boot-starter-data-jpaSpring Data JPA 相关支持,默认使用 Hibernate 作为 JPA 实现
spring-boot-starter-data-redisSpring Data Redis 相关支持,默认使用 Lettuce 作为 Redis 客户端
spring-boot-starter-jdbcSpring 的 JDBC 支持
spring-boot-starter-logging日志依赖,默认使用 Logback
spring-boot-starter-securitySpring Security 相关支持
spring-boot-starter-test在 Spring 项目中进行测试所需的相关库
spring-boot-starter-web构建 Web 项目所需的各种依赖,默认使用 Tomcat 作为内嵌容器

后续大家还会接触到很多起步依赖,比如 Spring Cloud 的各种组件,也有第三方的,比如 MyBatis 的 mybatis-spring-boot-starter 和 Druid 的 druid-spring-boot-starter

在引入了起步依赖后,如果我们希望修改某些依赖的版本,如何操作呢?可以在 Maven 的 <properties/> 中指定对应依赖的版本。通常这种情况是想要升级某些依赖、修复安全漏洞或使用新功能,但 Spring Boot 的依赖并未升级。例如,想要指定 Jackson 的版本来升级 Jackson Databind,就可以像下面这样:

<properties>
    <jackson-bom.version>2.11.0</jackson-bom.version>
</properties>

具体的属性可以在 org.springframework.boot:spring-boot-dependencies 的 pom.xml 中寻找。当然,也可以再彻底一些,在项目 POM 文件的 <dependencies/> 中直接引入自己所需要的依赖,同时,在引入的起步依赖中排除刚才你所加的依赖。

Spring Boot 本身也提供了一些可以互相替换的起步依赖,例如,Log4j2 可以代替 Logback,Jetty 和 Netty 可以代替 Tomcat,如代码示例 4-1 所示。

代码示例 4-1 用 Log4j2 代替 Logback

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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>
</dependencies>

4.2.2 起步依赖的实现原理

如果熟悉 Maven,那么相信大家已经猜到了,起步依赖背后使用的其实就是 Maven 的传递依赖机制 。看似只添加了一个依赖,但实际上通过传递依赖,我们已经引入了一堆的依赖。

我们可以在 Maven 的 <dependencyManagement/> 中统一定义依赖的信息,比如版本、排除的传递依赖项等,随后在 <dependencies/> 中添加这个依赖时就不用再重复配置这些信息了。起步依赖与其中定义的依赖项都是通过这种方式定义的,所以使用了起步依赖后就不用再考虑版本和应该排除哪些东西了。

以 2.3.0.RELEASE 版本的 org.springframework.boot:spring-boot-starter-web 为例,它的 POM 文件分为如下三部分:

  • 起步依赖本身的描述信息
  • 导入依赖管理项
  • 具体依赖项

<dependencyManagement/> 中用 import 的方式导入 org.springframework.boot:spring-boot-dependencies 里配置的依赖信息,具体如下所示:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

随后,在 <dependencies/> 中配置具体要引入的依赖,而这些依赖所间接依赖的内容也会被传递进来,具体如下所示:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.3.0.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
        <version>2.3.0.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>2.3.0.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <scope>compile</scope>
    </dependency>
</dependencies>

而到了后面的版本,Spring Boot Starter 的内容变得更直接了,以 2.6.3 版本的 org.springframework.boot:spring-boot-starter-web 为例,其中去掉了 <dependencyManagement/> 的部分,所有依赖的版本信息直接硬编码写死在了 <dependencies/> 里。这两种方式对使用 Spring Boot 的开发者而言,在使用体验上并没有什么差异,所以我们不用在意这些细节。

4.3 自动配置

Spring Boot 可以根据 CLASSPATH、配置项等条件自动进行常规配置,省去了我们自己动手把一模一样的配置复制来复制去的麻烦。既然框架能猜到你想这么配,那它自己就能帮你搞定,如果它的配置不是我们想要的,再做些手动配置就好了。

我们已经在代码示例 1-1 中看到过 @SpringBootApplication 注解了,查看这个注解,可以发现它上面添加了 @EnableAutoConfiguration,它可以开启自动配置功能。这两个注解上都有 exclude 属性,我们可以在其中排除一些不想启用的自动配置类。如果不想启用自动配置功能,也可以在配置文件中配置 spring.boot.enableautoconfiguration=false,关闭该功能。

4.3.1 自动配置的实现原理

自动配置类其实就是添加了 @Configuration 的普通 Java 配置类,它利用 Spring Framework 4.0 加入的条件注解 @Conditional 来实现“根据特定条件启用相关配置类”,注解中传入的 Condition 类就是不同条件的判断逻辑。Spring Boot 内置了很多条件注解,表 4-2 中列举了 org.springframework.boot.autoconfigure.condition 包中的条件注解。

表 4-2 Spring Boot 内置的条件注解

条件注解生效条件
@ConditionalOnBean存在特定名称、特定类型、特定泛型参数或带有特定注解的 Bean
@ConditionalOnMissingBean与前者相反,不存在特定 Bean
@ConditionalOnClass存在特定的类
@ConditionalOnMissingClass与前者相反,不存在特定类
@ConditionalOnCloudPlatform运行在特定的云平台上,截至 2.6.3 版本,代表云平台的枚举类支持无云平台、CloudFoundry、Heroku、SAP、Kubernetes 和 Azure,可以通过 spring.main.cloud-platform 配置强制使用的云平台
@ConditionalOnExpression指定的 SpEL 表达式为真
@ConditionalOnJava运行在满足条件的 Java 上,可以比指定版本新,也可以比指定版本旧
@ConditionalOnJndi指定的 JNDI 位置必须存在一个,如没有指定,则需要存在 InitialContext
@ConditionalOnProperty属性值满足特定条件,比如给定的属性值都不能为 false
@ConditionalOnResource存在特定资源
@ConditionalOnSingleCandidate当前上下文中,特定类型的 Bean 有且仅有一个
@ConditionalOnWarDeployment应用程序是通过传统的 War 方式部署的,而非内嵌容器
@ConditionalOnWebApplication应用程序是一个 Web 应用程序
@ConditionalOnNotWebApplication与前者相反,应用程序不是一个 Web 应用程序

@ConditionalOnClass 注解为例,它的定义如下所示, @Target 指明该注解可用于类型和方法定义, @Rentention 指明注解的信息在运行时也能获取到,而其中最关键的就是 OnClassCondition 条件类,里面是具体的条件计算逻辑:

 @Target({ ElementType.TYPE, ElementType.METHOD })
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Conditional(OnClassCondition.class)
 public @interface ConditionalOnClass {
     Class<?>[] value() default {};
     String[] name() default {};
 }

了解了条件注解后,再来看看它们是如何与配置类结合使用的。以后续章节中会用到的 JdbcTemplateAutoConfiguration 为例,它的完整类定义代码如下所示:

  @Configuration(proxyBeanMethods = false)
  @ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
  @ConditionalOnSingleCandidate(DataSource.class)
  @AutoConfigureAfter(DataSourceAutoConfiguration.class)
  @EnableConfigurationProperties(JdbcProperties.class)
  @Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
  public class JdbcTemplateAutoConfiguration {}

可以看到这个配置类的生效条件是存在 DataSourceJdbcTemplate 类,且在上下文中只能有一个 DataSource。此外,这个自动配置需要在 DataSourceAutoConfiguration 之后再配置(可以用 @AutoConfigureBefore@AutoConfigureAfter@AutoConfigureOrder 来控制自动配置的顺序)。这个配置类还会同时导入 JdbcTemplateConfigurationNamedParameterJdbcTemplateConfiguration 里的配置。

茶歇时间:通过 ImportSelector 选择性导入配置

普通的配置类需要被扫描到才能生效,可是自动配置类并不在我们项目的扫描路径中,它们又是怎么被加载上来的呢?

秘密在于 @EnableAutoConfiguration 上的 @Import(AutoConfigurationImportSelector.class),其中的 AutoConfigurationImportSelector 类是 ImportSelector 的实现,这个接口的作用就是根据特定条件决定可以导入哪些配置类,接口中的 selectImports() 方法返回的就是可以导入的配置类名。

AutoConfigurationImportSelector 通过 SpringFactoriesLoader 来加载 /META-INF/spring.factories 里配置的自动配置类列表,所用的键是 org.springframework.boot.autoconfigure.EnableAutoConfiguration,值是以逗号分隔的自动配置类全限定类名(包含了完整包名与类名)清单。

所以,只要在我们的类上增加 @SpringBootApplication 或者 @EnableAutoConfiguration 后,Spring Boot 就会自动替我们加载所有的自动配置类。

自动配置固然帮我们做了很多事,降低了配置的复杂度,但总有些情况我们会想要强制禁用某些自动配置,这时就需要做以下处理:

  • 在配置文件中使用 spring.autoconfigure.exclude 配置项,它的值是要排除的自动配置类的全限定类名;
  • @SpringBootApplication 注解中添加 exclude 配置,它的值是要排除的自动配置类。

4.3.2 配置项加载机制详解

如果自动配置的东西不满足我们的需要,我们可以自己动手进行配置,但在动手之前,可以先了解下 Spring Boot 的自动配置是否有给我们留下什么“开关参数”,用来定制配置内容。以 AOP 的配置为例,它就可以通过 spring.aop.proxy-target-class 属性来做微调:

    @Configuration(proxyBeanMethods = false)
    @EnableAspectJAutoProxy(proxyTargetClass = false)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
                           matchIfMissing = false)
    static class JdkDynamicAutoProxyConfiguration {}

    @Configuration(proxyBeanMethods = false)
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
                           matchIfMissing = true)
    static class CglibAutoProxyConfiguration {}

在 2.3.1 节中,我们了解过了 Spring Framework 的 PropertySource 抽象机制,Spring Boot 将它又向前推进了一大步。

  1. Spring Boot 的属性加载优先级

    Spring Boot 有 18 种方式来加载属性,且存在覆盖关系,本节根据优先级列出其中的一部分:

    (1) 测试类上的 @TestPropertySource 注解;

    (2) 测试类上的 @SpringBootTest 注解中的 properties 属性,还有些其他 @...Test 注解也有该属性;

    (3) 命令行参数(在 5.3 节中会讨论如何获取命令行参数);

    (4) java:comp/env 中的 JNDI 属性;

    (5) System.getProperties() 中获取到的系统属性;

    (6) 操作系统环境变量;

    (7) RandomValuePropertySource 提供的 random.* 属性(比如 $$$$);

    (8) 应用配置文件(有好几个地方可以配置,下面会详细说明);

    (9) 配置类上的 @PropertySource 注解。

    如果存在同名的属性,越往前的位置优先级越高,例如 my.prop 出现在命令行上,又出现在配置文件里,那最终会使用命令行里的值。

  2. Spring Boot 的配置文件

    Spring Boot 还为我们提供了一套配置文件,默认以 application 作为主文件名,支持 Properties 格式(文件以 .properties 结尾)和 YAML 格式(文件以 .yml 结尾)。Spring Boot 会按如下优先级加载属性(以 .properties 文件为例, .yml 文件的顺序是一样的):

    (1) 打包后的 Jar 包以外的 application-.properties

    (2) 打包后的 Jar 包以外的 application.properties

    (3) Jar 包内部的 application-.properties

    (4) Jar 包内部的 application.properties

    可以看到 Jar 包外部的文件比内部的优先级高,特定 Profile 的文件比公共的文件优先级高。

    在 Spring Boot 2.4.0 之前,上述第 2 和第 3 个文件的优先级顺序是反的,所有 application-.properties 文件的顺序都要高于 application.properties,无论是否在 Jar 包外。从 2.4.0 开始,调整为 Jar 包外部的文件优先级更高。可以设置 spring.config.use-legacy-processing=true 来开启兼容逻辑,Spring Boot 3.0 里会移除这个开关。

    Spring Boot 会在如下几处位置寻找 application.properties 文件,并将其中的内容添加到 Spring 的 Environment 中:

    • 当前目录的 /config 子目录;
    • 当前目录;
    • CLASSPATH 中的 /config 目录;
    • CLASSPATH 根目录。

如果我们不想用 application 来做主文件名,可以通过 spring.config.name 来改变默认值。通过下面的方式可以将 application.properties 改为 spring.properties

▸ java -jar foo.jar --spring.config.name=spring

还可以通过 spring.config.location 来修改查找配置文件的路径,默认是下面这样的,用逗号分隔,越靠后的优先级越高:

classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/

如果同时存在 .propertries 文件和 .yml 文件,那么后者中配置的属性优先级更高。

  1. 类型安全的配置属性

    通常,我们会在类中用 @Value("${}") 注解来访问属性,或者在 XML 文件中使用 ${} 占位符。在配置中,可能会有大量的属性需要一一对应到类的成员变量上,Spring Boot 提供了一种结构化且类型安全的方式来处理配置属性(configuration properties)——使用 @ConfigurationProperties 注解。

    下面的代码是 DataSourceProperties 类的一部分,这是一个典型的配置属性类(当然,它也是一个 POJO),Spring Boot 会把环境中以 spring.datasource 打头的属性都绑定到类的成员变量上,并且完成对应的类型转换。例如, spring.datasource.url 就会绑定到 url 上。

        @ConfigurationProperties(prefix = "spring.datasource")
        public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
            private ClassLoader classLoader;
            private String name;
            private boolean generateUniqueName = true;
            private Class<? extends DataSource> type;
            private String driverClassName;
            private String url;
            // 以下省略
        }
    

    如果为类加上 @ConstructorBinding 注解,还可以通过构造方法完成绑定,不过这种做法相对而言并不常用。

    ConfigurationPropertiesAutoConfiguration 自动配置类添加了 @EnableConfigurationProperties 注解,开启了对 @ConfigurationProperties 的支持。我们可以通过添加 @EnableConfigurationProperties (DataSourceProperties.class) 注解这样的方式将绑定后的 DataSourceProperties 注册为 Bean,此时的 Bean 名称为“属性前缀 - 配置类的全限定类名”,例如 spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;也可以直接用 @Component 注解或其他标准 Bean 配置方式将其注册为 Bean,以供其他 Bean 注入使用。

    除了添加到类上, @ConfigurationProperties 注解也可以被加到带有 @Bean 注解的方法上,这样就能为方法返回的 Bean 对象绑定上下文中的属性了。

    Spring Boot 在绑定属性时非常灵活,几乎可以说怎么写都能绑上,它一共支持四种属性命名形式:

    • 短横线分隔,推荐的写法,比如 spring.datasource.driver-class-name
    • 驼峰式,比如 spring.datasource.driverClassName
    • 下划线分隔,比如 spring.datasource.driver_class_name
    • 全大写且用下划线分隔,比如 SPRING_DATASOURCE_DRIVERCLASSNAME

前三种形式多用于 .properties 文件、 .yml 文件和 Java 系统属性的配置方式,第四种则更多出现在系统的环境变量中。而 @ConfigurationProperties 中的 prefix 属性只能使用第一种形式。

4.4 编写我们自己的自动配置与起步依赖

既然 Spring Boot 为我们提供了这么灵活强大的自动配置与起步依赖功能,那我们是否也可以参考其实现原理,实现专属于自己的自动配置与起步依赖呢?答案是肯定的。不仅如此,我们还可以对实现稍作修改,让它适用于非 Spring Boot 环境,甚至是低版本的 Spring Framework 环境。

4.4.1 编写自己的自动配置

根据 4.3.1 节的描述,我们很容易就能想到,要编写自己的自动配置,只需要以下三个步骤:

(1) 编写常规的配置类;

(2) 为配置类增加生效条件与顺序;

(3) 在 /META-INF/spring.factories 文件中添加自动配置类。

从第 4 章的例子开始,我们将正式开始开发二进制奶茶店的代码。作为贯穿专栏的案例,它几乎会串联起所有的重要知识点,方便大家理解并加深印象。

需求描述 二进制奶茶店新店开张,有很多准备工作要做,因此在尚未做好对外营业的准备时,不能开门迎客。现在,我们需要将具体的准备情况和每天的营业时间信息找个地方统一管理起来,以便合理安排门店的营业工作。

在 Spring Initializr 中,选择新建一个 Spring Boot 2.6.3 版本的 Maven 工程,具体信息如表 4-3 所示。点击生成按钮后,就能获得一个 binarytea.zip 压缩文件,这个文件被解压后即是原始工程。我们将这个工程放在 ch4/binarytea 目录中。

表 4-3 BinaryTea 的项目信息

条目内容
项目Maven Project
语言Java
Spring Boot 版本2.6.3
Grouplearning.spring
Artifactbinarytea
名称BinaryTea
Java 包名learning.spring.binarytea
打包方式Jar
Java 版本11
  1. 编写配置类并指定条件

    考虑到在 Spring Boot 项目里可以很方便地从配置文件里加载配置,我们可以把具体的准备情况和每天的营业时间都放在配置文件里,通过对应配置项来控制程序的运行逻辑。

    编写一个简单的 ShopConfiguration 类,上面增加了 @Cofiguration 注解,表示这是一个配置类。这个配置类生效的条件是 binarytea.ready 属性的值为 true,也就是店铺已经准备就绪了,除此之外的值或者不存在该属性时 ShopConfiguration 都不会生效。 ShopConfiguration 类的代码大致如代码示例 4-2 所示。

    代码示例 4-2 ShopConfiguration 自动配置类定义

     package learning.spring.config;
    
     // 省略import部分
    
     @Configuration
     @EnableConfigurationProperties(BinaryTeaProperties.class)
     @ConditionalOnProperty(name = "binarytea.ready", havingValue = "true")
     public class ShopConfiguration {
     }
    

    它的作用是创建一个 BinaryTeaProperties 的 Bean,并将就绪状态和营业时间绑定到 Bean 的成员上。 BinaryTeaProperties 的代码大致如代码示例 4-3 所示。

    代码示例 4-3 BinaryTeaProperties 代码片段

     @ConfigurationProperties("binarytea")
     public class BinaryTeaProperties {
         private boolean ready;
         private String openHours;
         // 省略Getter和Setter
     }
    

    binarytea 打头的属性值会被绑定到 BinaryTeaPropertiesreadyopenHours 成员上。例如, application.properties 文件包含如下内容,它们就会被绑定上去:

        binarytea.ready=true
        binarytea.open-hours=8:30-22:00
    
  2. 配置 spring.factories 文件

    为了让 Spring Boot 能找到我们写的这个配置类,我们需要在工程的 src/resources 目录中创建 META-INF/spring.factories 文件,其内容如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=learning.spring.config.ShopConfiguration
    

    由于工程生成的包名是 learning.spring.binarytea,所以默认会扫描这个包下的类。出于演示的目的,我们不希望 Spring Boot 工程自动扫描到 ShopConfiguration 类,所以特意将它放在 learning.spring.config 包中。Spring Boot 的自动配置机制会通过 spring.factories 文件里的配置,找到我们的 ShopConfiguration 类。

  3. 测试

    要测试我们的自动配置是否生效,只要看 Spring 上下文中是否存在 BinaryTeaProperties 类型的 Bean。 @SpringBootTest 注解提供了基本的 Spring Boot 工程测试能力, classes 属性的值是该测试类依赖的配置类, properties 属性中以键值对的形式提供了属性配置,代替了在测试文件夹中提供的 application.properties,我们还可以根据测试需要调整属性值。

    如果店铺已经准备好开门营业了,规定每天的营业时间是早 8 点 30 分至晚 10 点,检查整个自动配置功能是否符合预期的测试代码应该如代码示例 4-4 所示。

    代码示例 4-4 ShopConfigurationEnableTest 测试类

    
        package learning.spring.config;
    
        // 省略import部分
    
        @SpringBootTest(classes = BinaryTeaApplication.class, properties = {
             "binarytea.ready=true",
             "binarytea.open-hours=8:30-22:00"
        })
        public class ShopConfigurationEnableTest {
            @Autowired
            private ApplicationContext applicationContext;
    
            @Test
            void testPropertiesBeanAvailable() {
                assertNotNull(applicationContext.getBean(BinaryTeaProperties.class));
                assertTrue(applicationContext
                        .containsBean("binarytea-learning.spring.binarytea.BinaryTeaProperties"));
            }
    
            @Test
            void testPropertyValues() {
                BinaryTeaProperties properties = applicationContext.getBean(BinaryTeaProperties.class);
                assertTrue(properties.isReady());
                assertEquals("8:30-22:00", properties.getOpenHours());
            }
        }
    

    其中, testPropertiesBeanAvailable() 方法的作用是检查 Spring 上下文中是否存在 BinaryTeaProperties 类型的 Bean,Bean 的名字是否如 4.3.2 节所描述的那样; testPropertyValues() 方法的作用是检查属性内容是否被正确绑定到成员变量中。

    如果店铺还没准备好,那么自动配置类不应该生效。我们可以通过 ShopConfigurationDisableTest 类来测试,其中会检查 binarytea.ready 属性值,并确认上下文中不存在 BinaryTeaProperties 类型的 Bean,具体代码如代码示例 4-5 所示。

    代码示例 4-5 ShopConfigurationDisableTest 测试类代码片段

        @SpringBootTest(classes = BinaryTeaApplication.class, properties = {
            "binarytea.ready=false"
        })
        public class ShopConfigurationDisableTest {
            @Autowired
            private ApplicationContext applicationContext;
    
            @Test
            void testPropertiesBeanUnavailable() {
                assertEquals("false", applicationContext.getEnvironment().getProperty("binarytea.ready"));
                assertFalse(applicationContext.containsBean("binarytea-learning.spring.binarytea.BinaryTeaProperties"));
            }
        }
    

    算上生成工程时自动生成的 BinaryTeaApplicationTests 类中的 contextLoads() 测试方法,通过 mvn test 命令执行测试后,如果测试全部成功,我们可以看到类似下面的结果:

    [INFO] Results:
    [INFO]
    [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    

4.4.2 脱离 Spring Boot 实现自动配置

上一节中,我们依赖 Spring Boot 提供的一些能力,实现了一个自动配置类。可是,如果没有 Spring Boot,又该怎么办?甚至出于某些原因,我们要在 Spring Framework 4. x 或者更低的版本上也做些自动配置,又该怎么办呢?

这里需要解决两个问题:

  • 如何找到配置类;
  • 如何实现配置类上的条件。

第一个问题相对容易解决,只需要根据当前工程的情况进行调整,让我们的配置类位于工程会扫描的包里(比如 @ComponentScan 配置的扫描目标里),或者把我们的配置类追加到工程的扫描范围里。第二个问题则要复杂一些,如果我们用的是 4. x 版本的 Spring Framework,那么它本身就有 @Conditional 注解,我们完全可以按照 Spring Boot 中那些条件注解的实现,按需复制过来;如果用的是 3. x 版本的 Spring Framework,就只能通过自定义 BeanFactoryPostProcessor,根据一定的条件,再决定是否用编程的方式注册 Bean。

假设我们根据配置来决定 HelloWorld 程序输出的语言种类,如代码示例 4-6,在 learning.spring.speaker 包中定义接口与类。

代码示例 4-6 Speaker 接口及其实现代码片段

 public interface Speaker {
     String speak();
 }

 public class ChineseSpeaker implements Speaker {
     @Override
     public String speak() {
         return "你好,我爱Spring。";
     }
 }
 public class EnglishSpeaker implements Speaker {
     @Override
     public String speak() {
         return "Hello, I love Spring.";
     }
 }
  1. 定义配置类并实现条件判断

    如果工程的 Spring Bean 扫描路径是 learning.spring.helloworld,那就在这个包下放一个配置类,具体如代码示例 4-7 所示,其中创建了 SpeakerBeanFactoryPostProcessor,同时也让容器加载了 application.properties 文件 。

    代码示例 4-7 用于添加 BeanFactoryPostProcessor 的配置类代码片段

        @Configuration
        @PropertySource("classpath:/application.properties")
        public class AutoConfiguration {
            @Bean
            public static SpeakerBeanFactoryPostProcessor speakerBeanFactoryPostProcessor() {
                return new SpeakerBeanFactoryPostProcessor();
            }
        }
    

    SpeakerBeanFactoryPostProcessor 的工作比较多,有如下几种。

    • 根据 spring.speaker.enable 开关确定是否自动配置 speaker Bean。
    • 根据 spring.speaker.language 动态确定使用哪个 Speaker 接口的实现。
    • 将确定的 Speaker 实现注册到 Spring 上下文中。

具体获取属性进行判断和注册的代码如代码示例 4-8 所示。

代码示例 4-8 SpeakerBeanFactoryPostProcessor 处理逻辑

    public class SpeakerBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
        private static final Log log = LogFactory.getLog(SpeakerBeanFactoryPostProcessor.class);
        // 为了获得配置属性,注入Environment
        private Environment environment;

        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            // 获取属性值
            String enable = environment.getProperty("spring.speaker.enable");
            String language = environment.getProperty("spring.speaker.language", "English");
            String clazz = "learning.spring.speaker." + language + "Speaker";

            // 开关为true则注册Bean,否则结束
            if (!"true".equalsIgnoreCase(enable)) {
                return;
            }
            // 如果目标类不存在,结束处理
            if (!ClassUtils.isPresent(clazz, SpeakerBeanFactoryPostProcessor.class.getClassLoader())) {
                return;
            }

            if (beanFactory instanceof BeanDefinitionRegistry) {
                registerBeanDefinition((BeanDefinitionRegistry) beanFactory, clazz);
            } else {
                registerBean(beanFactory, clazz);
            }
        }
            // 省略其他代码
    }

代码示例 4-8 的 postProcessBeanFactory() 方法最后,根据 BeanFactory 的类型选择了不同的 Bean 注册方式,实际情况中会更倾向于使用 BeanDefinitionRegistry 来注册 Bean 定义。两种不同的注册方法如代码示例 4-9 所示。

代码示例 4-9 SpeakerBeanFactoryPostProcessor 中的 Bean 注册逻辑

    public class SpeakerBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
        // 如果是BeanDefinitionRegistry,可以注册BeanDefinition
        private void registerBeanDefinition(BeanDefinitionRegistry beanFactory, String clazz) {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClassName(clazz);
            beanFactory.registerBeanDefinition("speaker", beanDefinition);
        }

        // 如果只能识别成ConfigurableListableBeanFactory,直接注册一个Bean实例
        private void registerBean(ConfigurableListableBeanFactory beanFactory, String clazz) {
            try {
                Speaker speaker = (Speaker) ClassUtils.forName(clazz, SpeakerBeanFactoryPostProcessor.class.
                                      getClassLoader()).getDeclaredConstructor().newInstance();
                beanFactory.registerSingleton("speaker", speaker);
            } catch (Exception e) {
                log.error("Can not create Speaker.", e);
            }
        }
            // 省略其他代码
    }
  1. 运行与测试

    为了简化演示工程,假设 learning.spring.helloworld 中是需要运行的代码,我们可以直接在 main() 方法中创建 Spring 上下文,具体如代码示例 4-10 所示。

    代码示例 4-10 Application 类定义

        @Configuration
        @ComponentScan("learning.spring.helloworld")
            public class Application {
            public static void main(String[] args) {
                AnnotationConfigApplicationContext applicationContext =
                        new AnnotationConfigApplicationContext(Application.class);
                Speaker speaker = applicationContext.getBean("speaker", Speaker.class);
                System.out.println(speaker.speak());
            }
        }
    

    配套的 application.properties 文件仅包含两个配置,具体如下:

        spring.speaker.enable=true
        spring.speaker.language=Chinese
    

    程序的运行效果就是输出一句中文:

        你好,我爱Spring。
    

    如果将 spring.speaker.language 删除,或者改为 English,输出则为英文:

       Hello, I love Spring.
    

    与之前一样,我们也提供了基本的测试代码。由于配置内容不同,不同的组合需要我们编写不同的测试类,比如,和上述的运行一样,要求正常输出中文,可以使用代码示例 4-11 的测试代码。

    代码示例 4-11 ChineseAutoConfigurationTest 中文测试类

       @SpringJUnitConfig(AutoConfiguration.class)
       @TestPropertySource(properties = {
           "spring.speaker.enable=true",
           "spring.speaker.language=Chinese"
       })
       public class ChineseAutoConfigurationTest {
           @Autowired
           private ApplicationContext applicationContext;
    
           @Test
           void testHasChineseSpeaker() {
               assertTrue(applicationContext.containsBean("speaker"));
               Speaker speaker = applicationContext.getBean("speaker", Speaker.class);
               assertEquals(ChineseSpeaker.class, speaker.getClass());
           }
       }
    

    @SpringJUnitConfig 注解是 Spring 的测试框架提供的组合注解,可以代替之前看到过的 @ExtendWith(SpringExtension.class),同时配置一些 Spring 相关的内容。 @TestPropertySource 注解可以指定属性文件的位置,也可以直接提供属性键值对。

    如果要关闭开关,则可以像代码示例 4-12 那样。

    代码示例 4-12 DisableAutoConfigurationTest 关闭开关的测试

       @SpringJUnitConfig(AutoConfiguration.class)
       @TestPropertySource(properties = {"spring.speaker.enable=false"})
       public class DisableAutoConfigurationTest {
           @Autowired
           private ApplicationContext applicationContext;
    
           @Test
           void testHasNoSpeaker() {
               assertFalse(applicationContext.containsBean("speaker"));
           }
       }
    

    如果 spring.speaker.language 的值我们不支持,那只需要调整 @TestPropertySource 中提供的属性值,其他测试代码和断言逻辑与 DisableAutoConfigurationTest 完全一样,具体如代码示例 4-13 所示。

    代码示例 4-13 WrongAutoConfigurationTest 错误语言的测试

       @SpringJUnitConfig(AutoConfiguration.class)
       @TestPropertySource(properties = {
           "spring.speaker.enable=true",
           "spring.speaker.language=Japanese"
       })
       public class WrongAutoConfigurationTest {
           // 具体代码省略,同DisableAutoConfigurationTest
       }
    

    在 IDEA 中运行所有这些测试类的效果如图 4-1 所示。

    image.png
    图 4-1 IDEA 中的测试运行效果

4.4.3 编写自己的起步依赖

在通常情况下,起步依赖主要由两部分内容组成:

(1) 所需要管理的依赖项;

(2) 对应功能的自动配置。

  1. 依赖项该如何管理

    如 4.2.2 节所述,Spring Boot 的起步依赖本身就是一个 Maven 模块,所以将要管理的依赖项直接放在它的 pom.xml 中即可,即放在 <dependencies/> 中。对于自动配置,我们一般都会单独编写一个模块,把相关自动配置类和 spring.factories 等文件放在一起。前者就是起步依赖自身,即 starter 模块,后者就是 autoconfigure 模块。

    由于 Spring Boot 官方的起步依赖都使用 spring-boot 开头,因而我们的自定义起步依赖需要 避免 使用这个前缀。大家可以根据要实现的功能,或者要引入的内容来设计前缀,后缀使用 -spring-boot-starter,例如 binarytea-spring-boot-starter

  2. 自动配置模块该如何定义

    autoconfigure 模块中一般还会包含所需的属性配置,通常是带有 @ConfigurationProperties 的类,这些属性的前缀最好和 starter 中的保持一致,不要和 Spring Boot 内置的自动配置使用的 springserver 等前缀混在一起。而 autoconfigure 模块的命名后缀可以使用 -spring-boot-autoconfigure,例如 binarytea-spring-boot-autoconfigure

    在准备完自动配置模块 binarytea-spring-boot-autoconfigure 后,务必将它也放到 binaryteaspring-boot-starter<dependencies/> 中去,这样就能和其他起步依赖一样,在使用时只需引入 starter 模块就可以了。当然,如果两者的内容都十分简单,也可以将它们合并到一起,直接放到 starter 中。

    顺便再强调一下,起步依赖本身就是一个普通的 Maven 模块,因此无论是否用在 Spring Boot 工程里,它的实现和功能都不会有太大差异。

4.5 小结

本章我们了解了为何在 Spring Framework 已经成为事实行业标准的情况下,Spring 团队仍然孕育出了 Spring Boot 这么一个炙手可热的项目,大有“不用 Spring Boot 就不算开发 Spring 项目”的意思。我们也一同学习了 Spring Boot 中使用最广泛的两个功能——起步依赖与自动配置,了解了它们的基本使用和二者背后的实现原理。在此过程之中,对于 Spring Boot 是如何加载配置项的,它的加载位置与优先级顺序等一系列问题,我们都做了简单的说明。

章节的最后,大家一起动手解决了几个问题,即如何编写自己的起步依赖与自动配置,还把问题延展了一下:如果没有 Spring Boot 又该如何实现呢?

下一章,我们会进一步展开说明 Spring Boot 中与生产运行相关的功能,并学习 Spring Boot Actuator、监控与部署相关的一些话题。

二进制奶茶店项目开发小结

本章我们正式开始搭建二进制奶茶店的示例工程,主要做了两件事:

(1) 通过 Spring Initializr 初始化了二进制奶茶店的 BinaryTea 工程,完成了项目骨架的搭建;

(2) 编写了一个读取是否开门营业和营业时间配置的自动配置类。

这只是整个示例的“第一步”,后续章节中的演示都会基于本章的工程展开。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/714827.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【计算机组成原理】指令系统考研真题详解之拓展操作码!

计算机组成原理&#xff1a;指令系统概述与深入解析 1. 指令系统概述 计算机软硬件界面的概念 在计算机组成原理中&#xff0c;指令系统扮演着至关重要的角色&#xff0c;它是计算机软硬件界面的核心。软件通过指令与硬件进行通信&#xff0c;硬件根据指令执行相应的操作。指…

基于STM32和人工智能的智能气象站系统

目录 引言环境准备智能气象站系统基础代码实现&#xff1a;实现智能气象站系统 4.1 数据采集模块4.2 数据处理与分析4.3 控制系统4.4 用户界面与数据可视化应用场景&#xff1a;智能气象管理与优化问题解决方案与优化收尾与总结 1. 引言 随着气象科技的进步&#xff0c;智能…

定个小目标之刷LeetCode热题(21)

这是道技巧题&#xff0c;利用了 &#xff08;num - 1&#xff09;% n 计算下标的形式来将数组元素与数组索引产生映射关系&#xff0c;代码如下&#xff0c;可以看下注释 class Solution {public List<Integer> findDisappearedNumbers(int[] nums) {int n nums.lengt…

Qt画五角星,简单图表

五角星&#xff1a; 代码&#xff1a; widget.cpp #include "widget.h" #include "ui_widget.h" #include <QPaintEvent> #include <QPainter> #include <QPainterPath> Widget::Widget(QWidget *parent): QWidget(parent), ui(new U…

广东启动“粤企质量提升工作会议” 着力提升产品和服务质量

6月5日,由广东质量峰会组委会牵头,联合相关质量、信用、打假和检验检测等部门共同举办的“粤企质量提升工作会议”在广州正式启动。本次工作会议旨在贯彻落实《质量强国建设纲要》及《广东省质量强省建设纲要》精神,深入开展全民质量行动,弘扬企业家和工匠精神,营造政府重视质量…

互联网应用主流框架整合之SpringMVC基础组件开发

多种传参方式 在前一篇文章互联网应用主流框架整合之SpringMVC初始化及各组件工作原理中讨论了最简单的参数传递&#xff0c;而实际情况要复杂的多&#xff0c;比如REST风格&#xff0c;它往往会将参数写入请求路径中&#xff0c;而不是以HTTP请求参数传递&#xff1b;比如查询…

acwing 5575. 改变数值 | c++题解及解释

acwing 5575. 改变数值 题目 代码及解释 #include <iostream> #include <cstring> #include <algorithm> #include <unordered_map> using namespace std;const int N305; int a[N],b[N]; unordered_map<int,int>f[N]; const int INF1e9;int gc…

异或运算的原理以及应用

异或&#xff08;XOR&#xff09;是计算机科学和数字电路中常用的运算之一。异或运算符通常用符号“⊕”或“^”表示&#xff0c;它有着简单而独特的性质&#xff0c;使其在数据加密、错误检测与纠正等多个领域得到了广泛的应用。在网络上我们传输的每一比特数据都经过了异或运…

如何用 ChatGPT DALL-E3绘画(10个案例)

如何用ChatGPT绘画——10个案例&#xff08;附提示词&#xff09; DALL•E 3可以在ChatGPT plus里直接使用了。 如果想免费使用&#xff0c;可以用新必应免费使用。 上次有个朋友问&#xff1a;DALL•E 3 有什么用。 这里用十个案例&#xff0c;来解释一下这个问题。 1.创…

计算机缺失msvcr110.dll如何解决,这6种解决方法可有效解决

电脑已经成为我们生活和工作中不可或缺的工具&#xff0c;然而在使用电脑的过程中&#xff0c;我们常常会遇到一些问题&#xff0c;其中之一就是电脑找不到msvcr110.dll文件。这个问题可能会给我们带来一些困扰&#xff0c;但是只要我们了解其原因并采取相应的解决方法&#xf…

CSS 实现电影信息卡片

CSS 实现电影信息卡片 效果展示 CSS 知识点 CSS 综合知识运用 页面整体布局 <div class"card"><div class"poster"><img src"./poster.jpg" /></div><div class"details"><img src"./avtar…

【Liunx】基础开发工具的使用介绍-- yum / vim / gcc / gdb / make

前言 本章将介绍Linux环境基础开发工具的安装及使用&#xff0c;在Linux下安装软件&#xff0c;编写代码&#xff0c;调试代码等操作。 目录 1. yum 工具的使用1.1 什么是软件包&#xff1a;1.2 如何下载软件&#xff1a;1.3 配置国内yum源&#xff1a; 2. vim编辑器2.1 vim的安…

创建comfyui自定义节点

参考 https://github.com/liubai-liubai/ComfyUI-ImgSeg-LB/tree/main https://blog.styxhelix.life/?p33 安装 不需要安装任何其他依赖文件&#xff0c;只需要把0x_erthor_node文件夹复制到custom_nodes文件夹下&#xff0c;就能安装成功。 a1&#xff1a;展示了代码结构&…

数字化转型,不做是等死,做了是找死

“ 有不少人调侃说&#xff1a;数字化转型&#xff0c;不做是等死&#xff0c;做了是找死。如果你是一个老板&#xff0c;你会怎么选择呢&#xff0c;下面我来剖析一下。” 我按照“做正确的事&#xff0c;正确的做事”来分析数字化转型&#xff0c;再通过抓痛点和流程再造两项…

guli商城业务逻辑-基础篇笔记

这里写目录标题 0.1 viscode设置用户代码片段1.实现多级菜单接口1.1 对接前端菜单1.2 对接网关接口解决跨域问题&#xff0c;如果不解决跨域&#xff0c;浏览器还是访问不了api1.3 把商品服务添加网关1.4 修改前端显示分类菜单1.5 给菜单添加删除修改功能1.5.1 删除功能的后端业…

【猫狗分类】Pytorch VGG16 实现猫狗分类5-预测新图片

背景 好了&#xff0c;现在开尝试预测新的图片&#xff0c;并且让vgg16模型判断是狗还是猫吧。 声明&#xff1a;整个数据和代码来自于b站&#xff0c;链接&#xff1a;使用pytorch框架手把手教你利用VGG16网络编写猫狗分类程序_哔哩哔哩_bilibili 预测 1、导包 from to…

分数布朗运动FBM期权定价模型

BS定价模型和蒙特卡洛模拟期权定价方法都 假设标的资产价格的对数服从布朗运动 &#xff0e; 但是实际 的金融市场中标的资产价格运动过程具有 “尖峰厚尾 ” 现象 &#xff0c; 运用分数布朗运动 &#xff08;FBM &#xff09;来刻画标的资产 价格的运动过程可能更加合适。 …

HP惠普暗影精灵10 OMEN Gaming Laptop 16-wf1xxx原厂Win11系统镜像下载

惠普hp暗影精灵10笔记本电脑16-wf1000TX原装出厂Windows11&#xff0c;恢复开箱状态oem预装系统安装包&#xff0c;带恢复重置还原 适用型号:16-wf1xxx 16-wf1000TX,16-wf1023TX,16-wf1024TX,16-wf1025TX, 16-wf1026TX,16-wf1027TX,16-wf1028TX,16-wf1029TX, 16-wf1030TX,16-…

docker拉取镜像太慢解决方案

前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 创建daemon.json文件,输入以下信息 vim /etc/docker/daemon.json{"registry-mirrors": ["https://9cpn8tt6.mirror…

JAVA开发 选择多个文件,系统运行后自动生成ZIP压缩包

选择多个文件&#xff0c;系统运行后自动生成ZIP压缩包 实现方法1.1 代码块1.2 运行结果截取 相关知识 实现方法 案例简述&#xff1a;通过启动java代码来打开文件选择器对话框&#xff0c;用户选择确认需要进行压缩的文件&#xff0c;可一次性选择多个文件&#xff0c;选择完…
最新文章