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:
flowchart LR
JAR["1 artifact JAR"]
Dev["dev startup<br/>profile=dev"]
Prod["prod startup<br/>profile=prod"]
DevContainer["DevCache<br/>H2 DataSource<br/>ConsoleExporter"]
ProdContainer["RedisCache<br/>PG DataSource<br/>CloudWatchExporter"]
JAR --> Dev --> DevContainer
JAR --> Prod --> ProdContainerKế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.
flowchart TB
Start["App start<br/>profiles.active=prod,monitoring"]
PropSource["Load application-prod.yml<br/>Load application-monitoring.yml<br/>Merge vao PropertySource chain"]
BeanFilter["Scan @Component / @Bean<br/>@Profile('prod') -> dang ky<br/>@Profile('monitoring') -> dang ky<br/>@Profile('dev') -> SKIP"]
Container["Container san sang<br/>chi co bean match active profile"]
Start --> PropSource --> BeanFilter --> Container3. 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 @Profile tại pha đăng ký bean definition thay vì lúc getBean()? Điều gì xảy ra nếu evaluate muộn hơn?▸
@Profile tại pha đăng ký bean definition thay vì lúc getBean()? Điều gì xảy ra nếu evaluate muộn hơn?@Profile là @Conditional(ProfileCondition.class) — được ConfigurationClassPostProcessor gọi trong bước invokeBeanFactoryPostProcessors của refresh(). Đây là pha đăng ký bean definition vào beanDefinitionMap — trước khi bất kỳ instance nào được tạo.
Nếu evaluate muộn tới lúc getBean(): container đã nhận toàn bộ bean definition vào map, bao gồm cả bean sai profile. Bean A có thể giữ reference tới bean B chỉ có ở profile khác — kết quả là runtime bug ngầm, không bị phát hiện lúc startup.
Evaluate sớm đảm bảo fail-fast: nếu CommonService inject ProdHelper chỉ có ở profile prod nhưng đang chạy profile dev, app không khởi động được — lỗi lộ ra ngay lúc deploy, không phải lúc request đầu tiên lúc 2 giờ sáng.
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?▸
5 cách activate:
spring.profiles.activetrongapplication.yml— mặc định cho developer local.- JVM system property
-Dspring.profiles.active=prod— override lúc chạy JAR. - Environment variable
SPRING_PROFILES_ACTIVE=prod— relaxed binding từ uppercase underscore. - Command line argument
--spring.profiles.active=prod— ưu tiên cao nhất ngoài test. - Test annotation
@ActiveProfiles("test")— chỉ trong test context.
Phổ biến nhất trong container/K8s: env var SPRING_PROFILES_ACTIVE. Lý do: K8s Deployment inject env var qua spec.containers.env hoặc ConfigMap — không cần sửa JAR, không cần sửa config file được bake vào image. Mỗi namespace/cluster/region có env var riêng, cùng một image tag.
application.yml không dùng cho production vì phải rebuild image để đổi profile. Command line arg cũng được nhưng cần sửa entrypoint — env var sạch hơn và tách biệt config khỏi image.
Q3Bean @Component @Profile("prod") được inject vào @Service không có @Profile. Active profile là dev. Điều gì xảy ra? Cách fix?▸
@Component @Profile("prod") được inject vào @Service không có @Profile. Active profile là dev. Điều gì xảy ra? Cách fix?App fail to start với NoSuchBeanDefinitionException.
Lý do từng bước:
@Servicekhông có@Profile→ register ở mọi profile, bao gồmdev.- Service cần inject bean kiểu
ProdHelpervào constructor/field. - Profile
devactive — bean@Profile("prod")không có trongbeanDefinitionMap. - Container không tìm được bean để inject nên throw lúc startup.
Cách fix tốt nhất — interface + 2 implementation:
public interface Helper { ... }
@Component @Profile("prod")
public class ProdHelper implements Helper { ... }
@Component @Profile("!prod")
public class DevHelper implements Helper { ... }
@Service
public class CommonService {
@Autowired Helper helper; // moi profile co implementation
}Code business inject qua interface — không biết implementation nào đang chạy. Container chọn đúng theo profile. Không cần null-check, không cần @Autowired(required = false).
Q4Phân biệt bean không có @Profile và bean có @Profile("default"). Cho ví dụ minh hoạ với bảng.▸
@Profile và bean có @Profile("default"). Cho ví dụ minh hoạ với bảng.Khác biệt:
- Bean không có
@Profile: register ở mọi trường hợp, kể cả khi không profile nào active. - Bean
@Profile("default"): register chỉ khi không có profile nào active (active profiles rỗng).
Bảng minh hoạ:
| spring.profiles.active | CommonService (no @Profile) | FallbackSeeder (@Profile default) | DevSeeder (@Profile dev) |
|---|---|---|---|
| (empty) | dang ky | dang ky | skip |
| dev | dang ky | skip | dang ky |
| prod | dang ky | skip | skip |
Use case thực tế của @Profile("default"): bean seed data mẫu khi developer chạy local mà không set bất kỳ profile nào — tự động có data để test UI. Khi CI set SPRING_PROFILES_ACTIVE=test, bean này bị skip (không làm bẩn test database).
Lưu ý: nếu set spring.profiles.default=dev trong application.yml, profile dev được coi như active → @Profile("default") bean không đăng ký (vì đã có profile active là dev).
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 (beanDefinitionMap vs singletonObjects).▸
beanDefinitionMap vs singletonObjects).Container Spring tách làm 2 pha rõ ràng (xem bài BeanFactory vs ApplicationContext):
- Pha đăng ký definition —
beanDefinitionMap: metadata (class, scope, dependencies). Chưa có object nào. - Pha tạo instance —
singletonObjects: object thật sau khicreateBean().
@Profile được evaluate ở pha 1, trước khi pha 2 bắt đầu. Điều này có hệ quả quan trọng:
- Container chỉ biết các bean hợp lệ cho profile hiện tại. Không có definition → không thể inject nhầm, không thể tạo nhầm instance.
- Dependency resolution an toàn: khi container đi từng bean trong
beanDefinitionMapđể resolve dependency, mọi bean ở đó đều đã qua lọc profile. Không có "bean zombie" chờ sẵn nhưng sai profile. - Fail-fast tại startup: nếu dependency resolution thất bại (bean inject không tồn tại vì sai profile), exception xảy ra trong
refresh()— app không lên được, không phải runtime NPE sau khi lên.
Nếu filter ở pha tạo instance (singletonObjects), container vẫn phải giữ definition của bean sai profile trong map — dependency graph phức tạp hơn, lỗi cấu hình chỉ lộ ra khi getBean thật sự gọi bean đó.
Bài tiếp theo: Profile groups, multi-doc YAML & patterns
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