🌍 Spring的Environment:配置界的"特工007"!

34 阅读6分钟

副标题:所有配置信息尽在我掌握! 🕵️‍♂️


🎬 开场白:配置都从哪儿来?

嘿,小伙伴们!👋 今天我们要聊一个Spring中超级强大的抽象——Environment!

想象这个场景:

  • ⚙️ 配置文件里有配置
  • 🔧 系统环境变量里有配置
  • 💻 JVM参数里有配置
  • 🎯 命令行参数里也有配置

如果配置冲突了怎么办?谁的优先级最高?

别慌!Spring的Environment就像一个"特工007"🕵️‍♂️,帮你管理所有配置!


📚 第一幕:什么是Environment?

核心概念

Environment是Spring 3.1引入的一个抽象,它代表了应用程序运行时的环境,包含两个关键方面:

  1. Profiles(环境配置):dev、test、prod等不同环境
  2. Properties(属性配置):所有的配置信息
public interface Environment extends PropertyResolver {
    
    // Profile相关
    String[] getActiveProfiles();
    String[] getDefaultProfiles();
    boolean acceptsProfiles(String... profiles);
    
    // 继承自PropertyResolver的方法
    boolean containsProperty(String key);
    String getProperty(String key);
    String getProperty(String key, String defaultValue);
    <T> T getProperty(String key, Class<T> targetType);
}

生活比喻:

Environment就像你的智能管家🤵,他知道:

  • 你家的环境(Profile):是工作日还是周末?
  • 你的偏好(Properties):喜欢喝咖啡还是茶?

🎪 第二幕:配置来源大揭秘

PropertySource - 配置源

每个配置来源都是一个PropertySource

public abstract class PropertySource<T> {
    
    protected final String name;  // 配置源名称
    protected final T source;     // 配置源对象
    
    // 获取配置值
    public abstract Object getProperty(String name);
}

Spring Boot的配置优先级(从高到低)

优先级配置源示例图标
1命令行参数java -jar app.jar --server.port=9090🎯
2SPRING_APPLICATION_JSON环境变量中的JSON配置📋
3ServletConfig初始化参数Web应用配置🌐
4ServletContext初始化参数Web容器配置🏪
5JNDI属性java:comp/env🔧
6JVM系统属性-Dserver.port=9090💻
7操作系统环境变量export SERVER_PORT=9090🖥️
8RandomValuePropertySource${random.int}🎲
9jar包外的application-{profile}.properties外部配置文件📦
10jar包内的application-{profile}.properties内部配置文件📄
11jar包外的application.properties默认外部配置📦
12jar包内的application.properties默认内部配置📄
13@PropertySource注解的配置自定义配置文件🔖
14默认属性SpringApplication.setDefaultProperties⚙️

记忆口诀:

命令行最牛逼 👑
JSON紧跟随 📋
系统环境排中间 💻🖥️
配置文件靠后站 📄
默认属性最底层 ⚙️

🎯 第三幕:实战案例 - 获取配置

方式1:直接注入Environment

@Component
public class ConfigDemo {
    
    @Autowired
    private Environment environment;
    
    public void showConfig() {
        
        // 获取简单配置
        String appName = environment.getProperty("spring.application.name");
        System.out.println("📛 应用名称:" + appName);
        
        // 获取配置(带默认值)
        String port = environment.getProperty("server.port", "8080");
        System.out.println("🔌 端口号:" + port);
        
        // 获取配置(指定类型)
        Integer maxThreads = environment.getProperty(
            "server.tomcat.max-threads", 
            Integer.class, 
            200
        );
        System.out.println("🧵 最大线程数:" + maxThreads);
        
        // 检查配置是否存在
        boolean hasDb = environment.containsProperty("spring.datasource.url");
        System.out.println("🗄️ 配置了数据库?" + hasDb);
    }
}

方式2:使用@Value注解

@Component
public class AppConfig {
    
    // 注入简单值
    @Value("${app.name}")
    private String appName;
    
    // 注入带默认值
    @Value("${app.version:1.0.0}")
    private String appVersion;
    
    // 注入并转换类型
    @Value("${app.max-users:1000}")
    private Integer maxUsers;
    
    // 使用SpEL表达式
    @Value("#{${app.timeout:5000} + 1000}")
    private Long timeout;
    
    // 注入数组
    @Value("${app.allowed-ips:127.0.0.1,192.168.1.1}")
    private String[] allowedIps;
    
    @PostConstruct
    public void init() {
        System.out.println("📛 应用名称:" + appName);
        System.out.println("🔢 版本号:" + appVersion);
        System.out.println("👥 最大用户数:" + maxUsers);
        System.out.println("⏱️ 超时时间:" + timeout + "ms");
        System.out.println("🌐 允许的IP:" + Arrays.toString(allowedIps));
    }
}

方式3:使用@ConfigurationProperties

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    private String name;
    private String version;
    private Integer maxUsers;
    private Database database = new Database();
    private Map<String, String> settings = new HashMap<>();
    
    // 嵌套配置
    public static class Database {
        private String url;
        private String username;
        private String password;
        private Integer poolSize = 10;
        
        // getters and setters...
    }
    
    // getters and setters...
}

// 在application.yml中配置
/*
app:
  name: MyApp
  version: 2.0.0
  max-users: 5000
  database:
    url: jdbc:mysql://localhost:3306/db
    username: root
    password: secret
    pool-size: 20
  settings:
    feature-x: enabled
    feature-y: disabled
*/

🌟 第四幕:Profile - 环境切换

什么是Profile?

Profile就像你的变身术🦸,让应用在不同环境下表现不同!

@Configuration
public class DataSourceConfig {
    
    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        System.out.println("💻 创建开发环境数据源");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:h2:mem:testdb");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }
    
    @Bean
    @Profile("test")
    public DataSource testDataSource() {
        System.out.println("🧪 创建测试环境数据源");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://test-server:3306/testdb");
        dataSource.setUsername("test_user");
        dataSource.setPassword("test_pass");
        return dataSource;
    }
    
    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        System.out.println("🏭 创建生产环境数据源");
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://prod-server:3306/proddb");
        dataSource.setUsername("prod_user");
        dataSource.setPassword("prod_pass");
        dataSource.setMaximumPoolSize(50);
        return dataSource;
    }
}

激活Profile的方式

方式1:配置文件中激活

# application.properties
spring.profiles.active=dev
# application.yml
spring:
  profiles:
    active: dev

方式2:命令行参数

# 方式A:使用--spring.profiles.active
java -jar app.jar --spring.profiles.active=prod

# 方式B:使用-Dspring.profiles.active
java -Dspring.profiles.active=prod -jar app.jar

方式3:环境变量

export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

方式4:编程方式

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        
        // 设置默认Profile
        app.setAdditionalProfiles("dev");
        
        app.run(args);
    }
}

多Profile激活

# 同时激活多个Profile
java -jar app.jar --spring.profiles.active=prod,swagger
spring:
  profiles:
    active: prod,swagger

效果:

  • ✅ 生产环境配置生效
  • ✅ Swagger文档也启用

Profile表达式

// 非dev环境
@Profile("!dev")
public class ProdConfig {
    // ...
}

// dev或test环境
@Profile({"dev", "test"})
public class NonProdConfig {
    // ...
}

// dev并且debug
@Profile("dev & debug")
public class DevDebugConfig {
    // ...
}

// prod或uat
@Profile("prod | uat")
public class ProductionLikeConfig {
    // ...
}

🎨 第五幕:自定义PropertySource

场景:从数据库加载配置

/**
 * 数据库配置源
 */
public class DatabasePropertySource extends PropertySource<Map<String, Object>> {
    
    private final Map<String, Object> properties;
    
    public DatabasePropertySource(String name, JdbcTemplate jdbcTemplate) {
        super(name);
        this.properties = loadPropertiesFromDatabase(jdbcTemplate);
    }
    
    @Override
    public Object getProperty(String name) {
        return properties.get(name);
    }
    
    /**
     * 从数据库加载配置
     */
    private Map<String, Object> loadPropertiesFromDatabase(JdbcTemplate jdbcTemplate) {
        
        String sql = "SELECT config_key, config_value FROM sys_config";
        
        Map<String, Object> props = new HashMap<>();
        
        jdbcTemplate.query(sql, rs -> {
            props.put(rs.getString("config_key"), rs.getString("config_value"));
        });
        
        System.out.println("🗄️ 从数据库加载了 " + props.size() + " 个配置");
        
        return props;
    }
}

注册自定义PropertySource

@Configuration
public class CustomPropertySourceConfig implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        
        // 创建数据源(这里简化处理)
        JdbcTemplate jdbcTemplate = createJdbcTemplate();
        
        // 创建自定义配置源
        DatabasePropertySource databasePropertySource = 
            new DatabasePropertySource("databaseConfig", jdbcTemplate);
        
        // 添加到配置源列表(最高优先级)
        propertySources.addFirst(databasePropertySource);
        
        System.out.println("✅ 已注册数据库配置源");
    }
    
    private JdbcTemplate createJdbcTemplate() {
        // 创建数据源...
        return new JdbcTemplate(dataSource);
    }
}

在spring.factories中注册

# src/main/resources/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.example.config.CustomPropertySourceConfig

🔧 第六幕:占位符解析

${} 占位符

app:
  name: MyApp
  version: 1.0.0
  full-name: ${app.name} v${app.version}
  
server:
  port: 8080
  url: http://localhost:${server.port}

解析后:

app.full-name = MyApp v1.0.0
server.url = http://localhost:8080

占位符默认值

@Value("${app.timeout:5000}")
private Long timeout;  // 如果没配置,默认5000

@Value("${app.feature.enabled:false}")
private Boolean featureEnabled;  // 默认false

@Value("${app.max-retries:#{3}}")
private Integer maxRetries;  // 使用SpEL表达式作为默认值

复杂占位符解析

app:
  env: ${ENVIRONMENT:dev}
  db:
    host: ${DB_HOST:localhost}
    port: ${DB_PORT:3306}
    name: ${DB_NAME:mydb}
    url: jdbc:mysql://${app.db.host}:${app.db.port}/${app.db.name}

🎪 第七幕:实战场景

场景1:多环境配置文件

文件结构:

src/main/resources/
├── application.yml           # 公共配置
├── application-dev.yml       # 开发环境
├── application-test.yml      # 测试环境
└── application-prod.yml      # 生产环境

application.yml(公共配置)

spring:
  application:
    name: my-app
  profiles:
    active: @spring.profiles.active@  # Maven占位符

app:
  common-config: 这是所有环境共享的配置

application-dev.yml(开发环境)

server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver

logging:
  level:
    root: DEBUG
    com.example: TRACE

app:
  env-specific: 这是开发环境的配置

application-prod.yml(生产环境)

server:
  port: 80

spring:
  datasource:
    url: jdbc:mysql://prod-server:3306/proddb
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

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

app:
  env-specific: 这是生产环境的配置

场景2:配置加密

@Configuration
public class EncryptedPropertyConfig {
    
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyConfigurer() {
        PropertySourcesPlaceholderConfigurer configurer = 
            new PropertySourcesPlaceholderConfigurer();
        
        // 设置占位符解析器
        configurer.setPlaceholderPrefix("${");
        configurer.setPlaceholderSuffix("}");
        
        // 设置值解析器(解密)
        configurer.setValueResolver(placeholderName -> {
            // 如果是加密的值,解密
            if (placeholderName.startsWith("encrypted:")) {
                String encryptedValue = placeholderName.substring(10);
                return decrypt(encryptedValue);
            }
            return null;
        });
        
        return configurer;
    }
    
    private static String decrypt(String encrypted) {
        // 实现解密逻辑
        System.out.println("🔓 解密配置:" + encrypted);
        return "decrypted_" + encrypted;
    }
}

使用:

app:
  password: ${encrypted:MTIzNDU2}

场景3:动态刷新配置

@Component
@RefreshScope  // 支持配置刷新
public class DynamicConfig {
    
    @Value("${app.dynamic-value}")
    private String dynamicValue;
    
    public String getDynamicValue() {
        return dynamicValue;
    }
}

@RestController
public class ConfigController {
    
    @Autowired
    private DynamicConfig dynamicConfig;
    
    @GetMapping("/config")
    public String getConfig() {
        return "当前配置:" + dynamicConfig.getDynamicValue();
    }
}

刷新配置:

# 使用Spring Boot Actuator刷新
curl -X POST http://localhost:8080/actuator/refresh

🔍 第八幕:Environment源码解析

Environment继承体系

Environment (接口)
    ↓
ConfigurableEnvironment (接口)
    ↓
AbstractEnvironment (抽象类)
    ↓
StandardEnvironment (标准实现)
    ↓
StandardServletEnvironment (Web实现)

PropertySource优先级管理

@Component
public class PropertySourceDemo implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    @Override
    public void initialize(ConfigurableApplicationContext context) {
        
        ConfigurableEnvironment environment = context.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        
        // 查看所有PropertySource
        System.out.println("📋 所有配置源:");
        for (PropertySource<?> ps : propertySources) {
            System.out.println("  - " + ps.getName() + " (" + ps.getClass().getSimpleName() + ")");
        }
        
        // 添加自定义配置源(最高优先级)
        Map<String, Object> customProps = new HashMap<>();
        customProps.put("custom.property", "custom value");
        propertySources.addFirst(new MapPropertySource("customConfig", customProps));
        
        // 添加到最低优先级
        Map<String, Object> defaultProps = new HashMap<>();
        defaultProps.put("default.property", "default value");
        propertySources.addLast(new MapPropertySource("defaultConfig", defaultProps));
        
        // 在特定位置添加
        Map<String, Object> middleProps = new HashMap<>();
        middleProps.put("middle.property", "middle value");
        propertySources.addBefore(
            "systemProperties", 
            new MapPropertySource("middleConfig", middleProps)
        );
    }
}

⚠️ 第九幕:常见坑点

坑点1:配置不生效

# ❌ 错误:缩进不对
spring:
profiles:
  active: dev

# ✅ 正确:
spring:
  profiles:
    active: dev

坑点2:占位符循环引用

# ❌ 错误:循环引用
app:
  value1: ${app.value2}
  value2: ${app.value1}

# ✅ 正确:
app:
  base-value: hello
  value1: ${app.base-value}_world
  value2: ${app.value1}_!

坑点3:Profile文件名错误

❌ 错误命名:
- application_dev.yml
- application.dev.yml
- applicationDev.yml

✅ 正确命名:
- application-dev.yml
- application-test.yml
- application-prod.yml

坑点4:环境变量命名规则

# ❌ 错误:环境变量不支持小写和点
export spring.datasource.url=jdbc:mysql://...

# ✅ 正确:使用大写和下划线
export SPRING_DATASOURCE_URL=jdbc:mysql://...

转换规则:

spring.datasource.url → SPRING_DATASOURCE_URL
server.port → SERVER_PORT
app.max-users → APP_MAX_USERS (或 APP_MAXUSERS)

🎯 第十幕:最佳实践

✅ 1. 配置分层

公共配置
    ↓
环境配置(dev/test/prod)
    ↓
本地配置(application-local.yml,不提交到Git)

.gitignore

application-local.yml
application-local.properties

✅ 2. 敏感信息外部化

# ❌ 不要把密码写在配置文件里!
spring:
  datasource:
    password: mySecretPassword

# ✅ 使用环境变量
spring:
  datasource:
    password: ${DB_PASSWORD}

✅ 3. 使用ConfigurationProperties代替@Value

// ❌ 不推荐:配置分散
@Component
public class AppConfig {
    @Value("${app.name}")
    private String name;
    
    @Value("${app.version}")
    private String version;
    
    @Value("${app.timeout}")
    private Long timeout;
}

// ✅ 推荐:集中管理
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String version;
    private Long timeout;
    // getters and setters
}

✅ 4. 配置校验

@Component
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
    
    @NotBlank(message = "应用名称不能为空")
    private String name;
    
    @Min(value = 1, message = "超时时间至少1秒")
    @Max(value = 3600, message = "超时时间最多3600秒")
    private Long timeout;
    
    @Email(message = "邮箱格式不正确")
    private String adminEmail;
    
    // getters and setters
}

🎉 总结

Environment核心功能

功能说明图标
Profile管理多环境配置切换🌍
属性管理统一管理所有配置⚙️
优先级控制灵活的配置覆盖🎯
占位符解析配置间引用🔗
扩展性自定义配置源🔧

配置优先级记忆口诀

命令行最高 🎯
系统环境次之 💻
配置文件再次 📄
默认最低 ⚙️

🚀 课后作业

  1. 初级: 配置多环境(dev/test/prod)并切换
  2. 中级: 实现一个从Redis加载配置的PropertySource
  3. 高级: 实现配置动态刷新机制

📚 参考资料

  • Spring Framework官方文档 - Environment抽象
  • Spring Boot官方文档 - Externalized Configuration
  • 《Spring Boot实战》

最后的彩蛋: 🎁

Spring的Environment就像一个"配置管家"🤵,他:

  • 📋 知道所有配置的来源
  • 🎯 知道哪个配置优先级最高
  • 🌍 知道当前是什么环境
  • 🔍 能帮你找到任何配置

记住这句话:

"配置虽多,Environment帮你理清头绪!" 💡


关注我,下期更精彩! 🌟

用Environment掌控所有配置! ⚙️✨


#Spring #Environment #配置管理 #最佳实践