场景,我结合实际项目经历,给你讲 2 个最常见的落地案例(覆盖数据源切换、多环境配置加载),每个案例都包含核心代码和场景说明。

案例 1:核心场景 —— 多环境动态切换数据源 

业务背景

电商项目中,开发 / 测试环境用轻量的 H2 内存数据库(快速启动、无需部署),生产环境用 MySQL(稳定、高可用),需要在不修改代码的前提下,根据启动时的环境标识(dev/prod)自动切换数据源 Bean。

核心实现(通过 ApplicationContextAware 获取容器,动态注册数据源 Bean)

java

运行

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;

// 数据源配置类:实现ApplicationContextAware+EnvironmentAware,动态获取容器和环境信息
@Component
public class DynamicDataSourceConfig implements ApplicationContextAware, EnvironmentAware {
    // 1. 保存Spring容器和环境对象
    private ApplicationContext applicationContext;
    private Environment environment;

    // 2. Spring容器初始化时,自动注入ApplicationContext
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 3. 自动注入环境对象(可读取配置文件中的环境标识:spring.profiles.active)
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    // 4. 核心方法:根据环境动态创建并注册数据源Bean
    public DataSource getDynamicDataSource() {
        // 读取当前激活的环境(dev/prod,配置在application.yml中)
        String activeProfile = environment.getActiveProfiles()[0];
        DriverManagerDataSource dataSource = new DriverManagerDataSource();

        // 分支1:开发环境(dev)→ H2数据库
        if ("dev".equals(activeProfile)) {
            dataSource.setDriverClassName("org.h2.Driver");
            dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
            dataSource.setUsername("sa");
            dataSource.setPassword("");
        }
        // 分支2:生产环境(prod)→ MySQL数据库
        else if ("prod".equals(activeProfile)) {
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            // 读取prod环境的配置(application-prod.yml中的参数)
            dataSource.setUrl(environment.getProperty("spring.datasource.url"));
            dataSource.setUsername(environment.getProperty("spring.datasource.username"));
            dataSource.setPassword(environment.getProperty("spring.datasource.password"));
        }

        // 可选:将动态创建的数据源Bean注册到Spring容器(供其他组件使用)
        applicationContext.getBeanFactory().registerSingleton("dynamicDataSource", dataSource);
        return dataSource;
    }
}
配套配置文件(application.yml)

yaml

# 全局配置:指定默认激活环境(dev/prod)
spring:
  profiles:
    active: dev # 开发环境;生产环境启动时改为此值:prod

# 开发环境配置(application-dev.yml)
---
spring:
  config:
    activate:
      on-profile: dev
# 生产环境配置(application-prod.yml)
---
spring:
  config:
    activate:
      on-profile: prod
spring:
  datasource:
    url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/ecommerce?useUnicode=true&characterEncoding=utf8
    username: prod_user
    password: prod_pwd123
场景价值
  • 无需手动修改数据源配置,启动时通过--spring.profiles.active=prod即可切换环境;
  • 开发环境用 H2 无需搭建 MySQL,提升开发效率;生产环境用 MySQL 保证稳定性;
  • 通过 ApplicationContext 将动态创建的数据源注册到容器,其他组件(如 MyBatis)可直接@Autowired使用。

案例 2:扩展场景 —— 多租户系统动态加载租户专属配置

业务背景

SaaS 系统中,不同租户(客户)有专属的配置(如接口超时时间、权限规则),配置文件按租户 ID 分类(tenant_1.yml/tenant_2.yml),需要根据当前请求的租户 ID,从 Spring 容器中动态获取对应配置 Bean。

核心实现

java

运行

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;

@Component
public class TenantConfigLoader implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 根据租户ID动态加载专属配置
    public Map<String, Object> loadTenantConfig(String tenantId) {
        try {
            // 1. 从容器中获取租户配置文件资源(classpath:tenant/tenant_1.yml)
            Resource resource = applicationContext.getResource("classpath:tenant/tenant_" + tenantId + ".yml");
            
            // 2. 读取并解析配置文件
            Yaml yaml = new Yaml();
            try (InputStream inputStream = resource.getInputStream()) {
                Map<String, Object> tenantConfig = yaml.load(inputStream);
                
                // 3. 可选:将租户配置注册为容器中的Bean(供全系统使用)
                applicationContext.getBeanFactory().registerSingleton("tenant_" + tenantId + "_config", tenantConfig);
                
                return tenantConfig;
            }
        } catch (Exception e) {
            throw new RuntimeException("加载租户" + tenantId + "配置失败", e);
        }
    }
}
调用示例(接口层根据租户 ID 动态获取配置)

java

运行

@RestController
@RequestMapping("/api/tenant")
public class TenantController {
    @Autowired
    private TenantConfigLoader configLoader;

    @GetMapping("/config")
    public Map<String, Object> getTenantConfig(@RequestParam String tenantId) {
        // 根据前端传的租户ID,动态加载配置
        return configLoader.loadTenantConfig(tenantId);
    }
}
场景价值
  • 每个租户的配置隔离,新增租户只需添加 yml 文件,无需修改代码;
  • 通过 ApplicationContext 获取容器内的资源加载器,统一管理配置文件路径;
  • 配置 Bean 注册到容器后,可在 Service/DAO 层复用,避免重复读取文件。

总结

  1. 核心场景(数据源切换):通过 ApplicationContext+Environment 获取环境信息,动态创建 / 注册数据源 Bean,实现开发 / 生产环境的无感切换;
  2. 扩展场景(多租户配置):通过 ApplicationContext 获取资源加载器,根据业务参数(租户 ID)动态加载专属配置,适配 SaaS 系统的个性化需求;
  3. 共性价值:都是通过 ApplicationContext “感知” 容器信息,将 “固定配置” 改为 “动态配置”,符合开闭原则,提升系统的扩展性和适配性。

<--------------------------------------------------------------------------------------------------------------------------->

先明确:@Value能做什么,不能做什么
1. @Value能实现的场景(简单参数读取)
如果只是读取单个环境参数(比如激活的环境、数据库地址),@Value完全可以做到,代码更简洁:
java
运行
@Component
public class SimpleDataSourceConfig {
    // 读取激活的环境(spring.profiles.active)
    @Value("${spring.profiles.active}")
    private String activeProfile;

    // 读取MySQL地址(仅prod环境有效)
    @Value("${spring.datasource.url:default_url}") // 加默认值避免参数不存在报错
    private String dbUrl;

    public DataSource getDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        if ("dev".equals(activeProfile)) {
            // 开发环境H2配置
            dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
        } else {
            // 生产环境MySQL配置(用@Value注入的dbUrl)
            dataSource.setUrl(dbUrl);
        }
        return dataSource;
    }
}
2. @Value的局限性(为什么动态配置场景优先用Environment)
在动态数据源配置这类场景中,@Value有 3 个核心短板,而Environment能完美解决:
特性    @Value注解    Environment对象
多环境参数校验    无法判断参数是否存在,只能靠默认值兜底    可通过containsProperty()判断参数是否存在,避免空值报错
多 Profile 读取    只能读取当前激活环境的参数    可读取任意环境(如 prod)的参数,即使当前激活的是 dev
批量参数处理    需逐个注解注入,代码冗余    可批量获取某前缀的所有参数(如spring.datasource.*)
动态 Bean 创建时机    注解注入在 Bean 初始化后完成,若动态创建 Bean 时(初始化阶段)读取,可能为 null    可在setEnvironment()阶段(Bean 初始化早期)获取,无空值风险
结合动态数据源场景,看Environment的不可替代性
场景 1:参数存在性校验(避免配置缺失报错)
生产环境若忘记配置spring.datasource.password,@Value直接注入会抛IllegalArgumentException,而Environment可提前判断:
java
运行
// Environment的优势:参数校验
public DataSource getDynamicDataSource() {
    String activeProfile = environment.getActiveProfiles()[0];
    DriverManagerDataSource dataSource = new DriverManagerDataSource();

    if ("prod".equals(activeProfile)) {
        // 先判断参数是否存在,不存在则抛明确异常
        if (!environment.containsProperty("spring.datasource.password")) {
            throw new RuntimeException("生产环境必须配置spring.datasource.password!");
        }
        // 读取参数(无默认值也不会报错,因为已校验)
        dataSource.setPassword(environment.getProperty("spring.datasource.password"));
    }
    // ... 其他配置
    return dataSource;
}
场景 2:读取任意环境的参数(跨环境配置读取)
比如需要在开发环境中临时读取生产环境的配置模板,@Value做不到,但Environment可以:
java
运行
// Environment的优势:读取非激活环境的参数
public void readProdConfigInDev() {
    // 读取prod环境的数据库地址(即使当前激活的是dev)
    String prodDbUrl = environment.getProperty("spring.datasource.url", "prod", "default_prod_url");
    System.out.println("生产环境数据库地址:" + prodDbUrl);
}
场景 3:批量获取同前缀参数(减少冗余代码)
如果数据源配置有多个参数(url、username、password、driver),@Value需要逐个注入,而Environment可批量处理(结合RelaxedPropertyResolver):
java
运行
import org.springframework.core.env.RelaxedPropertyResolver;

public DataSource getBatchConfigDataSource() {
    RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, "spring.datasource.");
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // 批量读取前缀为spring.datasource的参数,无需逐个@Value注入
    dataSource.setUrl(resolver.getProperty("url"));
    dataSource.setUsername(resolver.getProperty("username"));
    dataSource.setPassword(resolver.getProperty("password"));
    dataSource.setDriverClassName(resolver.getProperty("driver-class-name"));
    return dataSource;
}
场景 4:Bean 初始化早期读取参数(避免 null 值)
@Value的注入时机是Bean 初始化完成后,如果在@PostConstruct或setApplicationContext()等早期方法中读取,可能拿到 null;而Environment通过EnvironmentAware注入,时机更早,无此问题:
java
运行
@Component
public class InitTimeConfig implements EnvironmentAware, ApplicationContextAware {
    @Value("${spring.profiles.active}")
    private String valueProfile; // 初始化早期可能为null

    private String envProfile; // Environment注入的参数,早期可用

    // 1. EnvironmentAware的注入时机(早)
    @Override
    public void setEnvironment(Environment environment) {
        this.envProfile = environment.getProperty("spring.profiles.active");
        System.out.println("Environment读取的环境:" + envProfile); // 正常输出
        System.out.println("@Value读取的环境:" + valueProfile); // 输出null
    }

    // 2. Bean初始化完成后(@Value才生效)
    @PostConstruct
    public void init() {
        System.out.println("@Value初始化后读取:" + valueProfile); // 正常输出
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 动态创建Bean的逻辑通常在这里,此时@Value还未注入,必须用Environment
    }
}
总结:该怎么选?
场景    推荐方案    原因
读取单个简单参数(如端口、地址)    @Value    代码简洁,无需额外实现接口
动态创建 Bean(如数据源)    Environment    注入时机早,可校验参数、批量读取,适配动态逻辑
多环境参数判断 / 跨环境读取    Environment    支持getActiveProfiles()、containsProperty()等高级方法
简单业务逻辑(非配置类)    @Value    减少代码复杂度,符合 “简单场景简单实现” 的原则
核心结论
@Value能实现基础的环境参数注入,但仅限 “单个参数、Bean 初始化完成后” 的场景;
在动态数据源配置中,因为需要早期读取环境、校验参数、批量获取配置,所以Environment对象更适配;
实际项目中,也可以两者结合:简单参数用@Value,复杂环境判断用Environment,兼顾简洁性和灵活性。
简单说:@Value是 “轻量工具”,适合小场景;Environment是 “全功能工具”,适合动态配置这类复杂场景。

<---------------------------------------------------------------------------------------------------------------------->

执行器(Actuator)能动态修改配置并实现热部署

核心注解及使用场景

Spring Boot 实现配置热刷新的核心是@RefreshScope,配合 Actuator 的/refresh端点使用,以下是完整的注解说明和使用方式:

1. 核心注解:@RefreshScope
  • 作用:标记该注解的 Bean 会被放入 “刷新作用域”,当配置发生变化并触发刷新后,Spring 会重新创建该 Bean 实例,从而加载最新的配置值。
  • 适用场景:所有需要动态读取最新配置的 Bean(比如你的租户配置管理器、业务 Service 等)。
  • 注意@RefreshScopeSpring Cloud Context 提供的注解,不是原生 Spring Boot 的,需要引入对应依赖。
2. 辅助配置:开启 Actuator 端点

不需要额外注解,但需要在配置文件中开启refresh端点(Spring Boot 2.x + 默认关闭)。

完整实现步骤(结合你的租户配置场景)

步骤 1:添加依赖(pom.xml)

xml

<!-- Actuator核心依赖:提供配置刷新端点 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- RefreshScope依赖(Spring Cloud Context) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
    <version>4.1.0</version> <!-- 与Spring Boot版本匹配,如Boot 3.2.x对应4.1.x -->
</dependency>
步骤 2:配置 Actuator 端点(application.yml)

yaml

# 暴露refresh端点(2.x+默认只暴露health、info)
management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info # 至少包含refresh
  endpoint:
    refresh:
      enabled: true # 开启refresh端点
步骤 3:在需要热刷新的 Bean 上添加@RefreshScope

结合你之前的租户配置管理器,改造后示例:

java

运行

import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

// 关键:添加@RefreshScope,让该Bean支持配置热刷新
@RefreshScope
@Component
public class TenantConfigManager {
    // 原有缓存映射
    private final Map<String, TenantYmlConfig> tenantConfigCache = new ConcurrentHashMap<>();

    // ========== 新增:支持从全局配置读取租户配置根路径(可动态修改) ==========
    // 这里用@Value读取全局配置,配合@RefreshScope可动态刷新
    @Value("${tenant.config.root-path:classpath:tenants/}")
    private String tenantConfigRootPath;

    // 加载租户配置的方法(使用动态的根路径)
    private TenantYmlConfig loadTenantConfigFromYml(String tenantId) {
        try {
            // 读取动态配置的根路径 + 租户yml文件
            String ymlPath = tenantConfigRootPath + "tenant_" + tenantId + ".yml";
            ClassPathResource resource = new ClassPathResource(ymlPath);
            // 原有解析逻辑...
        } catch (Exception e) {
            throw new RuntimeException("加载租户配置失败", e);
        }
    }

    // 原有getCurrentTenantConfig()等方法...

    // ========== 新增:主动清空指定租户缓存,触发重新加载 ==========
    public void clearTenantConfigCache(String tenantId) {
        tenantConfigCache.remove(tenantId);
    }
}
步骤 4:触发配置热刷新
方式 1:调用 Actuator 的/refresh端点(全局刷新)

bash

运行

# POST请求触发全局配置刷新
curl -X POST http://localhost:8080/actuator/refresh
  • 效果:所有标注@RefreshScope的 Bean 会被重新创建,@Value注入的配置会更新(比如上面的tenant.config.root-path)。
方式 2:结合你的租户场景(精准刷新)

如果只想刷新某个租户的配置,可在业务代码中主动清空缓存,配合@RefreshScope实现:

java

运行

@RestController
@RequestMapping("/tenant")
public class TenantConfigController {
    @Autowired
    private TenantConfigManager tenantConfigManager;

    // 自定义端点:刷新指定租户的配置
    @PostMapping("/refresh/{tenantId}")
    public String refreshTenantConfig(@PathVariable String tenantId) {
        // 1. 清空该租户的配置缓存
        tenantConfigManager.clearTenantConfigCache(tenantId);
        // 2. (可选)调用Actuator的refresh端点,刷新全局配置
        // restTemplate.postForObject("http://localhost:8080/actuator/refresh", null, String.class);
        return "租户" + tenantId + "配置已刷新,下次使用时会重新加载最新yml";
    }
}
步骤 5:验证热刷新效果
  1. 启动应用,调用业务接口,读取到初始配置(如tenant.config.root-path=classpath:tenants/);
  2. 修改application.yml中的tenant.config.root-pathclasspath:new-tenants/
  3. 调用POST /actuator/refresh触发刷新;
  4. 再次调用业务接口,会发现tenantConfigRootPath已更新为新值,实现配置热部署。

扩展:更优雅的动态配置(可选)

如果你的场景是多租户动态配置,除了@RefreshScope,还可以结合:

  • @ConfigurationProperties:比@Value更适合批量配置绑定,且天然支持刷新(配合@RefreshScope);
  • Spring Cloud Config/Nacos/Apollo:分布式配置中心,支持配置推送刷新,无需手动调用/refresh

示例(@ConfigurationProperties + @RefreshScope):

java

运行

@RefreshScope
@ConfigurationProperties(prefix = "tenant.config")
@Component
@Data
public class TenantGlobalConfig {
    // 对应application.yml中的tenant.config.root-path
    private String rootPath = "classpath:tenants/";
    // 其他全局租户配置
    private int cache-expire-seconds = 3600;
}

总结

  1. 核心注解@RefreshScope(必须),标记需要热刷新的 Bean,使其支持配置更新后重新实例化;
  2. 配套配置:Actuator 需暴露refresh端点(通过 yml 配置,无额外注解);
  3. 触发方式:调用POST /actuator/refresh全局刷新,或业务代码主动清空缓存实现精准刷新。

这个方案既满足 Actuator 动态修改配置的需求,又能适配你之前的租户按需加载场景,实现配置的热部署。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐