02 Spring Boot企业级配置详解

本文深入剖析Spring Boot配置体系,包括多环境配置、属性绑定、SpEL表达式、profile隔离策略以及敏感信息加密,结合企业实践讲解配置最佳实践。

2.1 application.yml多环境配置

为什么需要多环境配置?

在软件开发生命周期中,一个应用通常需要部署到多个环境:

环境 用途 特点 配置差异
dev(开发) 本地开发调试 数据可随意修改 本地数据库、调试日志、mock服务
test(测试) 功能测试验证 模拟生产数据 测试数据库、详细日志
uat(预发布) 用户验收测试 接近生产环境 预发数据库、部分真实接口
prod(生产) 正式对外服务 真实数据、高可用 生产数据库、错误日志、真实接口

不同环境配置差异示例:

yaml 复制代码
# 数据库连接不同
dev:  mysql://localhost:3306/demo_dev
test: mysql://192.168.1.100:3306/demo_test
prod: mysql://prod-db.company.com:3306/demo_prod

# 日志级别不同
dev:  DEBUG  # 查看详细日志
test: INFO   # 记录关键信息
prod: WARN   # 只记录警告和错误

# 第三方服务不同
dev:  mock服务(本地模拟)
test: 测试环境API
prod: 生产环境API

Spring Boot多环境配置方案

Spring Boot提供了profile机制实现多环境配置,有两种常用方案:

方案一:多文件方式(推荐)

复制代码
src/main/resources/
├── application.yml              # 主配置文件(公共配置)
├── application-dev.yml          # 开发环境配置
├── application-test.yml         # 测试环境配置
├── application-uat.yml          # 预发布环境配置
└── application-prod.yml         # 生产环境配置

方案二:单文件多profile方式

yaml 复制代码
# application.yml
---
# 公共配置
spring:
  application:
    name: demo-service

---
# 开发环境
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:mysql://localhost:3306/demo_dev

---
# 生产环境
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-db:3306/demo_prod

两种方案对比:

维度 多文件方式 单文件方式
可读性 高,每个环境独立文件 低,所有配置混在一起
维护性 高,修改互不影响 低,容易改错环境
复杂度 低,结构清晰 高,需要---分隔
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐

企业实践推荐:使用多文件方式。

主配置文件设计

application.yml(公共配置)

yaml 复制代码
# ==================== 应用基本信息 ====================
spring:
  application:
    name: demo-service
  
  # 激活的profile(通过启动参数或环境变量指定)
  profiles:
    active: @spring.profiles.active@  # Maven占位符,打包时替换
  
  # Jackson序列化配置(所有环境通用)
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
    default-property-inclusion: non_null  # null值不序列化
    serialization:
      write-dates-as-timestamps: false    # 时间格式化为字符串

# ==================== MyBatis-Plus配置 ====================
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  global-config:
    db-config:
      id-type: auto              # 主键自增
      logic-delete-field: deleted  # 逻辑删除字段
      logic-delete-value: 1        # 逻辑已删除值
      logic-not-delete-value: 0    # 逻辑未删除值

# ==================== Server配置 ====================
server:
  port: 8080
  servlet:
    context-path: /api
  tomcat:
    uri-encoding: UTF-8
    threads:
      max: 200              # 最大线程数
      min-spare: 10         # 最小空闲线程
    max-connections: 10000  # 最大连接数
    accept-count: 100       # 等待队列长度

# ==================== 日志配置 ====================
logging:
  level:
    root: INFO
    com.company: DEBUG  # 本项目包路径
  pattern:
    console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n'
    file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n'

关键设计点:

1. Maven占位符

yaml 复制代码
spring:
  profiles:
    active: @spring.profiles.active@

配合pom.xml使用:

xml 复制代码
<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <spring.profiles.active>dev</spring.profiles.active>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>  <!-- 默认激活 -->
        </activation>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>

打包时指定环境:

bash 复制代码
# 打包dev环境
mvn clean package -P dev

# 打包生产环境
mvn clean package -P prod

效果: 打包后的jar包中,application.yml里的@spring.profiles.active@会被替换为实际值(dev或prod)。

2. 公共配置提取

将所有环境通用的配置放在application.yml,避免重复配置:

  • 应用名称
  • Jackson序列化配置
  • MyBatis-Plus全局配置
  • 日志格式

差异化配置放在各环境文件中:

  • 数据库连接
  • Redis连接
  • 第三方API地址
  • 日志级别

开发环境配置

application-dev.yml

yaml 复制代码
# ==================== 数据源配置 ====================
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo_dev?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    
    # Druid连接池配置
    druid:
      initial-size: 5           # 初始连接数
      min-idle: 5               # 最小空闲连接
      max-active: 20            # 最大活跃连接(开发环境不需要太多)
      max-wait: 60000           # 获取连接最大等待时间(ms)
      validation-query: SELECT 1
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000

# ==================== Redis配置 ====================
  redis:
    host: localhost
    port: 6379
    password:                   # 开发环境无密码
    database: 0
    timeout: 5000
    lettuce:
      pool:
        max-active: 8           # 开发环境连接池较小
        max-wait: -1
        max-idle: 8
        min-idle: 0

# ==================== 日志配置 ====================
logging:
  level:
    root: INFO
    com.company: DEBUG          # 开发环境打印DEBUG日志
    com.company.module.order.dal.mapper: DEBUG  # 打印SQL日志
  file:
    name: ./logs/demo-dev.log   # 日志文件路径

# ==================== 第三方服务配置 ====================
third-party:
  payment:
    api-url: http://localhost:9001/mock/payment  # Mock支付服务
    app-key: dev-app-key
    app-secret: dev-app-secret
    timeout: 30000
    
  sms:
    api-url: http://localhost:9002/mock/sms      # Mock短信服务
    access-key: dev-access-key
    access-secret: dev-access-secret

# ==================== 功能开关配置 ====================
feature:
  cache-enabled: true           # 开启缓存(方便测试缓存功能)
  async-enabled: true           # 开启异步(测试异步任务)
  rate-limit-enabled: false     # 关闭限流(方便调试)

生产环境配置

application-prod.yml

yaml 复制代码
# ==================== 数据源配置 ====================
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://prod-db-master.company.com:3306/demo_prod?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=true
    username: ${DB_USERNAME}    # 从环境变量读取(安全)
    password: ${DB_PASSWORD}    # 从环境变量读取(安全)
    
    # Druid连接池配置(生产环境优化)
    druid:
      initial-size: 10          # 初始连接数
      min-idle: 10              # 最小空闲连接
      max-active: 100           # 最大活跃连接(生产环境需要更多)
      max-wait: 60000
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false     # 获取连接时不测试(提升性能)
      test-on-return: false
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      
      # 监控配置(生产环境开启)
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: ${DRUID_USERNAME}
        login-password: ${DRUID_PASSWORD}

# ==================== Redis配置 ====================
  redis:
    host: ${REDIS_HOST}         # 从环境变量读取
    port: ${REDIS_PORT:6379}    # 默认6379
    password: ${REDIS_PASSWORD}
    database: 0
    timeout: 5000
    lettuce:
      pool:
        max-active: 50          # 生产环境连接池较大
        max-wait: 10000
        max-idle: 20
        min-idle: 5
      cluster:
        refresh:
          adaptive: true        # 自适应刷新拓扑
          period: 30000

# ==================== 日志配置 ====================
logging:
  level:
    root: WARN                  # 生产环境只记录警告
    com.company: INFO           # 业务日志INFO级别
  file:
    name: /app/logs/demo-prod.log
    max-size: 100MB             # 单文件最大100MB
    max-history: 30             # 保留30天
    total-size-cap: 10GB        # 总大小限制10GB

# ==================== 第三方服务配置 ====================
third-party:
  payment:
    api-url: https://api.payment.company.com  # 真实支付服务
    app-key: ${PAYMENT_APP_KEY}
    app-secret: ${PAYMENT_APP_SECRET}
    timeout: 30000
    retry-times: 3              # 重试3次
    
  sms:
    api-url: https://api.sms.company.com      # 真实短信服务
    access-key: ${SMS_ACCESS_KEY}
    access-secret: ${SMS_ACCESS_SECRET}
    max-send-per-day: 1000      # 每天最多发送1000条

# ==================== 功能开关配置 ====================
feature:
  cache-enabled: true           # 生产环境必须开启缓存
  async-enabled: true           # 开启异步提升性能
  rate-limit-enabled: true      # 生产环境必须限流

# ==================== 监控配置 ====================
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus  # 只暴露必要端点
  metrics:
    export:
      prometheus:
        enabled: true

配置优先级与覆盖规则

Spring Boot配置加载优先级(从高到低):

复制代码
1. 命令行参数
   java -jar app.jar --server.port=9090

2. 操作系统环境变量
   export SERVER_PORT=9090

3. application-{profile}.yml(激活的profile配置)
   application-prod.yml

4. application.yml(主配置文件)

5. @PropertySource注解加载的配置文件

6. 默认值(@Value注解的defaultValue)

覆盖规则示例:

yaml 复制代码
# application.yml
server:
  port: 8080
  tomcat:
    max-threads: 200

# application-prod.yml
server:
  port: 80              # 覆盖端口
  tomcat:
    max-threads: 500    # 覆盖线程数
    accept-count: 100   # 新增配置(不会删除application.yml中的其他配置)

最终生效的配置(prod环境):

yaml 复制代码
server:
  port: 80              # 来自application-prod.yml
  tomcat:
    max-threads: 500    # 来自application-prod.yml
    accept-count: 100   # 来自application-prod.yml

动态切换环境的方式

方式1:启动参数指定(推荐)

bash 复制代码
# 启动时指定profile
java -jar demo-service.jar --spring.profiles.active=prod

# 同时指定多个profile
java -jar demo-service.jar --spring.profiles.active=prod,redis-cluster

方式2:环境变量指定

bash 复制代码
# Linux/Mac
export SPRING_PROFILES_ACTIVE=prod
java -jar demo-service.jar

# Windows
set SPRING_PROFILES_ACTIVE=prod
java -jar demo-service.jar

方式3:JVM参数指定

bash 复制代码
java -Dspring.profiles.active=prod -jar demo-service.jar

方式4:IDEA运行配置

复制代码
Run -> Edit Configurations -> Active profiles: prod

生产环境最佳实践:

bash 复制代码
# 使用启动脚本
#!/bin/bash
APP_NAME=demo-service
PROFILE=prod
JVM_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC"

java $JVM_OPTS \
  -Dspring.profiles.active=$PROFILE \
  -jar $APP_NAME.jar \
  >> /app/logs/startup.log 2>&1 &

2.2 @ConfigurationProperties属性绑定

为什么需要属性绑定?

在传统的配置读取方式中,需要使用@Value注解逐个读取配置:

java 复制代码
// 传统方式:每个字段都要@Value注解,繁琐且容易出错
@Component
public class PaymentConfig {
    
    @Value("${third-party.payment.api-url}")
    private String apiUrl;
    
    @Value("${third-party.payment.app-key}")
    private String appKey;
    
    @Value("${third-party.payment.app-secret}")
    private String appSecret;
    
    @Value("${third-party.payment.timeout}")
    private Integer timeout;
    
    // 10个配置就要写10个@Value...
}

痛点:

  1. 代码冗长:每个字段都要写@Value注解
  2. 容易出错:配置key写错了编译不报错,运行才发现
  3. 不支持复杂对象:嵌套对象、List、Map等难以绑定
  4. 无类型校验:配置值类型错误运行时才报错

@ConfigurationProperties的优势:

  1. 批量绑定:自动将配置绑定到对象属性
  2. 类型安全:编译期检查,配置错误立即发现
  3. 支持复杂类型:嵌套对象、集合、Map等
  4. 支持校验:配合@Validated进行参数校验
  5. IDE友好:配置文件中有智能提示

@ConfigurationProperties基本用法

1. 定义配置类

java 复制代码
package com.company.config;

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

import javax.validation.constraints.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * 第三方服务配置
 * 
 * <p>关键注解:
 * - @ConfigurationProperties: 绑定配置前缀
 * - @Component: 注册为Spring Bean
 * - @Validated: 启用参数校验
 */
@Data
@Component
@ConfigurationProperties(prefix = "third-party")
@Validated
public class ThirdPartyProperties {
    
    /** 支付服务配置 */
    private PaymentConfig payment;
    
    /** 短信服务配置 */
    private SmsConfig sms;
    
    /** OSS配置 */
    private OssConfig oss;
    
    /**
     * 支付服务配置
     */
    @Data
    public static class PaymentConfig {
        
        /** API地址(必填) */
        @NotBlank(message = "支付API地址不能为空")
        private String apiUrl;
        
        /** 应用Key(必填) */
        @NotBlank(message = "支付AppKey不能为空")
        private String appKey;
        
        /** 应用Secret(必填) */
        @NotBlank(message = "支付AppSecret不能为空")
        private String appSecret;
        
        /** 超时时间(默认30秒) */
        @Min(value = 1000, message = "超时时间不能小于1秒")
        @Max(value = 300000, message = "超时时间不能大于5分钟")
        private Integer timeout = 30000;
        
        /** 重试次数(默认3次) */
        @Min(value = 0, message = "重试次数不能为负数")
        @Max(value = 5, message = "重试次数不能超过5次")
        private Integer retryTimes = 3;
        
        /** 是否启用(默认true) */
        private Boolean enabled = true;
        
        /** 支持的支付方式 */
        private List<String> supportedMethods;
    }
    
    /**
     * 短信服务配置
     */
    @Data
    public static class SmsConfig {
        
        @NotBlank
        private String apiUrl;
        
        @NotBlank
        private String accessKey;
        
        @NotBlank
        private String accessSecret;
        
        /** 每天最大发送量 */
        @Min(value = 1, message = "每天至少发送1条")
        private Integer maxSendPerDay = 1000;
        
        /** 发送间隔(使用Duration类型) */
        private Duration sendInterval = Duration.ofSeconds(60);
        
        /** 短信模板配置(Map类型) */
        private Map<String, String> templates;
    }
    
    /**
     * OSS配置
     */
    @Data
    public static class OssConfig {
        
        @NotBlank
        private String endpoint;
        
        @NotBlank
        private String accessKeyId;
        
        @NotBlank
        private String accessKeySecret;
        
        @NotBlank
        private String bucketName;
        
        /** 允许的文件类型 */
        private List<String> allowedExtensions;
        
        /** 最大文件大小(单位:字节) */
        @Min(1)
        private Long maxFileSize = 10 * 1024 * 1024L;  // 默认10MB
    }
}

2. application.yml配置

yaml 复制代码
third-party:
  # 支付服务配置
  payment:
    api-url: https://api.payment.com
    app-key: your-app-key
    app-secret: your-app-secret
    timeout: 30000
    retry-times: 3
    enabled: true
    supported-methods:
      - alipay
      - wechat
      - unionpay
  
  # 短信服务配置
  sms:
    api-url: https://api.sms.com
    access-key: your-access-key
    access-secret: your-access-secret
    max-send-per-day: 1000
    send-interval: 60s  # Duration类型自动解析
    templates:
      login: "验证码:{code},有效期5分钟"
      register: "欢迎注册,验证码:{code}"
      reset-password: "重置密码验证码:{code}"
  
  # OSS配置
  oss:
    endpoint: https://oss-cn-hangzhou.aliyuncs.com
    access-key-id: your-key-id
    access-key-secret: your-key-secret
    bucket-name: demo-bucket
    allowed-extensions:
      - jpg
      - png
      - pdf
      - docx
    max-file-size: 10485760  # 10MB

3. 使用配置

java 复制代码
package com.company.service;

import com.company.config.ThirdPartyProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
    
    // 直接注入配置对象
    private final ThirdPartyProperties thirdPartyProperties;
    
    public void createPayment(String orderNo, BigDecimal amount) {
        // 获取支付配置
        ThirdPartyProperties.PaymentConfig paymentConfig = thirdPartyProperties.getPayment();
        
        // 使用配置
        String apiUrl = paymentConfig.getApiUrl();
        String appKey = paymentConfig.getAppKey();
        Integer timeout = paymentConfig.getTimeout();
        
        log.info("[创建支付] URL={}, AppKey={}, Timeout={}ms", 
            apiUrl, appKey, timeout);
        
        // 调用支付接口...
        if (paymentConfig.getEnabled()) {
            callPaymentApi(apiUrl, appKey, orderNo, amount, timeout);
        }
    }
    
    private void callPaymentApi(String apiUrl, String appKey, 
                                String orderNo, BigDecimal amount, Integer timeout) {
        // 实际调用支付API
    }
}

复杂类型绑定

1. List类型绑定

yaml 复制代码
# application.yml
app:
  admin-users:
    - name: admin
      email: admin@company.com
      roles:
        - ADMIN
        - USER
    - name: operator
      email: operator@company.com
      roles:
        - OPERATOR
java 复制代码
@Data
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    private List<AdminUser> adminUsers;
    
    @Data
    public static class AdminUser {
        private String name;
        private String email;
        private List<String> roles;
    }
}

2. Map类型绑定

yaml 复制代码
# application.yml
app:
  error-codes:
    1001: "用户不存在"
    1002: "密码错误"
    1003: "账号已锁定"
  
  rate-limits:
    login: 5
    register: 3
    reset-password: 3
java 复制代码
@Data
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    /** 错误码映射 */
    private Map<Integer, String> errorCodes;
    
    /** 接口限流配置 */
    private Map<String, Integer> rateLimits;
}

3. Duration类型绑定

yaml 复制代码
# application.yml
app:
  cache-ttl: 30m      # 30分钟
  session-timeout: 2h  # 2小时
  token-expire: 7d     # 7天
java 复制代码
@Data
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    private Duration cacheTtl;        // PT30M
    private Duration sessionTimeout;  // PT2H
    private Duration tokenExpire;     // P7D
}

支持的Duration格式:

  • 10s = 10秒
  • 5m = 5分钟
  • 2h = 2小时
  • 3d = 3天

4. DataSize类型绑定

yaml 复制代码
# application.yml
app:
  max-upload-size: 10MB
  buffer-size: 1KB
  cache-size: 500MB
java 复制代码
@Data
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    private DataSize maxUploadSize;  // 10485760 bytes
    private DataSize bufferSize;     // 1024 bytes
    private DataSize cacheSize;      // 524288000 bytes
}

配置校验

使用JSR-303注解进行配置校验:

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "app")
@Validated  // 必须添加此注解才能启用校验
public class AppProperties {
    
    /** 应用名称(必填,长度3-50) */
    @NotBlank(message = "应用名称不能为空")
    @Size(min = 3, max = 50, message = "应用名称长度必须在3-50之间")
    private String name;
    
    /** 应用版本(必填,符合版本号格式) */
    @NotBlank
    @Pattern(regexp = "^\\d+\\.\\d+\\.\\d+$", message = "版本号格式错误,正确格式:1.0.0")
    private String version;
    
    /** 端口号(范围1024-65535) */
    @Min(value = 1024, message = "端口号不能小于1024")
    @Max(value = 65535, message = "端口号不能大于65535")
    private Integer port;
    
    /** 邮箱地址(必填,符合邮箱格式) */
    @NotBlank
    @Email(message = "邮箱格式错误")
    private String contactEmail;
    
    /** 官网地址(必填,符合URL格式) */
    @NotBlank
    @URL(message = "官网地址格式错误")
    private String website;
    
    /** 管理员列表(至少1个) */
    @NotEmpty(message = "至少需要配置一个管理员")
    private List<String> adminEmails;
    
    /** 嵌套对象校验 */
    @Valid  // 嵌套对象需要@Valid注解
    @NotNull
    private DatabaseConfig database;
    
    @Data
    public static class DatabaseConfig {
        
        @NotBlank
        private String url;
        
        @NotBlank
        @Size(min = 4, max = 20)
        private String username;
        
        @NotBlank
        @Size(min = 6, max = 50)
        private String password;
    }
}

配置错误时的异常:

复制代码
***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: 
Failed to bind properties under 'app' to com.company.config.AppProperties failed:

    Property: app.name
    Value: ab
    Reason: 应用名称长度必须在3-50之间

    Property: app.version
    Value: 1.0
    Reason: 版本号格式错误,正确格式:1.0.0

松散绑定(Relaxed Binding)

Spring Boot支持松散绑定,配置文件中的key可以使用多种格式:

yaml 复制代码
# 以下配置都能绑定到 appName 字段
app:
  app-name: demo        # 短横线(推荐)
  appName: demo         # 驼峰
  app_name: demo        # 下划线
  APP_NAME: demo        # 大写(环境变量)
java 复制代码
@Data
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String appName;  // 都能绑定到这个字段
}

绑定规则:

配置格式 示例 适用场景
短横线(kebab-case) app-name yml/properties文件(推荐)
驼峰(camelCase) appName yml/properties文件
下划线(snake_case) app_name yml/properties文件
大写下划线 APP_NAME 环境变量

@ConfigurationProperties vs @Value

对比维度 @ConfigurationProperties @Value
适用场景 批量配置绑定 单个配置读取
松散绑定 支持 不支持(必须精确匹配)
SpEL表达式 不支持 支持
JSR-303校验 支持 不支持
复杂类型 支持(List、Map、嵌套对象) 不支持
元数据支持 支持(IDE智能提示) 不支持
类型安全 编译期检查 运行时检查
推荐度 ⭐⭐⭐⭐⭐(复杂配置) ⭐⭐⭐(简单配置)

使用建议:

  • 配置类 :使用@ConfigurationProperties
  • 单个简单配置 :使用@Value
  • 需要SpEL表达式 :使用@Value

2.3 @Value注解与SpEL表达式

@Value基本用法

java 复制代码
@Component
public class AppConfig {
    
    /** 基本用法:读取配置值 */
    @Value("${app.name}")
    private String appName;
    
    /** 提供默认值:配置不存在时使用默认值 */
    @Value("${app.version:1.0.0}")
    private String appVersion;
    
    /** 读取系统属性 */
    @Value("${java.home}")
    private String javaHome;
    
    /** 读取环境变量 */
    @Value("${PATH}")
    private String systemPath;
}

SpEL表达式详解

SpEL(Spring Expression Language)是Spring的表达式语言,功能强大。

1. 字面量

java 复制代码
@Component
public class SpELDemo {
    
    /** 字符串字面量 */
    @Value("#{'Hello World'}")
    private String str1;
    
    /** 数字字面量 */
    @Value("#{100}")
    private Integer num1;
    
    /** 布尔字面量 */
    @Value("#{true}")
    private Boolean bool1;
    
    /** 数组/列表字面量 */
    @Value("#{{1,2,3,4,5}}")
    private List<Integer> numbers;
}

2. 算术运算

java 复制代码
@Component
public class SpELArithmetic {
    
    /** 加法 */
    @Value("#{10 + 20}")
    private Integer sum;  // 30
    
    /** 减法 */
    @Value("#{100 - 30}")
    private Integer diff;  // 70
    
    /** 乘法 */
    @Value("#{5 * 6}")
    private Integer product;  // 30
    
    /** 除法 */
    @Value("#{100 / 4}")
    private Integer quotient;  // 25
    
    /** 取模 */
    @Value("#{10 % 3}")
    private Integer remainder;  // 1
    
    /** 幂运算 */
    @Value("#{2 ^ 10}")
    private Integer power;  // 1024
}

3. 关系运算

java 复制代码
@Component
public class SpELComparison {
    
    @Value("#{10 > 5}")
    private Boolean gt;  // true
    
    @Value("#{10 >= 10}")
    private Boolean gte;  // true
    
    @Value("#{5 < 3}")
    private Boolean lt;  // false
    
    @Value("#{5 <= 5}")
    private Boolean lte;  // true
    
    @Value("#{10 == 10}")
    private Boolean eq;  // true
    
    @Value("#{10 != 5}")
    private Boolean ne;  // true
}

4. 逻辑运算

java 复制代码
@Component
public class SpELLogical {
    
    /** 逻辑与 */
    @Value("#{true and false}")
    private Boolean and;  // false
    
    /** 逻辑或 */
    @Value("#{true or false}")
    private Boolean or;  // true
    
    /** 逻辑非 */
    @Value("#{not true}")
    private Boolean not;  // false
    
    /** 组合表达式 */
    @Value("#{(10 > 5) and (20 < 30)}")
    private Boolean combined;  // true
}

5. 三元运算符

java 复制代码
@Component
public class SpELTernary {
    
    @Value("${app.env}")
    private String env;
    
    /** 三元运算符 */
    @Value("#{env == 'prod' ? 'https://api.prod.com' : 'http://api.dev.com'}")
    private String apiUrl;
    
    /** Elvis运算符(简化的三元) */
    @Value("#{env ?: 'dev'}")  // env为null时使用'dev'
    private String environment;
    
    /** 安全导航运算符 */
    @Value("#{user?.name}")  // user为null时不抛异常,返回null
    private String userName;
}

6. 正则表达式

java 复制代码
@Component
public class SpELRegex {
    
    @Value("${user.email}")
    private String email;
    
    /** 正则匹配 */
    @Value("#{email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'}")
    private Boolean isValidEmail;
    
    @Value("${user.phone}")
    private String phone;
    
    /** 手机号验证 */
    @Value("#{phone matches '^1[3-9]\\d{9}$'}")
    private Boolean isValidPhone;
}

7. 调用方法

java 复制代码
@Component
public class SpELMethod {
    
    @Value("${user.name}")
    private String userName;
    
    /** 调用String方法 */
    @Value("#{userName.toUpperCase()}")
    private String upperName;
    
    @Value("#{userName.length()}")
    private Integer nameLength;
    
    @Value("#{userName.substring(0, 3)}")
    private String namePrefix;
    
    /** 调用静态方法 */
    @Value("#{T(java.lang.Math).random()}")
    private Double randomNum;
    
    @Value("#{T(java.lang.Math).max(10, 20)}")
    private Integer maxNum;  // 20
    
    @Value("#{T(java.time.LocalDate).now()}")
    private LocalDate currentDate;
}

8. 引用Bean属性

java 复制代码
@Component("systemConfig")
public class SystemConfig {
    private String appName = "Demo System";
    private String version = "1.0.0";
    // getter/setter
}

@Component
public class SpELBeanRef {
    
    /** 引用Bean的属性 */
    @Value("#{systemConfig.appName}")
    private String appName;
    
    @Value("#{systemConfig.version}")
    private String version;
    
    /** 引用并调用方法 */
    @Value("#{systemConfig.appName.toUpperCase()}")
    private String upperAppName;
}

9. 集合操作

java 复制代码
@Component
public class SpELCollection {
    
    /** 过滤集合(selection) */
    @Value("#{numbers.?[#this > 5]}")  // 筛选大于5的元素
    private List<Integer> filtered;
    
    /** 映射集合(projection) */
    @Value("#{users.![name]}")  // 提取所有用户的name字段
    private List<String> userNames;
    
    /** 集合第一个元素 */
    @Value("#{users[0].name}")
    private String firstName;
    
    /** Map取值 */
    @Value("#{configs['database.url']}")
    private String dbUrl;
}

SpEL实战场景

场景1:根据环境动态配置

java 复制代码
@Component
public class DynamicConfig {
    
    @Value("${spring.profiles.active}")
    private String profile;
    
    /** 根据环境选择不同的API地址 */
    @Value("#{profile == 'prod' ? 'https://api.prod.com' : 'http://localhost:8080'}")
    private String apiUrl;
    
    /** 根据环境选择不同的日志级别 */
    @Value("#{profile == 'prod' ? 'WARN' : 'DEBUG'}")
    private String logLevel;
    
    /** 根据环境决定是否启用缓存 */
    @Value("#{profile == 'prod' or profile == 'uat'}")
    private Boolean cacheEnabled;
}

场景2:配置计算

java 复制代码
@Component
public class CalculatedConfig {
    
    @Value("${thread.pool.core-size:10}")
    private Integer coreSize;
    
    /** 最大线程数 = 核心线程数 * 2 */
    @Value("#{coreSize * 2}")
    private Integer maxSize;
    
    /** 队列容量 = 核心线程数 * 10 */
    @Value("#{coreSize * 10}")
    private Integer queueCapacity;
    
    @Value("${cache.ttl.seconds:300}")
    private Long ttlSeconds;
    
    /** 将秒转换为毫秒 */
    @Value("#{ttlSeconds * 1000}")
    private Long ttlMillis;
}

场景3:配置校验

java 复制代码
@Component
public class ValidatedConfig {
    
    @Value("${app.version}")
    private String version;
    
    /** 校验版本号格式 */
    @Value("#{version matches '^\\d+\\.\\d+\\.\\d+$' ? version : '1.0.0'}")
    private String validatedVersion;
    
    @Value("${server.port}")
    private Integer port;
    
    /** 校验端口范围 */
    @Value("#{port >= 1024 and port <= 65535 ? port : 8080}")
    private Integer validatedPort;
}

@Value的最佳实践

1. 提供默认值

java 复制代码
// ✅ 好习惯:提供默认值
@Value("${app.timeout:30000}")
private Integer timeout;

// ❌ 不推荐:配置不存在时启动失败
@Value("${app.timeout}")
private Integer timeout;

2. 使用静态常量定义配置key

java 复制代码
public class ConfigKeys {
    public static final String APP_NAME = "${app.name}";
    public static final String APP_VERSION = "${app.version:1.0.0}";
}

@Component
public class AppService {
    @Value(ConfigKeys.APP_NAME)
    private String appName;
    
    @Value(ConfigKeys.APP_VERSION)
    private String appVersion;
}

3. 复杂配置使用@ConfigurationProperties

java 复制代码
// ❌ 不推荐:大量@Value注解
@Value("${db.url}")
private String dbUrl;
@Value("${db.username}")
private String dbUsername;
@Value("${db.password}")
private String dbPassword;
// ... 还有10个配置

// ✅ 推荐:使用@ConfigurationProperties
@ConfigurationProperties(prefix = "db")
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;
    // ... 其他配置
}

4. 避免在@Value中使用复杂SpEL

java 复制代码
// ❌ 不推荐:逻辑太复杂
@Value("#{profile == 'prod' ? (region == 'cn' ? 'https://api-cn.prod.com' : 'https://api-us.prod.com') : 'http://localhost:8080'}")
private String apiUrl;

// ✅ 推荐:使用Java代码实现复杂逻辑
@PostConstruct
public void init() {
    if ("prod".equals(profile)) {
        apiUrl = "cn".equals(region) ? "https://api-cn.prod.com" : "https://api-us.prod.com";
    } else {
        apiUrl = "http://localhost:8080";
    }
}

2.4 profile配置隔离策略

profile激活机制

Spring Boot支持多种方式激活profile:

1. 配置文件激活(application.yml)

yaml 复制代码
spring:
  profiles:
    active: dev  # 激活dev profile

2. 命令行参数激活

bash 复制代码
java -jar app.jar --spring.profiles.active=prod

3. 环境变量激活

bash 复制代码
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

4. JVM系统属性激活

bash 复制代码
java -Dspring.profiles.active=prod -jar app.jar

5. Maven profile激活

xml 复制代码
<profiles>
    <profile>
        <id>prod</id>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>
bash 复制代码
mvn clean package -P prod

优先级(从高到低):

复制代码
命令行参数 > JVM系统属性 > 环境变量 > 配置文件

多profile同时激活

同时激活多个profile:

bash 复制代码
# 激活prod和redis-cluster两个profile
java -jar app.jar --spring.profiles.active=prod,redis-cluster
yaml 复制代码
# application-prod.yml(基础配置)
spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/demo

# application-redis-cluster.yml(Redis集群配置)
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.1:7000
        - 192.168.1.2:7000
        - 192.168.1.3:7000

最终生效: 两个配置文件都会加载,配置合并。

profile组合(Profile Groups)

Spring Boot 2.4+支持profile组合:

yaml 复制代码
# application.yml
spring:
  profiles:
    group:
      prod:
        - prod-base
        - prod-db
        - prod-redis
        - prod-mq
      dev:
        - dev-base
        - dev-db

使用:

bash 复制代码
# 只需激活prod,自动激活prod-base、prod-db、prod-redis、prod-mq
java -jar app.jar --spring.profiles.active=prod

@Profile注解

根据profile条件装配Bean:

java 复制代码
/**
 * 开发环境配置
 * 只在dev profile激活时生效
 */
@Configuration
@Profile("dev")
public class DevConfig {
    
    @Bean
    public DataSource devDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo_dev");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setMaximumPoolSize(10);  // 开发环境小连接池
        return dataSource;
    }
    
    @Bean
    public CacheManager devCacheManager() {
        // 开发环境使用简单缓存
        return new ConcurrentMapCacheManager();
    }
}

/**
 * 生产环境配置
 * 只在prod profile激活时生效
 */
@Configuration
@Profile("prod")
public class ProdConfig {
    
    @Bean
    public DataSource prodDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://prod-db:3306/demo_prod");
        dataSource.setUsername(System.getenv("DB_USERNAME"));
        dataSource.setPassword(System.getenv("DB_PASSWORD"));
        dataSource.setMaximumPoolSize(100);  // 生产环境大连接池
        return dataSource;
    }
    
    @Bean
    public CacheManager prodCacheManager(RedisConnectionFactory factory) {
        // 生产环境使用Redis缓存
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30));
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
    }
}

@Profile支持表达式:

java 复制代码
// 在dev或test环境生效
@Configuration
@Profile({"dev", "test"})
public class NonProdConfig {
}

// 不在prod环境生效
@Configuration
@Profile("!prod")
public class NonProdConfig {
}

// 在dev环境且debug模式生效
@Configuration
@Profile("dev & debug")
public class DevDebugConfig {
}

// 在dev或test,但不在local环境生效
@Configuration
@Profile("(dev | test) & !local")
public class ComplexConfig {
}

profile配置覆盖策略

配置文件加载顺序:

复制代码
1. application.yml(主配置)
2. application-{profile}.yml(profile配置)
3. 命令行参数

示例:

yaml 复制代码
# application.yml(所有环境通用)
server:
  port: 8080
  tomcat:
    max-threads: 200
    accept-count: 100

logging:
  level:
    root: INFO
yaml 复制代码
# application-prod.yml(生产环境覆盖)
server:
  port: 80              # 覆盖端口
  tomcat:
    max-threads: 500    # 覆盖最大线程数
    # accept-count继承主配置的100

logging:
  level:
    root: WARN          # 覆盖日志级别

最终生效配置(prod环境):

yaml 复制代码
server:
  port: 80              # 来自application-prod.yml
  tomcat:
    max-threads: 500    # 来自application-prod.yml
    accept-count: 100   # 来自application.yml(未被覆盖)

logging:
  level:
    root: WARN          # 来自application-prod.yml

配置文件位置优先级

Spring Boot会从以下位置加载配置文件(优先级从高到低):

复制代码
1. file:./config/         (当前目录/config子目录)
2. file:./                (当前目录)
3. classpath:/config/     (classpath下config目录)
4. classpath:/            (classpath根目录)

实战场景:

复制代码
# 生产环境部署结构
/app/
├── demo-service.jar      # 应用jar包
├── application-prod.yml  # 外部配置文件(优先级最高)
└── config/
    └── custom.yml        # 自定义配置

启动:

bash 复制代码
java -jar demo-service.jar --spring.profiles.active=prod

加载顺序:

  1. 先加载jar包内的application.yml(classpath:/)
  2. 再加载jar包外的application-prod.yml(file:./)
  3. 外部配置覆盖jar包内配置

优点:

  • jar包保持不变
  • 通过外部配置文件调整配置
  • 无需重新打包

2.5 配置加密与敏感信息处理

为什么需要配置加密?

配置文件中的敏感信息:

yaml 复制代码
# ❌ 明文存储(安全隐患)
spring:
  datasource:
    username: root
    password: MyPassword123!  # 密码明文
  
  redis:
    password: Redis@2024      # Redis密码明文

third-party:
  payment:
    app-secret: sk_live_51Hxxx...  # 支付密钥明文
  
  sms:
    access-secret: AbCd1234EfGh5678  # 短信密钥明文

安全风险:

  1. 代码泄露风险:配置文件提交到Git,密码泄露
  2. 运维风险:运维人员可以看到所有密码
  3. 审计困难:无法追踪谁访问了敏感配置
  4. 合规问题:不符合安全审计要求(如ISO27001)

配置加密方案

方案1:Jasypt加密(推荐)

Jasypt是一个Java加密库,Spring Boot有对应的starter。

1. 引入依赖

xml 复制代码
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

2. 配置加密密钥

yaml 复制代码
# application.yml
jasypt:
  encryptor:
    password: ${JASYPT_ENCRYPTOR_PASSWORD}  # 从环境变量读取
    algorithm: PBEWithMD5AndDES              # 加密算法
    iv-generator-classname: org.jasypt.iv.NoIvGenerator

3. 加密配置值

java 复制代码
import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class ConfigEncryptor implements CommandLineRunner {
    
    @Autowired
    private StringEncryptor encryptor;
    
    @Override
    public void run(String... args) {
        // 加密敏感配置
        String dbPassword = "MyPassword123!";
        String encrypted = encryptor.encrypt(dbPassword);
        System.out.println("加密后:" + encrypted);
        // 输出:oQw9K3xXzP7Yx8jF2nB4vA==
    }
}

4. 使用加密配置

yaml 复制代码
spring:
  datasource:
    username: root
    password: ENC(oQw9K3xXzP7Yx8jF2nB4vA==)  # ENC()包裹加密值
  
  redis:
    password: ENC(aB3cD5eF7gH9iJ1kL3mN5oP7==)

third-party:
  payment:
    app-secret: ENC(qR9sT1uV3wX5yZ7aB9cD1eF3==)

5. 启动应用

bash 复制代码
# 通过环境变量传递解密密钥
export JASYPT_ENCRYPTOR_PASSWORD=my-secret-key
java -jar app.jar

优点:

  • ✅ 配置文件中不再有明文密码
  • ✅ 解密密钥通过环境变量传递,不出现在代码中
  • ✅ 支持多种加密算法
  • ✅ 对应用代码无侵入

方案2:环境变量(推荐)

yaml 复制代码
# application.yml(不包含敏感信息)
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  
  redis:
    host: ${REDIS_HOST}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD}

启动脚本:

bash 复制代码
#!/bin/bash
# start.sh

# 设置环境变量
export DB_HOST=prod-db.company.com
export DB_PORT=3306
export DB_NAME=demo_prod
export DB_USERNAME=app_user
export DB_PASSWORD=SecurePassword123!
export REDIS_HOST=redis-cluster.company.com
export REDIS_PORT=6379
export REDIS_PASSWORD=RedisSecure456!

# 启动应用
java -jar demo-service.jar

或使用Docker:

yaml 复制代码
# docker-compose.yml
version: '3'
services:
  app:
    image: demo-service:latest
    environment:
      - DB_HOST=prod-db
      - DB_USERNAME=app_user
      - DB_PASSWORD=SecurePassword123!
      - REDIS_HOST=redis
      - REDIS_PASSWORD=RedisSecure456!

优点:

  • ✅ 配置文件完全不包含敏感信息
  • ✅ 每个环境可以设置不同的环境变量
  • ✅ 符合12-Factor App原则
  • ✅ 容器化部署友好

方案3:配置中心(企业级推荐)

使用Nacos、Apollo、Spring Cloud Config等配置中心:

yaml 复制代码
# bootstrap.yml(只包含配置中心地址)
spring:
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER:nacos.company.com:8848}
        namespace: ${NACOS_NAMESPACE:prod}
        group: DEFAULT_GROUP
        username: ${NACOS_USERNAME}
        password: ${NACOS_PASSWORD}

配置存储在Nacos:

yaml 复制代码
# 在Nacos Web界面配置(支持加密)
spring.datasource.url=jdbc:mysql://prod-db:3306/demo_prod
spring.datasource.username=app_user
spring.datasource.password=SecurePassword123!  # Nacos内部加密存储

优点:

  • ✅ 配置集中管理
  • ✅ 支持配置加密
  • ✅ 支持配置动态刷新
  • ✅ 权限控制(不同角色看到不同配置)
  • ✅ 配置变更审计
  • ✅ 配置版本管理

敏感信息处理最佳实践

1. 分离敏感配置

复制代码
src/main/resources/
├── application.yml              # 非敏感配置(可以提交Git)
├── application-dev.yml          # 开发环境(可以提交Git)
├── application-prod.yml         # 生产环境(不提交Git,通过配置中心管理)
└── .gitignore                   # 忽略敏感文件

.gitignore:

复制代码
# 忽略生产环境配置
application-prod.yml
application-uat.yml

# 忽略包含敏感信息的配置
*-secret.yml
*.key
*.pem

2. 使用占位符

yaml 复制代码
# application-prod.template.yml(模板文件,可提交Git)
spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/demo_prod
    username: ${DB_USERNAME}      # 占位符
    password: ${DB_PASSWORD}      # 占位符
  
  redis:
    host: ${REDIS_HOST}
    password: ${REDIS_PASSWORD}

部署时替换:

bash 复制代码
# 生成实际配置文件
envsubst < application-prod.template.yml > application-prod.yml

3. 密钥管理

java 复制代码
/**
 * 密钥配置
 * 敏感信息通过专门的密钥管理服务获取
 */
@Configuration
public class SecretConfig {
    
    @Value("${secret.manager.url}")
    private String secretManagerUrl;
    
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        
        // 从密钥管理服务获取数据库密码
        String dbPassword = getSecretFromVault("database/prod/password");
        
        dataSource.setJdbcUrl("jdbc:mysql://prod-db:3306/demo_prod");
        dataSource.setUsername("app_user");
        dataSource.setPassword(dbPassword);  // 不在配置文件中
        
        return dataSource;
    }
    
    /**
     * 从Vault/KMS获取密钥
     */
    private String getSecretFromVault(String secretPath) {
        // 调用密钥管理服务API
        // 例如:HashiCorp Vault、AWS KMS、Azure Key Vault
        RestTemplate restTemplate = new RestTemplate();
        String url = secretManagerUrl + "/v1/secret/data/" + secretPath;
        
        SecretResponse response = restTemplate.getForObject(url, SecretResponse.class);
        return response.getData().getPassword();
    }
}

4. 访问控制

yaml 复制代码
# 配置中心ACL(访问控制列表)
nacos:
  config:
    database-password:
      readers:              # 只读权限
        - ops-team
        - dba-team
      writers:              # 读写权限
        - admin-team
      encryption: true      # 强制加密
      audit-log: true       # 记录访问日志

5. 定期轮换密钥

java 复制代码
/**
 * 密钥轮换任务
 * 定期更新敏感配置,降低泄露风险
 */
@Component
public class SecretRotationTask {
    
    @Scheduled(cron = "0 0 2 1 * ?")  // 每月1号凌晨2点执行
    public void rotateSecrets() {
        // 1. 生成新密码
        String newPassword = generateSecurePassword();
        
        // 2. 更新数据库密码
        updateDatabasePassword(newPassword);
        
        // 3. 更新配置中心
        updateConfigCenter("spring.datasource.password", newPassword);
        
        // 4. 通知运维团队
        notifyOpsTeam("数据库密码已轮换");
    }
}

配置安全检查清单

开发阶段:

  • 使用.gitignore防止敏感配置提交
  • 使用配置模板文件(.template.yml
  • 敏感配置使用占位符(${VAR}
  • Code Review检查是否有硬编码密码

测试阶段:

  • 验证加密配置能正常解密
  • 验证环境变量注入是否正确
  • 测试配置缺失时的降级策略

生产部署:

  • 使用环境变量或配置中心
  • 启用配置加密(Jasypt/Nacos加密)
  • 配置访问权限控制
  • 启用审计日志
  • 定期轮换密钥

运维监控:

  • 监控配置访问日志
  • 告警异常配置读取
  • 定期审计密钥使用情况

总结

Spring Boot配置体系通过多环境隔离、灵活绑定、表达式支持、安全加密构建了完整的配置管理方案:

技术 适用场景 核心价值
多环境配置 dev/test/prod环境隔离 一套代码多环境部署
@ConfigurationProperties 批量配置绑定 类型安全、支持校验、IDE友好
@Value + SpEL 单个配置、动态计算 灵活表达式、运算支持
profile机制 条件装配、配置覆盖 按需加载、配置分层
配置加密 敏感信息保护 Jasypt加密、环境变量、配置中心

企业实践建议:

  1. 开发环境:使用本地配置文件,方便调试
  2. 测试环境:使用配置中心,模拟生产
  3. 生产环境:配置中心+环境变量+加密,三重保障
  4. 敏感信息:永不提交代码库,使用密钥管理服务

面试题精选

Q1: Spring Boot如何实现多环境配置?有哪些激活profile的方式?

参考答案:

多环境配置实现:

Spring Boot通过profile机制实现多环境配置,核心原理是根据激活的profile加载不同的配置文件。

配置文件命名规则:

复制代码
application.yml              # 主配置(所有环境通用)
application-{profile}.yml   # profile配置(特定环境)

常见profile:

  • dev:开发环境
  • test:测试环境
  • uat:预发布环境
  • prod:生产环境

激活profile的方式(优先级从高到低):

方式1:命令行参数(最高优先级)

bash 复制代码
java -jar app.jar --spring.profiles.active=prod

方式2:JVM系统属性

bash 复制代码
java -Dspring.profiles.active=prod -jar app.jar

方式3:操作系统环境变量

bash 复制代码
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

方式4:配置文件(application.yml)

yaml 复制代码
spring:
  profiles:
    active: dev

方式5:Maven profile

xml 复制代码
<profiles>
    <profile>
        <id>prod</id>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>

打包时指定:

bash 复制代码
mvn clean package -P prod

配置加载顺序:

  1. 加载application.yml(主配置)
  2. 根据激活的profile加载application-{profile}.yml
  3. profile配置覆盖主配置
  4. 命令行参数覆盖配置文件

同时激活多个profile:

bash 复制代码
java -jar app.jar --spring.profiles.active=prod,redis-cluster

最佳实践:

  • 开发环境:IDEA配置或本地application.yml
  • 测试/生产:启动脚本通过命令行参数指定
  • 避免在代码中硬编码profile

Q2: @ConfigurationProperties和@Value有什么区别?什么时候用哪个?

参考答案:

核心区别对比:

特性 @ConfigurationProperties @Value
绑定方式 批量绑定(前缀匹配) 单个绑定(精确key)
松散绑定 支持(app-name/appName/app_name都行) 不支持(必须精确匹配)
SpEL表达式 不支持 支持(#{}
JSR-303校验 支持(@Validated) 不支持
复杂类型 支持(List、Map、嵌套对象) 不支持
元数据支持 支持(IDE智能提示) 不支持
类型安全 编译期检查 运行时检查
默认值 字段赋值 ${key:defaultValue}

@ConfigurationProperties适用场景:

  1. 配置项较多(3个以上相关配置)
java 复制代码
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String version;
    private Integer port;
    private Duration timeout;
    private List<String> allowedOrigins;
    // ... 10个配置
}
  1. 需要参数校验
java 复制代码
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
    @NotBlank
    @Size(min = 3, max = 50)
    private String name;
    
    @Min(1024)
    @Max(65535)
    private Integer port;
}
  1. 复杂数据结构
java 复制代码
@ConfigurationProperties(prefix = "third-party")
public class ThirdPartyProperties {
    private PaymentConfig payment;
    private List<SmsTemplate> smsTemplates;
    private Map<String, ApiConfig> apis;
}

@Value适用场景:

  1. 单个简单配置
java 复制代码
@Value("${app.name}")
private String appName;
  1. 需要SpEL表达式
java 复制代码
@Value("#{systemProperties['user.home']}")
private String userHome;

@Value("#{10 * 1024 * 1024}")  // 10MB
private Long maxFileSize;
  1. 动态计算
java 复制代码
@Value("${app.env}")
private String env;

@Value("#{env == 'prod' ? 'https://api.prod.com' : 'http://localhost:8080'}")
private String apiUrl;

最佳实践:

  • 配置类 :优先使用@ConfigurationProperties
  • 单个配置 :使用@Value
  • 需要SpEL :使用@Value
  • 需要校验 :使用@ConfigurationProperties + @Validated

Q3: SpEL表达式有哪些常用功能?如何在@Value中使用?

参考答案:

SpEL(Spring Expression Language)核心功能:

1. 字面量

java 复制代码
@Value("#{'Hello World'}")  // 字符串
private String str;

@Value("#{100}")           // 数字
private Integer num;

@Value("#{true}")          // 布尔
private Boolean bool;

2. 算术运算

java 复制代码
@Value("#{10 + 20}")       // 加法:30
private Integer sum;

@Value("#{100 - 30}")      // 减法:70
private Integer diff;

@Value("#{5 * 6}")         // 乘法:30
private Integer product;

@Value("#{100 / 4}")       // 除法:25
private Integer quotient;

@Value("#{10 % 3}")        // 取模:1
private Integer mod;

@Value("#{2 ^ 10}")        // 幂运算:1024
private Integer power;

3. 关系运算

java 复制代码
@Value("#{10 > 5}")        // 大于:true
@Value("#{10 >= 10}")      // 大于等于:true
@Value("#{5 < 3}")         // 小于:false
@Value("#{10 == 10}")      // 等于:true
@Value("#{10 != 5}")       // 不等于:true

4. 逻辑运算

java 复制代码
@Value("#{true and false}")     // 与:false
@Value("#{true or false}")      // 或:true
@Value("#{not true}")           // 非:false
@Value("#{(10 > 5) and (20 < 30)}")  // 组合:true

5. 三元运算符

java 复制代码
@Value("${app.env}")
private String env;

@Value("#{env == 'prod' ? 80 : 8080}")
private Integer port;

// Elvis运算符(简化的三元)
@Value("#{env ?: 'dev'}")  // env为null时使用'dev'
private String environment;

// 安全导航运算符
@Value("#{user?.name}")    // user为null时不抛异常,返回null
private String userName;

6. 正则表达式

java 复制代码
@Value("${user.email}")
private String email;

@Value("#{email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'}")
private Boolean isValidEmail;

7. 调用方法

java 复制代码
// 调用String方法
@Value("#{userName.toUpperCase()}")
private String upperName;

@Value("#{userName.substring(0, 3)}")
private String prefix;

// 调用静态方法
@Value("#{T(java.lang.Math).random()}")
private Double random;

@Value("#{T(java.time.LocalDate).now()}")
private LocalDate today;

8. 引用Bean属性

java 复制代码
// 引用名为systemConfig的Bean的属性
@Value("#{systemConfig.appName}")
private String appName;

@Value("#{systemConfig.version.toUpperCase()}")
private String version;

9. 集合操作

java 复制代码
// 过滤集合
@Value("#{numbers.?[#this > 5]}")  // 筛选大于5的元素
private List<Integer> filtered;

// 映射集合
@Value("#{users.![name]}")         // 提取所有用户的name
private List<String> names;

// 访问元素
@Value("#{users[0].email}")
private String firstEmail;

// Map取值
@Value("#{configs['database.url']}")
private String dbUrl;

实战案例:

场景1:根据环境动态配置

java 复制代码
@Value("${spring.profiles.active}")
private String profile;

@Value("#{profile == 'prod' ? 'https://api.prod.com' : 'http://localhost:8080'}")
private String apiUrl;

场景2:配置值计算

java 复制代码
@Value("${thread.pool.core-size:10}")
private Integer coreSize;

@Value("#{coreSize * 2}")         // 最大线程数 = 核心线程数 * 2
private Integer maxSize;

@Value("#{coreSize * 10}")        // 队列容量 = 核心线程数 * 10
private Integer queueCapacity;

场景3:配置校验

java 复制代码
@Value("${server.port}")
private Integer port;

@Value("#{port >= 1024 and port <= 65535 ? port : 8080}")
private Integer validatedPort;

注意事项:

  1. SpEL语法错误会导致启动失败
  2. 复杂逻辑建议用Java代码实现,保持可读性
  3. SpEL表达式不支持@ConfigurationProperties

Q4: 如何保护配置文件中的敏感信息?有哪些方案?

参考答案:

敏感信息包括:

  • 数据库密码
  • Redis密码
  • 第三方API密钥(支付、短信等)
  • 证书私钥
  • Token/Secret

保护方案对比:

方案1:Jasypt加密(适合小型项目)

实现步骤:

  1. 引入依赖
xml 复制代码
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
  1. 配置加密密钥
yaml 复制代码
jasypt:
  encryptor:
    password: ${JASYPT_ENCRYPTOR_PASSWORD}  # 从环境变量读取
    algorithm: PBEWithMD5AndDES
  1. 加密敏感值
java 复制代码
StringEncryptor encryptor = new DefaultPBEStringEncryptor();
String encrypted = encryptor.encrypt("MyPassword123!");
// 输出:oQw9K3xXzP7Yx8jF2nB4vA==
  1. 使用加密配置
yaml 复制代码
spring:
  datasource:
    password: ENC(oQw9K3xXzP7Yx8jF2nB4vA==)  # ENC()包裹
  1. 启动应用
bash 复制代码
export JASYPT_ENCRYPTOR_PASSWORD=my-secret-key
java -jar app.jar

优点:

  • ✅ 配置文件中无明文密码
  • ✅ 对应用代码无侵入
  • ✅ 简单易用

缺点:

  • ❌ 加密密钥需要妥善保管
  • ❌ 所有环境共用一个密钥

方案2:环境变量(推荐)

配置文件:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

启动脚本:

bash 复制代码
#!/bin/bash
export DB_HOST=prod-db.company.com
export DB_USERNAME=app_user
export DB_PASSWORD=SecurePassword123!

java -jar app.jar

Docker Compose:

yaml 复制代码
version: '3'
services:
  app:
    image: demo-service
    environment:
      - DB_HOST=prod-db
      - DB_USERNAME=app_user
      - DB_PASSWORD=SecurePassword123!

优点:

  • ✅ 配置文件完全不含敏感信息
  • ✅ 符合12-Factor App原则
  • ✅ 容器化部署友好
  • ✅ 每个环境独立配置

缺点:

  • ❌ 需要在部署环境设置环境变量

方案3:配置中心(企业级推荐)

使用Nacos/Apollo/Spring Cloud Config:

yaml 复制代码
# bootstrap.yml(只包含配置中心地址)
spring:
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER}
        username: ${NACOS_USERNAME}
        password: ${NACOS_PASSWORD}

在Nacos Web界面配置:

yaml 复制代码
spring.datasource.password=SecurePassword123!  # Nacos内部加密

优点:

  • ✅ 配置集中管理
  • ✅ 支持配置加密
  • ✅ 权限控制(不同角色看不同配置)
  • ✅ 配置动态刷新
  • ✅ 配置变更审计
  • ✅ 配置版本管理

缺点:

  • ❌ 需要部署配置中心
  • ❌ 增加系统复杂度

方案4:密钥管理服务(最安全)

使用HashiCorp Vault/AWS KMS/Azure Key Vault:

java 复制代码
@Configuration
public class SecretConfig {
    
    @Bean
    public DataSource dataSource() {
        // 从密钥管理服务获取密码
        String password = vaultClient.read("secret/database/password");
        
        HikariDataSource ds = new HikariDataSource();
        ds.setPassword(password);  // 密码不在配置文件中
        return ds;
    }
}

优点:

  • ✅ 最高安全级别
  • ✅ 密钥轮换
  • ✅ 访问审计
  • ✅ 细粒度权限控制

缺点:

  • ❌ 需要额外服务
  • ❌ 学习成本高
  • ❌ 增加依赖

最佳实践建议:

项目规模 推荐方案 理由
小型项目 环境变量 简单、容器化友好
中型项目 Jasypt + 环境变量 加密配置 + 灵活部署
大型项目 配置中心(Nacos/Apollo) 集中管理、权限控制
金融/安全敏感 密钥管理服务(Vault/KMS) 最高安全级别

安全检查清单:

  • 敏感配置不提交到Git(.gitignore
  • 使用配置模板文件(.template.yml
  • 生产环境使用环境变量或配置中心
  • 启用配置加密
  • 配置访问权限控制
  • 定期轮换密钥
  • 启用审计日志

Q5: Spring Boot配置的加载顺序是什么?如何解决配置冲突?

参考答案:

Spring Boot配置加载优先级(从高到低):

复制代码
1. 命令行参数
   java -jar app.jar --server.port=9090

2. Java系统属性(-D参数)
   java -Dserver.port=9090 -jar app.jar

3. 操作系统环境变量
   export SERVER_PORT=9090

4. application-{profile}.yml(激活的profile配置)
   例如:application-prod.yml

5. application.yml(主配置文件)

6. @PropertySource加载的配置文件

7. 默认值(@Value注解的defaultValue)
   @Value("${server.port:8080}")

配置文件位置优先级(从高到低):

复制代码
1. file:./config/          (当前目录/config子目录)
2. file:./                 (当前目录)
3. classpath:/config/      (classpath下config目录)
4. classpath:/             (classpath根目录)

实战示例:

场景: 同一个配置在多处定义

yaml 复制代码
# application.yml(优先级:5)
server:
  port: 8080

# application-prod.yml(优先级:4)
server:
  port: 80

# 命令行参数(优先级:1)
java -jar app.jar --server.port=9090 --spring.profiles.active=prod

最终生效: server.port=9090(命令行参数优先级最高)

配置覆盖规则:

规则1:高优先级覆盖低优先级

yaml 复制代码
# application.yml
server:
  port: 8080
  tomcat:
    max-threads: 200

# application-prod.yml
server:
  port: 80  # 覆盖

最终生效:

yaml 复制代码
server:
  port: 80              # 来自application-prod.yml
  tomcat:
    max-threads: 200    # 来自application.yml(未被覆盖)

规则2:属性级别覆盖(非对象级别)

yaml 复制代码
# application.yml
server:
  tomcat:
    max-threads: 200
    accept-count: 100
    min-spare-threads: 10

# application-prod.yml
server:
  tomcat:
    max-threads: 500  # 只覆盖max-threads

最终生效:

yaml 复制代码
server:
  tomcat:
    max-threads: 500        # 来自prod(覆盖)
    accept-count: 100       # 来自application.yml(保留)
    min-spare-threads: 10   # 来自application.yml(保留)

解决配置冲突的方法:

方法1:查看实际生效的配置

bash 复制代码
# 启动时查看配置
java -jar app.jar --debug

# 或通过Actuator端点查看
http://localhost:8080/actuator/env
http://localhost:8080/actuator/configprops

方法2:使用Spring Boot Actuator

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: env,configprops

访问:http://localhost:8080/actuator/env/server.port

输出:

json 复制代码
{
  "property": {
    "source": "commandLineArgs",
    "value": "9090"
  },
  "propertySources": [
    {
      "name": "commandLineArgs",
      "property": {
        "value": "9090",
        "origin": "..."
      }
    },
    {
      "name": "application-prod.yml",
      "property": {
        "value": "80"
      }
    },
    {
      "name": "application.yml",
      "property": {
        "value": "8080"
      }
    }
  ]
}

方法3:显式声明配置来源

java 复制代码
@Component
public class ConfigDebugger implements CommandLineRunner {
    
    @Autowired
    private Environment env;
    
    @Override
    public void run(String... args) {
        // 打印配置来源
        String port = env.getProperty("server.port");
        System.out.println("server.port = " + port);
        
        // 获取配置源
        MutablePropertySources sources = 
            ((ConfigurableEnvironment) env).getPropertySources();
        
        for (PropertySource<?> source : sources) {
            System.out.println("配置源:" + source.getName());
        }
    }
}

最佳实践:

  1. 开发环境:使用默认配置(application.yml)
  2. 测试/生产:通过profile或命令行参数覆盖
  3. 敏感信息:使用环境变量(最高优先级,最安全)
  4. 调试冲突:使用Actuator端点查看实际生效值

下一篇预告: 《自定义Starter开发实战》- 从零开发企业级Starter,掌握AutoConfiguration自动装配机制。

相关推荐
廋到被风吹走17 小时前
【Spring】Spring Boot Starter设计:公司级监控SDK实战指南
java·spring boot·spring
hqiangtai17 小时前
Android 高级专家技术能力图谱
android·职场和发展
秋饼17 小时前
【手撕 @EnableAsync:揭秘 SpringBoot @Enable 注解的魔法开关】
java·spring boot·后端
aqi0017 小时前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
IT_陈寒17 小时前
Python 3.12 新特性实战:这5个改进让我的开发效率提升40%
前端·人工智能·后端
利兄的视界17 小时前
一步到位:M4 芯片 Mac 安装 PostgreSQL 16 并适配 pgvector 教程
后端·postgresql
GZKING17 小时前
ThinkPHP 8 报错"think\model\pivot" not found
后端
stevenzqzq17 小时前
Android Koin 注入入门教程
android·kotlin
Smoothzjc18 小时前
👉 求你了,别再裸写 fetch 做 AI 流式响应了!90% 的人都在踩这个坑
前端·人工智能·后端