乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@Profile 详解

本章定位:深入理解 Spring 的多环境配置隔离机制。@Profile 让同一套代码在不同运行环境(开发、测试、生产)下加载不同的 Bean 定义,是实现"一份代码,多处部署"的关键工具。


定义与作用

@Profile 是 Spring 提供的环境限定注解,用于指定某个 Bean 或配置类仅在特定的环境配置(profile)激活时才被注册到容器中。

解决的痛点:在软件开发生命周期中,不同环境需要不同的基础设施配置:

环境数据库日志级别缓存邮件发送
开发(dev)嵌入式 H2DEBUG本地 Caffeine打印到控制台
测试(test)内存数据库DEBUG本地 Caffeine捕获到文件
生产(prod)MySQL 集群WARNRedis 集群真实 SMTP 网关

在没有 @Profile 之前,开发者需要维护多套配置文件或大量 if/else 判断:

// 痛点:环境判断逻辑混杂在配置中
@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        String env = System.getProperty("env");
        if ("dev".equals(env)) {
            return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
        } else if ("prod".equals(env)) {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:mysql://prod-db:3306/student_db");
            return new HikariDataSource(config);
        }
        throw new IllegalStateException("未知环境");
    }
}

@Profile 让环境判断声明化,不同环境的 Bean 定义各自独立,容器根据激活的 profile 自动选择:

// 解决后:环境配置分离,声明化切换
@Configuration
public class AppConfig {

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

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return new HikariDataSource();
    }
}

适用位置与常用属性

适用位置

@Profile 可以标注在类级别和方法级别。

类级别:整个配置类仅在指定 profile 激活时生效。

@Configuration
@Profile("dev")
public class DevConfig {
    // 该类中的所有 @Bean 仅在 dev 环境注册
}

方法级别:单个 @Bean 方法仅在指定 profile 激活时生效。

@Configuration
public class AppConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() { ... }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() { ... }
}

常用属性

属性类型默认值说明
valueString[]{}指定生效的 profile 名称。支持多个,任一匹配即生效

逻辑表达式(Spring 5.1+)

从 Spring 5.1 开始,@Profile 支持逻辑表达式:

// 仅在 dev 或 test 环境生效
@Profile({"dev", "test"})

// 仅在 dev 且 debug 同时激活时生效(Spring 5.1+)
@Profile("dev & debug")

// 在 dev 环境生效,但 debug 未激活时生效(Spring 5.1+)
@Profile("dev & !debug")

// 在 dev 或 test 环境生效,但 integration 未激活时生效
@Profile("(dev | test) & !integration")

核心原理:Profile 条件注册机制

容器在解析 BeanDefinition 时,会检查 @Profile 条件。只有当前激活的 profile 与 @Profile 声明匹配时,该 BeanDefinition 才会被注册:

激活 Profile 的方式:

  1. 编程式:ctx.getEnvironment().setActiveProfiles("dev")
  2. JVM 参数:-Dspring.profiles.active=dev
  3. 环境变量:SPRING_PROFILES_ACTIVE=dev
  4. web.xml(Spring MVC):<context-param><param-name>spring.profiles.active</param-name><param-value>dev</param-value></context-param>

完整示例:飞翔科技公司的多环境部署

场景简述

飞翔科技的学生管理系统需要在三种环境下运行:开发环境(小崔本地开发)、测试环境(QA 验证)、生产环境(线上服务)。架构师白歌要求使用 @Profile 隔离不同环境的数据源、日志和通知配置。

操作前:环境判断硬编码在配置类中

// 操作前:AppConfig.java —— 环境判断混杂,难以维护
@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        String env = System.getProperty("env", "dev");
        switch (env) {
            case "dev":
                return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .addScript("classpath:schema.sql")
                    .build();
            case "test":
                HikariConfig testConfig = new HikariConfig();
                testConfig.setJdbcUrl("jdbc:mysql://test-db:3306/student_test");
                testConfig.setMaximumPoolSize(5);
                return new HikariDataSource(testConfig);
            case "prod":
                HikariConfig prodConfig = new HikariConfig();
                prodConfig.setJdbcUrl("jdbc:mysql://prod-db-master:3306/student_db");
                prodConfig.setMaximumPoolSize(50);
                prodConfig.setConnectionTimeout(30000);
                return new HikariDataSource(prodConfig);
            default:
                throw new IllegalArgumentException("未知环境: " + env);
        }
    }

    @Bean
    public EmailSender emailSender() {
        String env = System.getProperty("env", "dev");
        if ("prod".equals(env)) {
            return new SmtpEmailSender("smtp.learnto.cn", 587);
        } else {
            return new ConsoleEmailSender();  // 开发/测试环境打印到控制台
        }
    }
}

痛点:

  1. 环境判断逻辑散落在各个 @Bean 方法中,新增环境需要修改多处
  2. 配置类臃肿,不同环境的配置互相干扰
  3. 无法同时加载多个环境的配置进行组合测试
  4. 环境切换依赖 JVM 系统属性,容易拼写错误

操作后:使用 @Profile 分离环境配置

// 操作后:DevDataSourceConfig.java —— 开发环境
@Configuration
@Profile("dev")
public class DevDataSourceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:data-dev.sql")
            .build();
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
// 操作后:TestDataSourceConfig.java —— 测试环境
@Configuration
@Profile("test")
public class TestDataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://test-db:3306/student_test");
        config.setUsername("test_user");
        config.setPassword("test_pass");
        config.setMaximumPoolSize(5);
        return new HikariDataSource(config);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
// 操作后:ProdDataSourceConfig.java —— 生产环境
@Configuration
@Profile("prod")
public class ProdDataSourceConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.url}") String url,
            @Value("${db.username}") String username,
            @Value("${db.password}") String password) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(50);
        config.setConnectionTimeout(30000);
        config.setLeakDetectionThreshold(60000);
        return new HikariDataSource(config);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
// 操作后:NotificationConfig.java —— 通知配置,按方法级别区分环境
@Configuration
public class NotificationConfig {

    @Bean
    @Profile({"dev", "test"})
    public EmailSender consoleEmailSender() {
        return new ConsoleEmailSender();  // 开发/测试环境:打印到控制台
    }

    @Bean
    @Profile("prod")
    public EmailSender smtpEmailSender(
            @Value("${smtp.host}") String host,
            @Value("${smtp.port}") int port) {
        return new SmtpEmailSender(host, port);  // 生产环境:真实发送
    }
}
// 操作后:AppConfig.java —— 主配置类
@Configuration
@ComponentScan("com.feixiang.student")
@Import({DevDataSourceConfig.class, TestDataSourceConfig.class, ProdDataSourceConfig.class})
public class AppConfig {
}
// 操作后:启动类 —— 按环境启动
public class Main {
    public static void main(String[] args) {
        // 开发环境启动
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().setActiveProfiles("dev");
        ctx.register(AppConfig.class);
        ctx.refresh();

        DataSource ds = ctx.getBean(DataSource.class);
        EmailSender sender = ctx.getBean(EmailSender.class);

        System.out.println("当前环境:dev");
        System.out.println("数据源类型:" + ds.getClass().getSimpleName());
        System.out.println("邮件发送器:" + sender.getClass().getSimpleName());

        sender.send("admin@learnto.cn", "测试", "飞翔科技学生管理系统启动成功");
    }
}

运行结果及分析

开发环境(dev)启动:

当前环境:dev
数据源类型:EmbeddedDatabaseFactory$EmbeddedDataSourceProxy
邮件发送器:ConsoleEmailSender
[ConsoleEmailSender] 收件人:admin@learnto.cn,主题:测试,内容:飞翔科技学生管理系统启动成功

生产环境(prod)启动:

当前环境:prod
数据源类型:HikariDataSource
邮件发送器:SmtpEmailSender
[SmtpEmailSender] 连接到 smtp.learnto.cn:587...
[SmtpEmailSender] 邮件发送成功:admin@learnto.cn

改进点总结:

维度操作前(硬编码判断)操作后(@Profile 声明化)
代码结构环境判断散落在各方法中每个环境独立配置类,结构清晰
可维护性新增环境需修改多处新增环境只需新增一个配置类
可读性需要阅读 if/else 理解环境差异从类名和 @Profile 一目了然
组合测试难以同时加载多套配置可同时激活多个 profile
错误防范字符串拼写错误在运行时暴露profile 名称集中管理,IDE 可检查

@Profile 与 @Component 的结合

@Profile 也可以直接标注在 @Component、@Service、@Repository 等组件类上:

// 仅在 dev 环境注册该组件
@Component
@Profile("dev")
public class DevDataInitializer implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("[Dev] 加载开发环境测试数据...");
    }
}

// 仅在 prod 环境注册该组件
@Component
@Profile("prod")
public class ProdHealthChecker implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("[Prod] 检查生产环境依赖服务...");
    }
}

易错场景与面试考点

反例一:未激活任何 profile 导致 Bean 缺失

小崔启动了容器,但没有设置 active profile,发现 DataSource Bean 不存在:

// ❌ 错误:未激活 profile,所有 @Profile 标注的 Bean 都不注册
public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // 没有设置 active profile!
        DataSource ds = ctx.getBean(DataSource.class);  // 抛异常!
    }
}

启动报错:

NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available

纠正:确保启动时激活了正确的 profile:

// ✅ 正确:显式激活 profile
public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().setActiveProfiles("dev");  // 激活 dev 环境
        ctx.register(AppConfig.class);
        ctx.refresh();
    }
}

反例二:多个同类型 Bean 因 profile 切换导致注入歧义

小崔在 dev 和 prod 环境都定义了 DataSource,但某些配置类没有标注 @Profile,导致启动时注入歧义:

// ❌ 错误:通用配置类中同时存在 profile 和非 profile 的 DataSource
@Configuration
public class MixedConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() { ... }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() { ... }

    @Bean  // 没有 @Profile!所有环境都注册
    public DataSource defaultDataSource() { ... }
}

当激活 dev 时,容器中会有两个 DataSource(devDataSource 和 defaultDataSource),注入时产生歧义。

纠正:确保每个环境有且只有一个同类型 Bean,或使用 @Primary:

// ✅ 正确:每个环境只有一个 DataSource
@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("dev")
    public DataSource dataSource() {  // 统一名称
        return new EmbeddedDatabaseBuilder().build();
    }

    @Bean
    @Profile("prod")
    public DataSource dataSource() {  // 统一名称
        return new HikariDataSource();
    }
}

反例三:@Profile 与 @Conditional 混淆使用

小崔同时使用了 @Profile 和 @ConditionalOnProperty,发现行为不符合预期:

// ❌ 错误:@Profile 和 @Conditional 条件冲突
@Configuration
@Profile("dev")
@ConditionalOnProperty(name = "feature.x.enabled", havingValue = "true")
public class FeatureXConfig {
}

问题:该类需要同时满足"profile = dev"和"feature.x.enabled = true"才会注册。如果开发者只关注 @Profile 而忽略了 @Conditional,会产生困惑。

纠正:统一使用一种条件机制,或明确文档说明:

// ✅ 正确:使用 @Profile 的表达式能力替代简单的 @Conditional
@Configuration
@Profile("dev & feature-x")  // 需要同时激活 dev 和 feature-x 两个 profile
public class FeatureXConfig {
}

面试高频题

Q1:@Profile 和 @Conditional 有什么区别?

@Profile 是 Spring 原生的环境隔离机制,基于 Environment 的 activeProfiles;@Conditional 是更通用的条件注册机制,通过实现 Condition 接口自定义判断逻辑。@Profile 本质上是 @Conditional(ProfileCondition.class) 的快捷方式。对于简单的环境切换,使用 @Profile 更简洁;对于复杂的条件判断(如类路径存在性、属性值组合),使用 @Conditional 更灵活。

Q2:如何同时激活多个 profile?

通过逗号分隔或多次调用 setActiveProfiles:ctx.getEnvironment().setActiveProfiles("dev", "debug"); 或 JVM 参数 -Dspring.profiles.active=dev,debug。多个 profile 之间是"或"关系,只要激活的 profile 中包含 @Profile 声明的任一值,该 Bean 就会注册。

Q3:如果没有激活任何 profile,@Profile("!prod") 会生效吗?

会。@Profile("!prod") 表示"只要 prod 未激活就生效",包括没有激活任何 profile 的情况。这是实现"默认配置"的常用技巧。

Q4:Spring Boot 中 application-dev.properties 和 @Profile 有什么关系?

Spring Boot 的 profile-specific 配置文件(如 application-dev.properties)与 @Profile 是协同关系。当激活 dev profile 时,Spring Boot 会自动加载 application-dev.properties 中的属性,同时 @Profile("dev") 标注的 Bean 也会被注册。两者共同实现"环境隔离",但作用于不同层面:配置文件负责属性值,@Profile 负责 Bean 定义。

上一页
@Import 详解
下一页
@PropertySource 详解