20-Spring Boot 配置体系详解

2 阅读5分钟

Spring Boot 配置体系详解

一、知识概述

Spring Boot 提供了灵活而强大的配置体系,支持多种配置方式和配置源。开发者可以根据不同环境和需求,灵活管理应用配置。配置体系的核心包括:

  • 配置文件格式:Properties、YAML
  • 多环境配置:Profile 机制
  • 配置属性绑定:类型安全的配置
  • 配置加载顺序:优先级规则
  • 外部化配置:配置中心、环境变量

理解 Spring Boot 配置体系,是管理应用配置的关键。

二、知识点详细讲解

2.1 配置文件格式

Properties 格式
app.name=MyApp
app.port=8080
app.security.enabled=true
app.database.url=jdbc:mysql://localhost:3306/mydb
YAML 格式
app:
  name: MyApp
  port: 8080
  security:
    enabled: true
  database:
    url: jdbc:mysql://localhost:3306/mydb
YAML 优势
  1. 层次结构清晰
  2. 支持复杂对象
  3. 支持列表和数组
  4. 可读性更强

2.2 配置文件位置与加载顺序

Spring Boot 按以下顺序加载配置文件(后者覆盖前者):

1. jar 包外部的 application.properties/yml
2. jar 包内部的 application.properties/yml
3. @Configuration 注解类上的 @PropertySource
4. 默认属性(SpringApplication.setDefaultProperties)

具体目录:

当前目录的 /config 子目录
当前目录
classpath 的 /config 包
classpath 根目录

2.3 Profile 多环境配置

Profile 文件命名
application.yml           # 默认配置
application-dev.yml       # 开发环境
application-test.yml      # 测试环境
application-prod.yml      # 生产环境
激活 Profile
# application.yml
spring:
  profiles:
    active: dev

或命令行:

java -jar app.jar --spring.profiles.active=prod

2.4 配置属性绑定

@Value 注入
@Value("${app.name}")
private String appName;
@ConfigurationProperties 绑定
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private int port;
    // getter/setter
}

2.5 配置加载优先级

从高到低:

  1. 命令行参数
  2. JNDI 属性
  3. Java 系统属性
  4. 环境变量
  5. jar 外部配置文件
  6. jar 内部配置文件
  7. @PropertySource
  8. 默认属性

2.6 配置属性松散绑定

Spring Boot 支持多种命名格式:

app:
  user-name: John    # 驼峰形式
  user_name: John    # 下划线形式
  user-name: John    # 短横线形式
  USERNAME: John     # 大写形式

对应 Java 属性:

private String userName;  // 以上四种格式都可以绑定

三、代码示例

3.1 基础配置示例

# application.yml - 主配置文件

# 应用配置
spring:
  application:
    name: my-application
  
  # 环境
  profiles:
    active: dev
  
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  
  # JPA 配置
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  
  # Redis 配置
  data:
    redis:
      host: localhost
      port: 6379
      password: 
      database: 0
      timeout: 3000ms
  
  # 缓存配置
  cache:
    type: redis
    redis:
      time-to-live: 3600000
  
  # Web 配置
  web:
    resources:
      cache:
        period: 3600
  
  # 文件上传配置
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 100MB
  
  # JSON 配置
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /api
  tomcat:
    max-threads: 200
    accept-count: 100

# 日志配置
logging:
  level:
    root: INFO
    com.example: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/application.log

# 自定义配置
app:
  name: My Application
  version: 1.0.0
  security:
    enabled: true
    secret-key: my-secret-key
    token-validity: 3600
  features:
    - feature1
    - feature2
    - feature3

3.2 多环境配置示例

# application.yml - 公共配置
spring:
  application:
    name: my-application

# 公共配置
app:
  name: ${spring.application.name}
  default-timeout: 30000

---
# application-dev.yml - 开发环境
spring:
  config:
    activate:
      on-profile: dev
  
  datasource:
    url: jdbc:mysql://localhost:3306/mydb_dev
    username: dev
    password: dev123
  
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  
server:
  port: 8080

logging:
  level:
    com.example: DEBUG

app:
  debug: true
  cache:
    enabled: false

---
# application-test.yml - 测试环境
spring:
  config:
    activate:
      on-profile: test
  
  datasource:
    url: jdbc:mysql://test-server:3306/mydb_test
    username: test
    password: test123
  
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

server:
  port: 8081

logging:
  level:
    com.example: INFO

app:
  debug: false
  cache:
    enabled: true

---
# application-prod.yml - 生产环境
spring:
  config:
    activate:
      on-profile: prod
  
  datasource:
    url: jdbc:mysql://prod-server:3306/mydb_prod
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: none

server:
  port: 80

logging:
  level:
    root: WARN
    com.example: INFO

app:
  debug: false
  cache:
    enabled: true
    ttl: 7200

3.3 属性绑定示例

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;
import java.util.*;

// 简单属性绑定
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    private String name;
    private String version;
    private int defaultTimeout = 30000;
    private boolean debug = false;
    
    private final Security security = new Security();
    private final Cache cache = new Cache();
    private final Features features = new Features();
    
    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getVersion() { return version; }
    public void setVersion(String version) { this.version = version; }
    public int getDefaultTimeout() { return defaultTimeout; }
    public void setDefaultTimeout(int defaultTimeout) { this.defaultTimeout = defaultTimeout; }
    public boolean isDebug() { return debug; }
    public void setDebug(boolean debug) { this.debug = debug; }
    public Security getSecurity() { return security; }
    public Cache getCache() { return cache; }
    public Features getFeatures() { return features; }
    
    // 安全配置
    public static class Security {
        private boolean enabled = true;
        
        @NotBlank
        private String secretKey;
        
        @Min(60)
        private long tokenValidity = 3600;
        
        private final Jwt jwt = new Jwt();
        
        // getter/setter
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
        public String getSecretKey() { return secretKey; }
        public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
        public long getTokenValidity() { return tokenValidity; }
        public void setTokenValidity(long tokenValidity) { this.tokenValidity = tokenValidity; }
        public Jwt getJwt() { return jwt; }
        
        public static class Jwt {
            private String header = "Authorization";
            private String prefix = "Bearer ";
            
            // getter/setter
            public String getHeader() { return header; }
            public void setHeader(String header) { this.header = header; }
            public String getPrefix() { return prefix; }
            public void setPrefix(String prefix) { this.prefix = prefix; }
        }
    }
    
    // 缓存配置
    public static class Cache {
        private boolean enabled = true;
        private String type = "redis";
        private long ttl = 3600;
        private int maxSize = 1000;
        
        // getter/setter
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
        public String getType() { return type; }
        public void setType(String type) { this.type = type; }
        public long getTtl() { return ttl; }
        public void setTtl(long ttl) { this.ttl = ttl; }
        public int getMaxSize() { return maxSize; }
        public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
    }
    
    // 功能开关配置
    public static class Features {
        private List<String> enabled = new ArrayList<>();
        private Map<String, Boolean> switches = new HashMap<>();
        
        // getter/setter
        public List<String> getEnabled() { return enabled; }
        public void setEnabled(List<String> enabled) { this.enabled = enabled; }
        public Map<String, Boolean> getSwitches() { return switches; }
        public void setSwitches(Map<String, Boolean> switches) { this.switches = switches; }
        
        public boolean isEnabled(String featureName) {
            return enabled.contains(featureName) || 
                   switches.getOrDefault(featureName, false);
        }
    }
}

// 启用配置属性
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
}

// 使用配置属性
@Service
public class FeatureService {
    
    private final AppProperties properties;
    
    public FeatureService(AppProperties properties) {
        this.properties = properties;
    }
    
    public void checkFeatures() {
        System.out.println("应用名称: " + properties.getName());
        System.out.println("安全启用: " + properties.getSecurity().isEnabled());
        System.out.println("缓存类型: " + properties.getCache().getType());
        
        if (properties.getFeatures().isEnabled("feature1")) {
            System.out.println("feature1 已启用");
        }
    }
}

3.4 @Value 注入示例

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ValueInjectionService {
    
    // 注入简单值
    @Value("${app.name}")
    private String appName;
    
    // 注入默认值
    @Value("${app.description:Default Description}")
    private String description;
    
    // 注入数值
    @Value("${server.port}")
    private int port;
    
    // 注入布尔值
    @Value("${app.debug:false}")
    private boolean debug;
    
    // 注入列表
    @Value("${app.features}")
    private List<String> features;
    
    // 注入系统属性
    @Value("${java.home}")
    private String javaHome;
    
    // 注入环境变量
    @Value("${HOME:unknown}")
    private String homeDir;
    
    // SpEL 表达式
    @Value("#{systemProperties['user.name']}")
    private String userName;
    
    // SpEL 数学运算
    @Value("#{2 * 1024}")
    private int calculatedValue;
    
    // SpEL 引用其他 Bean
    @Value("#{appProperties.name}")
    private String appNameFromBean;
    
    // SpEL 条件表达式
    @Value("#{environment['spring.profiles.active'] == 'prod' ? 'production' : 'development'}")
    private String environment;
    
    public void printValues() {
        System.out.println("应用名称: " + appName);
        System.out.println("描述: " + description);
        System.out.println("端口: " + port);
        System.out.println("调试模式: " + debug);
        System.out.println("功能列表: " + features);
        System.out.println("Java Home: " + javaHome);
        System.out.println("用户目录: " + homeDir);
        System.out.println("用户名: " + userName);
        System.out.println("计算值: " + calculatedValue);
        System.out.println("环境: " + environment);
    }
}

3.5 条件化配置示例

import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.*;
import org.springframework.beans.factory.annotation.Value;

@Configuration
public class ConditionalConfig {
    
    // 根据属性值创建 Bean
    @Bean
    @ConditionalOnProperty(
        prefix = "app.cache", 
        name = "type", 
        havingValue = "redis"
    )
    public CacheService redisCacheService() {
        System.out.println("创建 Redis 缓存服务");
        return new RedisCacheService();
    }
    
    @Bean
    @ConditionalOnProperty(
        prefix = "app.cache", 
        name = "type", 
        havingValue = "local",
        matchIfMissing = true
    )
    public CacheService localCacheService() {
        System.out.println("创建本地缓存服务");
        return new LocalCacheService();
    }
    
    // 根据 Profile 创建 Bean
    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        System.out.println("创建开发环境数据源");
        return new DevDataSource();
    }
    
    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        System.out.println("创建生产环境数据源");
        return new ProdDataSource();
    }
    
    // 根据属性是否存在创建 Bean
    @Bean
    @ConditionalOnProperty(prefix = "app.security", name = "secret-key")
    public SecurityService securityService() {
        System.out.println("创建安全服务");
        return new SecurityService();
    }
}

// Profile 条件示例
@Configuration
public class ProfileBasedConfig {
    
    @Bean
    @Profile({"dev", "test"})
    public MockDataService mockDataService() {
        return new MockDataService();
    }
    
    @Bean
    @Profile("prod")
    public RealDataService realDataService() {
        return new RealDataService();
    }
}

3.6 配置刷新示例

import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;

// 支持配置刷新的 Bean
@Service
@RefreshScope
@ConfigurationProperties(prefix = "app.dynamic")
public class DynamicConfig {
    
    private String feature1;
    private boolean feature2Enabled;
    private int maxConnections;
    
    // getter/setter
    public String getFeature1() { return feature1; }
    public void setFeature1(String feature1) { this.feature1 = feature1; }
    public boolean isFeature2Enabled() { return feature2Enabled; }
    public void setFeature2Enabled(boolean feature2Enabled) { this.feature2Enabled = feature2Enabled; }
    public int getMaxConnections() { return maxConnections; }
    public void setMaxConnections(int maxConnections) { this.maxConnections = maxConnections; }
    
    public void printConfig() {
        System.out.println("Feature1: " + feature1);
        System.out.println("Feature2 Enabled: " + feature2Enabled);
        System.out.println("Max Connections: " + maxConnections);
    }
}

// 配置变更监听器
@Component
public class ConfigChangeListener {
    
    @EventListener
    public void onEnvironmentChangeEvent(EnvironmentChangeEvent event) {
        System.out.println("配置变更: " + event.getKeys());
    }
}

// 控制器查看配置
@RestController
@RequestMapping("/api/config")
@RefreshScope
public class ConfigController {
    
    private final DynamicConfig dynamicConfig;
    
    public ConfigController(DynamicConfig dynamicConfig) {
        this.dynamicConfig = dynamicConfig;
    }
    
    @GetMapping
    public Map<String, Object> getConfig() {
        Map<String, Object> config = new HashMap<>();
        config.put("feature1", dynamicConfig.getFeature1());
        config.put("feature2Enabled", dynamicConfig.isFeature2Enabled());
        config.put("maxConnections", dynamicConfig.getMaxConnections());
        return config;
    }
}

3.7 外部化配置示例

import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:config.properties")
public class ExternalConfig {
    
    @Autowired
    private Environment environment;
    
    // 加载外部配置文件
    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        
        // 加载多个配置文件
        configurer.setLocations(
            new ClassPathResource("config.properties"),
            new ClassPathResource("config-override.properties")
        );
        
        // 忽略未找到的配置文件
        configurer.setIgnoreResourceNotFound(true);
        
        // 忽略无法解析的占位符
        configurer.setIgnoreUnresolvablePlaceholders(true);
        
        return configurer;
    }
    
    // 通过 Environment 访问配置
    public void printEnvironmentConfig() {
        System.out.println("应用名称: " + environment.getProperty("app.name"));
        System.out.println("端口: " + environment.getProperty("server.port", Integer.class));
        System.out.println("调试模式: " + environment.getProperty("app.debug", Boolean.class, false));
        
        // 检查是否包含属性
        if (environment.containsProperty("app.security.secret-key")) {
            System.out.println("安全配置已设置");
        }
        
        // 获取激活的 Profile
        String[] activeProfiles = environment.getActiveProfiles();
        System.out.println("激活的 Profile: " + Arrays.toString(activeProfiles));
    }
}

// 环境变量配置
@Service
public class EnvironmentConfigService {
    
    private final Environment environment;
    
    public EnvironmentConfigService(Environment environment) {
        this.environment = environment;
    }
    
    public String getDatabaseUrl() {
        // 优先使用环境变量,否则使用配置文件
        return environment.getProperty("DATABASE_URL", 
            environment.getProperty("spring.datasource.url"));
    }
    
    public String getDatabaseUsername() {
        return environment.getProperty("DB_USERNAME", 
            environment.getProperty("spring.datasource.username"));
    }
    
    public String getDatabasePassword() {
        return environment.getProperty("DB_PASSWORD", 
            environment.getProperty("spring.datasource.password"));
    }
}

四、实战应用场景

4.1 多数据源配置

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.*;
import javax.sql.DataSource;
import java.util.*;

// 多数据源配置属性
@ConfigurationProperties(prefix = "app.datasource")
public class MultiDataSourceProperties {
    
    private Map<String, DataSourceConfig> sources = new HashMap<>();
    
    public Map<String, DataSourceConfig> getSources() {
        return sources;
    }
    
    public static class DataSourceConfig {
        private String url;
        private String username;
        private String password;
        private String driverClassName;
        private int initialSize = 5;
        private int maxActive = 20;
        
        // getter/setter
        public String getUrl() { return url; }
        public void setUrl(String url) { this.url = url; }
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
        public String getDriverClassName() { return driverClassName; }
        public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }
        public int getInitialSize() { return initialSize; }
        public void setInitialSize(int initialSize) { this.initialSize = initialSize; }
        public int getMaxActive() { return maxActive; }
        public void setMaxActive(int maxActive) { this.maxActive = maxActive; }
    }
}

// 动态数据源配置
@Configuration
@EnableConfigurationProperties(MultiDataSourceProperties.class)
public class DynamicDataSourceConfig {
    
    @Bean
    @Primary
    public DataSource dynamicDataSource(MultiDataSourceProperties properties) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        
        properties.getSources().forEach((name, config) -> {
            targetDataSources.put(name, createDataSource(config));
        });
        
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(
            targetDataSources.get("primary")
        );
        
        return dataSource;
    }
    
    private DataSource createDataSource(MultiDataSourceProperties.DataSourceConfig config) {
        // 创建数据源
        return null;
    }
}

// 数据源上下文
public class DataSourceContextHolder {
    private static final ThreadLocal<String> context = new ThreadLocal<>();
    
    public static void setDataSource(String name) {
        context.set(name);
    }
    
    public static String getDataSource() {
        return context.get();
    }
    
    public static void clear() {
        context.remove();
    }
}

// 动态数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

// 使用注解切换数据源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "primary";
}

// 切面处理
@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(dataSource)")
    public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        try {
            DataSourceContextHolder.setDataSource(dataSource.value());
            return point.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}
配置示例
app:
  datasource:
    sources:
      primary:
        url: jdbc:mysql://localhost:3306/primary_db
        username: root
        password: password
        driver-class-name: com.mysql.cj.jdbc.Driver
      secondary:
        url: jdbc:mysql://localhost:3306/secondary_db
        username: root
        password: password
        driver-class-name: com.mysql.cj.jdbc.Driver

4.2 配置加密

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;

// 加密配置
@Configuration
public class EncryptionConfig {
    
    @Bean
    public StandardPBEStringEncryptor encryptor(
            @Value("${jasypt.encryptor.password}") String password) {
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        encryptor.setPassword(password);
        encryptor.setAlgorithm("PBEWithMD5AndDES");
        return encryptor;
    }
}

// 加密属性绑定
@ConfigurationProperties(prefix = "app")
public class SecureProperties {
    
    // ENC() 标识加密值
    @Value("${app.database.password}")
    private String databasePassword;
    
    @Value("${app.security.secret-key}")
    private String secretKey;
    
    // getter
}

// 配置文件
/*
app:
  database:
    password: ENC(加密后的密文)
  security:
    secret-key: ENC(加密后的密文)
    
jasypt:
  encryptor:
    password: ${JASYPT_PASSWORD}
*/

4.3 配置验证

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;

@ConfigurationProperties(prefix = "app")
@Validated
public class ValidatedProperties {
    
    @NotBlank(message = "应用名称不能为空")
    @Size(min = 2, max = 50, message = "应用名称长度必须在2-50之间")
    private String name;
    
    @Min(value = 1, message = "端口必须大于0")
    @Max(value = 65535, message = "端口必须小于65535")
    private int port = 8080;
    
    @Email(message = "管理员邮箱格式不正确")
    private String adminEmail;
    
    @Pattern(regexp = "^https?://.*", message = "URL 必须以 http 或 https 开头")
    private String callbackUrl;
    
    @Valid
    private final Database database = new Database();
    
    // getter/setter
    
    public static class Database {
        @NotBlank
        private String url;
        
        @NotBlank
        private String username;
        
        @Size(min = 6, message = "密码长度至少6位")
        private String password;
        
        @Min(1)
        @Max(100)
        private int poolSize = 10;
        
        // getter/setter
    }
}

// 配置验证失败时,应用启动会抛出异常

五、总结与最佳实践

配置管理原则

  1. 敏感信息加密

    • 密码、密钥等敏感信息加密存储
    • 使用环境变量传递加密密钥
  2. 环境隔离

    • 使用 Profile 分离不同环境配置
    • 生产环境配置不要包含在代码中
  3. 配置验证

    • 使用 JSR-303 注解验证配置
    • 提供合理的默认值
  4. 配置文档化

    • 添加配置属性注释
    • 生成配置元数据

最佳实践

  1. 配置文件命名

    • 使用统一的命名规范
    • 配置项按功能分组
  2. 配置更新

    • 使用 @RefreshScope 支持动态刷新
    • 监听配置变更事件
  3. 配置中心

    • 大型项目使用配置中心
    • 支持灰度发布和回滚

Spring Boot 配置体系提供了灵活而强大的配置管理能力。掌握配置文件格式、属性绑定、多环境配置等核心概念,能够帮助开发者构建可配置、可维护的应用程序。