Spring Profiles — 1 artifact chạy mọi môi trường
Profile là cơ chế Spring gắn nhãn bean và config theo môi trường. Bài này giải thích tại sao profile tồn tại, 5 cách activate, @Profile trên bean/@Component/@Configuration, default profile, và cơ chế evaluate lúc đăng ký bean (không phải lúc getBean).
TL;DR: Profile là named tag (nhãn môi trường) trong Spring — khi profile prod active, container chỉ đăng ký bean có @Profile("prod") và load file application-prod.yml, skip mọi bean/config không match. 5 cách activate: spring.profiles.active property, JVM -Dspring.profiles.active, env var SPRING_PROFILES_ACTIVE, programmatic setAdditionalProfiles, test @ActiveProfiles. Bean @Profile("default") chỉ active khi không profile nào được bật. Cơ chế bên dưới: @Profile là một Condition được evaluate tại pha đăng ký bean định nghĩa (BeanDefinition) — trước khi bất kỳ instance nào ra đời.
Bài trước (externalized configuration) đã giải quyết "đọc config từ đâu". Bài này trả lời câu hỏi khác: làm sao build 1 artifact JAR duy nhất nhưng chạy đúng trên dev, staging, và prod — nơi cả config lẫn bean phải thay đổi theo môi trường?
1. Vấn đề profile giải quyết — 1 artifact, nhiều môi trường
Trước khi có profile, một app thường rơi vào một trong hai anti-pattern:
Anti-pattern 1 — Build riêng từng môi trường:
mvn package -Pdev → target/app-dev.jar
mvn package -Pprod → target/app-prod.jar
Hai JAR khác nhau → không thể đảm bảo "code test được là code prod chạy" (binary drift).
Anti-pattern 2 — if-else môi trường rải rác trong code:
// SAI — hardcode env check trong business logic
if (System.getenv("ENV").equals("prod")) {
return new RedisCacheManager(factory);
} else {
return new ConcurrentMapCacheManager();
}
Business code biết về môi trường — vi phạm separation of concerns, test khó vì phải mock env var.
Profile giải quyết bằng cách di chuyển logic môi trường ra khỏi code, vào container. Container đọc profile active lúc startup và quyết định bean nào đăng ký — code business không biết đang chạy ở đâu:
Kết quả: cùng 1 JAR, container ghép đúng bean theo môi trường. CI build 1 lần → ship lên mọi môi trường.
2. Profile là gì — về mặt cơ chế
Profile trong Spring là một named string thuộc Environment. Khi app khởi động, Environment giữ 2 tập:
- Active profiles — danh sách profile đang bật, vd
["prod", "monitoring"] - Default profiles — dùng khi active profiles rỗng, mặc định là
["default"]
Hai thứ xảy ra khi profile active:
- Config files — Spring Boot load thêm
application-{profile}.yml(cụ thể là tạo thêmPropertySource— xem Environment & PropertySource). - Bean registration — bean có
@Profilematching được đưa vàobeanDefinitionMap; bean không match thì bị bỏ qua hoàn toàn, không tồn tại trong container.
Ý thứ hai là ý dễ hiểu nhầm nhất, nên đặt hai lần khởi động cùng một JAR cạnh nhau:

Thứ khác nhau giữa hai lần khởi động không phải là "bean nào đang bật" mà là nội dung beanDefinitionMap. Bean không match không phải bean bị tắt — nó không có định nghĩa nào trong container, nên inject nó là app không khởi động được chứ không phải nhận về null.
3. 5 cách activate profile
3.1 Property spring.profiles.active
Trong application.yml — đặt default cho developer local:
spring:
profiles:
active: dev
Override khi chạy JAR (command line argument — ưu tiên cao hơn application.yml):
java -jar app.jar --spring.profiles.active=prod
3.2 Environment variable SPRING_PROFILES_ACTIVE
Spring Boot dùng relaxed binding — SPRING_PROFILES_ACTIVE (uppercase underscore) map tới spring.profiles.active (lowercase dot):
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
Pattern phổ biến nhất trong container/K8s — env var không cần sửa JAR hay config file:
# K8s Deployment
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
3.3 JVM system property -D
java -Dspring.profiles.active=prod -jar app.jar
Khác với command line argument --spring.profiles.active=prod: JVM system property (-D) được đọc bởi System.getProperty(), trong khi command line arg (--) được Spring Boot parse riêng. Cả hai đều override application.yml.
3.4 Programmatic — trước khi app start
public static void main(String[] args) {
SpringApplication app = new SpringApplication(App.class);
app.setAdditionalProfiles("prod"); // them vao, khong replace
app.run(args);
}
Hoặc thao tác trực tiếp trên ConfigurableEnvironment:
ConfigurableEnvironment env = app.run(args).getEnvironment();
// Sau khi start, doc active profiles:
String[] active = env.getActiveProfiles();
setAdditionalProfiles("prod") thêm vào danh sách hiện có. setActiveProfiles("prod") replace toàn bộ. Dùng Additional khi muốn giữ profile đã set từ env var.
3.5 Test — @ActiveProfiles
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceIntegrationTest {
@Autowired OrderService service;
@Test
void placeOrderSuccess() { ... }
}
@ActiveProfiles chỉ có tác dụng trong test context — không ảnh hưởng production code. Profile test thường đi kèm application-test.yml trong src/test/resources chứa Testcontainers JDBC URL hoặc H2 in-memory.
Thứ tự ưu tiên activate (cao xuống thấp):
| Nguồn | Ví dụ |
|---|---|
Test @ActiveProfiles | @ActiveProfiles("test") |
| Command line argument | --spring.profiles.active=prod |
| JVM system property | -Dspring.profiles.active=prod |
| Environment variable | SPRING_PROFILES_ACTIVE=prod |
application.yml | spring.profiles.active: dev |
4. @Profile trên bean — cơ chế evaluate
4.1 @Profile trên @Bean method
@Configuration
public class CacheConfig {
@Bean
@Profile("dev")
public CacheManager devCache() {
return new ConcurrentMapCacheManager(); // in-memory, khong can Redis
}
@Bean
@Profile("prod")
public CacheManager prodCache(RedisConnectionFactory factory) {
return new RedisCacheManager(factory); // Redis cho prod
}
}
Khi dev active: devCache vào beanDefinitionMap, prodCache bị skip. Business code inject CacheManager — không biết là Redis hay in-memory.
4.2 @Profile trên @Component
@Component
@Profile("prod")
public class CloudWatchMetricsExporter implements MetricsExporter { ... }
@Component
@Profile("dev")
public class ConsoleMetricsExporter implements MetricsExporter { ... }
Strategy pattern + profile: inject qua interface MetricsExporter, container chọn implementation đúng theo môi trường.
4.3 @Profile trên @Configuration class
@Configuration
@Profile("prod")
public class ProductionInfraConfig {
@Bean public DataSource dataSource() { ... }
@Bean public CacheManager cache() { ... }
@Bean public MetricsExporter metrics() { ... }
}
Cả class @Configuration — bao gồm tất cả @Bean bên trong — chỉ được load khi profile prod active. Pattern này gom toàn bộ prod infrastructure config vào một chỗ.
5. Cơ chế bên dưới — evaluate lúc đăng ký bean
@Profile không phải annotation Spring tự xử lý theo cách đặc biệt. Nó là một @Conditional cụ thể — meta-annotated với @Conditional(ProfileCondition.class):
// Source rut gon
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class) // <-- day la tat ca
public @interface Profile {
String[] value();
}
ProfileCondition implement Condition:
// ProfileCondition.java (rut gon)
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs =
metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(
Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
Tại sao evaluate lúc đăng ký bean, không phải lúc getBean?
Pha đăng ký bean (beanDefinitionMap) xảy ra trong refresh() — trước khi bất kỳ instance nào tồn tại. ConfigurationClassPostProcessor (một BeanFactoryPostProcessor) scan @Component/@Bean và với mỗi bean gặp annotation @Conditional, nó hỏi Condition.matches(). Nếu false → bean definition không được thêm vào map → bean không tồn tại trong container — không thể inject, không thể getBean.
Ngược lại nếu evaluate lúc getBean, container vẫn có thể inject nhầm dependency "chờ đợi profile" vào bean khác. Evaluate sớm tại pha định nghĩa đảm bảo fail-fast: nếu service A inject bean B chỉ có ở profile prod nhưng đang chạy profile dev — container không khởi động được, lỗi lộ ra ngay lúc deploy.
Để hiểu refresh() và BeanFactoryPostProcessor chi tiết hơn, xem refresh() — 12 bước khởi tạo container.
6. Default profile
Bean không có @Profile được đăng ký ở mọi profile active (kể cả khi không có profile nào). Bean có @Profile("default") chỉ đăng ký khi không profile nào active:
@Component
public class CommonService { ... } // moi profile, kể cả khi khong active gi
@Component
@Profile("default")
public class FallbackDataSeeder { ... } // CHI khi khong co profile active
@Component
@Profile("dev")
public class DevDataSeeder { ... } // chi khi dev active
spring.profiles.active | CommonService | FallbackDataSeeder | DevDataSeeder |
|---|---|---|---|
| (empty) | dang ky | dang ky | skip |
dev | dang ky | skip | dang ky |
prod | dang ky | skip | skip |
spring.profiles.default — profile dùng khi spring.profiles.active rỗng:
spring:
profiles:
default: dev # developer local chay khong set profile, fallback dev
Khi set spring.profiles.default=dev, profile dev được coi như active → @Profile("default") bean không được đăng ký (vì đã có dev active).
Bean @Profile("prod") @Component class ProdHelper {} inject vào @Service CommonService không có profile annotation — app fail khi chạy profile dev vì ProdHelper không tồn tại trong container. Luôn cung cấp implementation cho mọi profile qua interface:
@Component @Profile("prod") class ProdHelper implements Helper { ... }
@Component @Profile("!prod") class DevHelper implements Helper { ... }
@Service class CommonService {
@Autowired Helper helper; // moi profile co implementation
}
7. Liên hệ các bài khác
- 02 — @ConfigurationProperties: profile-specific config trong
application-{profile}.ymlđược inject qua@ConfigurationProperties— profile quyết định file nào được load;@ConfigurationPropertiesquyết định property nào được bind vào object. - Environment & PropertySource: profile là một phần của
Environment—env.getActiveProfiles()trả về danh sách profile active;PropertySourcechain giải thích thứ tự ưu tiên override giữa fileapplication-prod.ymlvà biến env. - refresh() — 12 bước khởi tạo container:
ProfileConditionđược gọi trong bướcinvokeBeanFactoryPostProcessorscủarefresh()— hiểu luồng này giúp debug khi bean@Profilekhông được đăng ký như mong đợi. - 04 — Profile groups & patterns: Spring Boot 2.4+ profile groups (
production = prod + prod-db + prod-cache), multi-document YAML, profile expression!,|,&.
Tóm tắt
- Profile là named tag — container đọc lúc startup để quyết định bean nào đăng ký và file config nào load.
- 5 cách activate theo thứ tự ưu tiên tăng dần:
application.yml→ env var → JVM-D→ command line--→ test@ActiveProfiles. @Profileđặt được trên@Bean,@Component,@Configurationclass — bean không match profile active bị bỏ qua hoàn toàn.- Cơ chế bên dưới:
@Profilelà@Conditional(ProfileCondition.class)— evaluate tại pha đăng ký bean definition (trongrefresh()), không phải lúcgetBean. Evaluate sớm → fail-fast khi config sai profile. - Bean không có
@Profile= universal (mọi profile). Bean@Profile("default")= chỉ khi không profile nào active. - Pattern an toàn: inject qua interface, mỗi profile có implementation riêng — tránh
NoSuchBeanDefinitionException.
Tự kiểm tra
- Q1Vì sao Spring evaluate
@Profiletại pha đăng ký bean definition thay vì lúcgetBean()? Điều gì xảy ra nếu evaluate muộn hơn? - Q2Kể đủ 5 cách activate profile. Cách nào được dùng phổ biến nhất trong môi trường container/K8s và vì sao?
- Q3Bean
@Component @Profile("prod")được inject vào@Servicekhông có@Profile. Active profile làdev. Điều gì xảy ra? Cách fix? - Q4Phân biệt bean không có
@Profilevà bean có@Profile("default"). Cho ví dụ minh hoạ với bảng. - Q5Tại sao Spring chọn cách profile activate bean ở tầng đăng ký định nghĩa thay vì tầng tạo instance? Giải thích theo data structure bên dưới (
beanDefinitionMapvssingletonObjects).
Bài tiếp theo: Profile groups, multi-doc YAML & patterns
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên