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:

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

Ý 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:

Cùng một JAR khởi động hai lần với profile dev và prod; ProfileCondition trả lời khác nhau nên beanDefinitionMap mỗi bên chỉ chứa một CacheManager, bean không khớp hoàn toàn không có định nghĩa

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 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
0/5 câu đã trả lời
  1. 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?
  2. 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?
  3. 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?
  4. Q4
    Phân biệt bean không có @Profile và bean có @Profile("default"). Cho ví dụ minh hoạ với bảng.
  5. 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).

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

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

Bài tiếp theo

Profile Groups & Production Patterns — gom profile, validate, blue/green