工程配置

前面章节我们介绍过,SpringBoot的一个核心特性是约定优于配置,基于这一理念,SpringBoot内部包含了大量“约定”配置以减少开发者需要编写的样板式配置代码。然而,任何实际项目都离不开必要的配置,例如数据库连接信息、服务器端口、业务配置参数等。为此SpringBoot提供了一套强大且灵活的工程配置机制,允许我们将配置从代码中剥离到多个配置源,并灵活的实现配置在不同环境(开发、测试、生产)间的切换。

这篇笔记我们将详细讲解SpringBoot中配置文件的使用,包括配置文件的格式、配置值的绑定、多环境配置以及配置的加载优先级。

配置文件格式

SpringBoot支持两种主要的配置文件格式:application.propertiesapplication.yml(或application.yaml)。它们可以共存,但实际开发中建议选择其中一种,避免二者混用。

Properties配置

.properties文件是Java中传统的配置文件格式,它采用key=value的简单语法,配置文件中可通过.来表示层级关系。

Properties配置的语法特点:

  • 配置为键值对格式,key=value
  • 使用点号.来分隔层级,例如server.port=8080
  • 注释以#开头
  • 值部分表达复杂数据结构(如List、Map)需要依靠特定格式(如逗号分隔)和约定来解析

下面是一个Properties配置文件的例子。

# server
server.port=8080

# mysql datasource
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=abc123

# app configuration
app.name=My Spring Boot Application
app.feature-enabled=true
app.ip-whitelist=192.168.1.1,192.168.1.2,10.0.0.1

Properties配置格式其实有两个缺点,一个是表达复杂数据结构(如List、Map)时可读性较差,另一个是该类型仅允许ISO-8859-1编码,对于中文用户,如果你在配置文件中使用中文编写注释信息可能会被IDE自动转为Unicode转义序列,可读性较差。不过Properties配置仍以其简洁、紧凑的语法而广泛使用。

YAML配置

YAML是一种更易读的配置格式,特别适合用来表达层次结构化的配置信息。SpringBoot也支持使用YAML格式配置。

YAML配置的语法特点:

  • 严格使用缩进来表示层级关系,注意禁止使用Tab
  • 键值对使用冒号:加空格分隔,如 key: value,注意冒号后必须有一个空格
  • YAML原生支持多种基本数据结构,包括标量(字符串、数字、布尔)、数组和映射
  • 注释以#开头

下面是一个YAML配置文件的例子。

# 服务器配置
server:
  port: 8080

# MySQL数据源配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: abc123

# 应用配置
app:
  name: My Spring Boot Application
  feature-enabled: true
  ip-whitelist:
    - 192.168.1.1
    - 192.168.1.2
    - 10.0.0.1

可以看到,YAML语法层次清晰、可读性更好,适用于复杂配置,然而它的缺点是语法严格且格式冗长。Properties和YAML具体如何选择,我们还是需要结合实际情况考虑。

加载配置

SpringBoot中,加载配置文件中的有两种主要方式:使用@Value注解,或使用@ConfigurationProperties进行类型安全的绑定。@Value适用于注入单个、分散的配置值,在Spring Framework章节我们已经介绍过了;而@ConfigurationProperties则是将一组具有共同前缀的配置批量、结构化地绑定到一个Java Bean,对于复杂的结构化配置信息,后者是更推荐的方式,因为它提供了更好的类型安全和IDE支持。

使用@ConfigurationProperties

假设我们有如下配置。

app.name=My Spring Boot Application
app.feature-enabled=true
app.ip-whitelist=192.168.1.1,192.168.1.2,10.0.0.1

对应的配置属性类可以如下定义。

package com.gacfox.demo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private boolean featureEnabled;
    private List<String> ipWhitelist;
}

对于配置属性类,我们可以像校验普通的Bean一样,使用JSR-303/380 Bean Validation注解来校验配置属性的值,不过这里别忘了使用配置校验需要添加相关的起步依赖。

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

下面是一个带有配置数据校验的配置属性类。

package com.gacfox.demo.config;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import java.util.List;

@Data
@Component
@Validated
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    @NotBlank
    private String name;
    @NotNull
    private Boolean featureEnabled;
    @NotEmpty
    private List<String> ipWhitelist;
}

如果配置值不满足校验条件,应用将无法启动并给出明确的错误信息。

松散绑定

SpringBoot支持配置的松散绑定(Relaxed Binding),即配置文件中的属性名可以采用多种格式(kebab-casesnake_casecamelCase),SpringBoot都能将其匹配到对应的配置属性类字段上,这在实际开发中非常实用。对于大多数SpringBoot程序,配置文件中通常都采用kebab-case,而Java类中使用camelCase,这种写法符合绝大多数Java开发者的习惯,这里也建议遵循这个约定。

Profile多环境配置

在实际开发中,应用通常需要在开发、测试、生产等不同环境中运行,每个环境的配置(如数据库地址、外部接口地址、日志级别等)可能截然不同。SpringBoot对配置的环境隔离提供了内置的支持。

环境配置文件

SpringBoot中,我们可以创建名为application-{profile}.properties(或.yml)的配置文件,例如:

  • application.properties:主配置文件
  • application-dev.properties:用于开发环境
  • application-test.properties:用于测试环境
  • application-prod.properties:用于生产环境

在主配置文件application.properties中我们可以设置默认激活的Profile,此外在对应环境上,我们还可以使用命令行参数--spring.profiles.active设置当前需要激活的Profile,后者拥有更高的优先级。加载时,环境特定的配置文件中的配置会覆盖主配置文件中的同名配置,并且只在该Profile激活时生效。下面是一个多环境配置例子,该组配置中默认激活dev环境。

application.properties

spring.profiles.active=dev
app.greeting=Hello from default config!

application-dev.properties

app.greeting=Hello from Development!
app.database-url=jdbc:h2:mem:devdb
app.log-level=debug

application-prod.properties

app.greeting=Welcome to Production!
app.database-url=jdbc:mysql://prod-db-host:3306/proddb
app.log-level=info

在生产环境服务器上,我们可以配置启动命令添加参数--spring.profiles.active=prod来激活生产配置。

激活Profile

实际上,除了配置文件中的默认激活Profile和命令行参数--spring.profiles.active,我们还有多种方式可以激活Profile,且Profile可以同时激活多个,优先级从高到低分别如下:

  1. 命令行参数java -jar myapp.jar --spring.profiles.active=prod,feature-a
  2. JVM系统属性-Dspring.profiles.active=prod
  3. 环境变量SPRING_PROFILES_ACTIVE=prod
  4. 配置文件内指定:在配置文件中设置spring.profiles.active,常用于配置默认激活dev环境

Profile分组

SpringBoot还支持Profile分组功能,我们可以将多个Profile定义为一组,激活该组时等同于激活组内所有Profile。

spring.profiles.group.production=proddb,prodmetrics
spring.profiles.group.local=dev,h2db,debuglog

如上配置后,例如设置--spring.profiles.active=production即可激活整个组。

在代码中使用Profile判断

我们可以使用@Profile注解来条件化地注册Bean或配置类,下面是一个例子。

@Configuration
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return DataSourceBuilder.create().build();
    }
}

在代码中读取当前激活的Profile

这里要尤其注意,虽然Profile可通过spring.profiles.active配置,但不要使用@Value("${spring.profiles.active}")来注入这个属性,这个属性在应用启动早期可能不可用或为空,会导致注入null或默认值。实际开发中,建议基于Environment来获取当前激活的Profile。

package com.gacfox.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class DemoComponent {
    private final Environment environment;

    @Autowired
    public DemoComponent(Environment environment) {
        this.environment = environment;
    }

    public void printProperties() {
        String[] activeProfiles = environment.getActiveProfiles();
        log.info("Active Profiles: {}", String.join(", ", activeProfiles));
    }
}

指定外部配置文件

除了之前提到的几个配置源,SpringBoot还可以在启动参数中使用spring.config.additional-location追加指定外部配置文件路径,它具有更高的优先级,下面是一个例子。

java -jar app.jar --spring.config.additional-location=/home/ubuntu/configmap.properties
  • --spring.config.additional-location:指定配置文件路径,支持可以指定多个(使用逗号分隔),后指定的相同配置键会覆盖前面的

@SpringBootApplication注解分析

前面我们多次提到过,SpringBoot的核心特性之一是自动配置。在手动配置Spring的时代,我们编写了大段XML,而到了SpringBoot时代,配置则相当简化了。那么SpringBoot是如何实现自动配置的?这里我们逐步分析。

SpringBoot的启动类上标注了@SpringBootApplication注解,我们查看其源码,其实它是一个复合的注解,其中主要包括@SpringBootConfiguration@EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // ...
}

我们可以发现,@SpringBootConfiguration其实和@Configuration的作用类似,都是用来标注配置类的。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    // ...
}

SpringBootApplication注解上的@EnableAutoConfiguration也是一个复合注解,包括@AutoConfigurationPackage@Import

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    // ...
}

AutoConfigurationImportSelector用于扫描加载SpringBoot内置的配置类。我们可以在spring-boot-autoconfigure包中的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中找到自动配置类的列表。

@AutoConfigurationPackage中也包含了@Import注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {
    // ...
}

AutoConfigurationPackages.Registrar用于扫描启动类包的子包中的组件注册到Spring容器。

由上述分析总结来看,我们可以得出结论,SpringBoot框架默认会从启动类和spring-boot-autoconfigure定义的自动配置类注册Spring配置,此外还会扫描启动类包的子包中的组件。这里再额外补充一点,如果我们定义的SpringBean不在启动类包的子包下,就需要手动指定扫描包,SpringBoot提供了@ComponentScan注解,我们可以将其标注到启动类上并配置相关的包名。

@SpringBootApplication
@ComponentScan({"com.gacfox.bootdemo"})
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

spring-boot-autoconfigure包中包含了大量自动配置,我们具体开发如果用到相关功能,推荐查看一下其源码,简要了解其中都配置了哪些内容。

覆盖配置JavaConfig

SpringBoot中,有时仅仅覆盖application.properties中的配置项在某些极特殊情况下可能还是无法满足我们的需求,此时我们需要彻底重写某个Bean的JavaConfig配置代码。实际上,spring-boot-autoconfigure包中的自动配置类大都允许我们这么做。这里我们简单分析一下,我们可以打开任意一个spring-boot-autoconfigure包中的自动配置类,下面是一个例子。

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(RedisConnectionDetails.class)
    PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties,
            ObjectProvider<SslBundles> sslBundles) {
        return new PropertiesRedisConnectionDetails(properties, sslBundles.getIfAvailable());
    }

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}

我们看到JavaConfig装配的Bean标注了@ConditionalOnMissingBean注解,该注解表示如果我们自定义了配置Bean,那么就使用我们自定义的,不会再使用内置的配置类生成Bean了;如果没有自定义的,才使用该配置对象。了解了这些,我们想覆盖默认的JavaConfig也就比较简单了,我们按照自动配置类定义的Bean,注册我们自己的JavaConfig配置类即可。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。