Spring Core & Boot/Spring Profiles — 1 artifact chạy mọi môi trường
37/41
Bài 37 / 41~12 phútConfig, Profiles & LoggingMiễn phí lượt xem

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 --> ProdContainer

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:

  1. Config files — Spring Boot load thêm application-{profile}.yml (cụ thể là tạo thêm PropertySource — xem Environment & PropertySource).
  2. Bean registration — bean có @Profile matching được đưa vào beanDefinitionMap; 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 --> Container

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 bindingSPRING_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 vs setActiveProfiles

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ồnVí dụ
Test @ActiveProfiles@ActiveProfiles("test")
Command line argument--spring.profiles.active=prod
JVM system property-Dspring.profiles.active=prod
Environment variableSPRING_PROFILES_ACTIVE=prod
application.ymlspring.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()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.activeCommonServiceFallbackDataSeederDevDataSeeder
(empty)dang kydang kyskip
devdang kyskipdang ky
proddang kyskipskip

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).

Pitfall: @Profile trên bean, inject vào common service

Bean @Profile("prod") @Component class ProdHelper {} inject vào @Service CommonService không có profile annotation — app fail khi chạy profile devProdHelper 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; @ConfigurationProperties quyết định property nào được bind vào object.
  • Environment & PropertySource: profile là một phần của Environmentenv.getActiveProfiles() trả về danh sách profile active; PropertySource chain giải thích thứ tự ưu tiên override giữa file application-prod.yml và biến env.
  • refresh() — 12 bước khởi tạo container: ProfileCondition được gọi trong bước invokeBeanFactoryPostProcessors của refresh() — hiểu luồng này giúp debug khi bean @Profile khô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, @Configuration class — bean không match profile active bị bỏ qua hoàn toàn.
  • Cơ chế bên dưới: @Profile@Conditional(ProfileCondition.class) — evaluate tại pha đăng ký bean definition (trong refresh()), không phải lúc getBean. 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

Tự kiểm tra
Q1
Vì 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@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.

Q2
Kể đủ 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:

  1. spring.profiles.active trong application.yml — mặc định cho developer local.
  2. JVM system property -Dspring.profiles.active=prod — override lúc chạy JAR.
  3. Environment variable SPRING_PROFILES_ACTIVE=prod — relaxed binding từ uppercase underscore.
  4. Command line argument --spring.profiles.active=prod — ưu tiên cao nhất ngoài test.
  5. 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.

Q3
Bean @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:

  1. @Service không có @Profile → register ở mọi profile, bao gồm dev.
  2. Service cần inject bean kiểu ProdHelper vào constructor/field.
  3. Profile dev active — bean @Profile("prod") không có trong beanDefinitionMap.
  4. 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).

Q4
Phân biệt bean không có @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.activeCommonService (no @Profile)FallbackSeeder (@Profile default)DevSeeder (@Profile dev)
(empty)dang kydang kyskip
devdang kyskipdang ky
proddang kyskipskip

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).

Q5
Tạ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).

Container Spring tách làm 2 pha rõ ràng (xem bài BeanFactory vs ApplicationContext):

  1. Pha đăng ký definitionbeanDefinitionMap: metadata (class, scope, dependencies). Chưa có object nào.
  2. Pha tạo instancesingletonObjects: object thật sau khi createBean().

@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

Đặt 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