Aplikasi yang sama hampir selalu hidup di lebih dari satu lingkungan: mesin pengembang Budi, server staging, dan cluster produksi. Nilai yang berubah antar lingkungan — URL basis data, kredensial, tingkat log — tidak boleh mengubah kode, sehingga Spring menyediakan dua mekanisme pelengkap: externalized configuration untuk memisahkan nilai dari kode, dan profile untuk memilih kumpulan bean per lingkungan. Pada Chapter ini kita pelajari anotasi @Profile, aktivasi lewat spring.profiles.active, lalu naik kelas ke konfigurasi type-safe dengan @ConfigurationProperties.

Memilih Bean dengan @Profile #

Anotasi @Profile ditempelkan pada method @Bean atau kelas komponen untuk membatasi kapan definisi itu aktif. Container mengevaluasi set profil yang aktif saat startup; definisi yang profilnya tidak cocok tidak diproses sama sekali — bean tidak dibuat, seolah definisinya tidak ada. Berikut pola klasiknya, ditampilkan apa adanya untuk Spring Boot 3.4/3.5:

java
@Configuration
public class KonfigurasiDataSource {

    @Bean
    @Profile("dev")
    public DataSource dataSourceDev() {
        return DataSourceBuilder.create()
                .url("jdbc:h2:mem:devdb")
                .username("sa")
                .build();
    }

    @Bean
    @Profile("prod")
    public DataSource dataSourceProd() {
        return DataSourceBuilder.create()
                .url(System.getenv("DB_URL"))
                .username(System.getenv("DB_USER"))
                .password(System.getenv("DB_PASSWORD"))
                .build();
    }
}

Profil diaktifkan dari luar kode lewat property spring.profiles.active. Cara umumnya: argumen command line --spring.profiles.active=prod, variabel lingkungan SPRING_PROFILES_ACTIVE, atau berkas application-{profil}.properties. Nilai bisa berisi beberapa profil sekaligus dipisah koma, misalnya dev,dblokal; anotasi juga menerima negasi @Profile("!prod") untuk “bukan produksi”.

Simulasi Pemilihan Profil #

Logika pencocokan profil layak dilihat secara mekanis. Program Java murni berikut menyimpan kandidat definisi bean beserta profilnya, lalu mengevaluasi tiga nilai spring.profiles.active yang berbeda:

import java.util.List;

public class ProfilLingkungan {

    // Definisi bean kandidat: profil kosong berarti berlaku untuk semua
    // lingkungan (default), meniru anotasi @Profile pada method @Bean.
    record DefinisiBean(String nama, List<String> profil, String url, String keterangan) {
        boolean cocokUntuk(String profilAktif) {
            return profil.isEmpty() || profil.contains(profilAktif);
        }
    }

    private static final List<DefinisiBean> KANDIDAT = List.of(
            new DefinisiBean("dataSourceDev", List.of("dev"),
                    "jdbc:h2:mem:devdb", "basis data in-memory, cepat, data dibuang tiap restart"),
            new DefinisiBean("dataSourceProd", List.of("prod"),
                    "jdbc:postgresql://db.example.com/produksi", "basis data produksi, koneksi terkelola"),
            new DefinisiBean("dataSourceDefault", List.of(),
                    "jdbc:h2:mem:default", "cadangan ketika tidak ada profil yang cocok"));

    public static void main(String[] args) {
        pilihDanCetak("dev");
        pilihDanCetak("prod");
        pilihDanCetak("staging");
    }

    static void pilihDanCetak(String profilAktif) {
        System.out.println("spring.profiles.active = " + profilAktif);

        DefinisiBean terpilih = null;
        for (DefinisiBean k : KANDIDAT) {
            if (!k.profil().isEmpty() && k.cocokUntuk(profilAktif)) {
                terpilih = k;
                break;
            }
        }
        if (terpilih == null) {
            terpilih = KANDIDAT.get(KANDIDAT.size() - 1);
            System.out.println("  tidak ada profil cocok -> definisi default dipakai");
        }

        System.out.println("  bean    : " + terpilih.nama());
        System.out.println("  url     : " + terpilih.url());
        System.out.println("  catatan : " + terpilih.keterangan());
        System.out.println();
    }
}

Output yang Dihasilkan:

text
spring.profiles.active = dev
  bean    : dataSourceDev
  url     : jdbc:h2:mem:devdb
  catatan : basis data in-memory, cepat, data dibuang tiap restart

spring.profiles.active = prod
  bean    : dataSourceProd
  url     : jdbc:postgresql://db.example.com/produksi
  catatan : basis data produksi, koneksi terkelola

spring.profiles.active = staging
  tidak ada profil cocok -> definisi default dipakai
  bean    : dataSourceDefault
  url     : jdbc:h2:mem:default
  catatan : cadangan ketika tidak ada profil yang cocok

Mari kita bedah aturan yang diimplementasikan simulasi ini:

  1. record DefinisiBean(...) dengan cocokUntuk(profilAktif): definisi berisi daftar profil dan dianggap cocok bila daftar memuat profil aktif. Daftar profil kosong berarti definisi default yang berlaku bila tidak ada kandidat lain — meniru bean tanpa @Profile yang selalu tersedia.
  2. Loop pencocokan di pilihDanCetak: kandidat diperiksa berurutan dan yang pertama cocok menang; urutan deklarasi menentukan prioritas, sama seperti urutan method @Bean dalam satu kelas @Configuration.
  3. Kasus staging: tidak ada kandidat yang profilnya cocok, sehingga definisi default yang dipilih. Dalam Spring nyata, keadaan ini menghasilkan bean tanpa profil — perilaku yang persis kita lihat pada baris tidak ada profil cocok -> definisi default dipakai.

Konfigurasi Type-Safe dengan @ConfigurationProperties #

Menyeret nilai satu-satu dengan @Value("${...}") cepat menjadi rapuh: tidak ada pemeriksaan tipe saat kompilasi, tidak ada dokumentasi terkumpul, dan salah ketik kunci baru terasa saat runtime. Anotasi @ConfigurationProperties menawarkan jalur yang lebih disiplin — kelompok properti diikat ke satu kelas, dan sejak Spring Boot 3 kelas itu bisa berupa record:

java
@ConfigurationProperties(prefix = "app")
public record PropertiAplikasi(
        String nama,
        boolean maintenance,
        int batasUnggahMb,
        List<String> hostDiizinkan
) { }
properties
app.nama=Layanan Pesanan
app.maintenance=false
app.batasi-unggah-mb=5

Perhatikan pemetaan batasi-unggah-mb ke komponen batasUnggahMb — mekanisme relaxed binding Spring menerjemahkan tanda hubung dan huruf besar secara konsisten. Kombinasi keduanya diaktifkan dengan @EnableConfigurationProperties(PropertiAplikasi.class) atau anotasi @ConfigurationPropertiesScan. Hasilnya: seluruh konfigurasi terpusat pada satu tipe yang bisa diinjeksikan sebagai bean biasa, lengkap dengan dukungan validasi Bean Validation dan metadata yang muncul di editor IDE.

Matriks @Value vs @ConfigurationProperties #

Aspek@Value@ConfigurationProperties
Bentuksatu field per propertisatu tipe per kelompok
Pemeriksaan tipesaat runtimesaat runtime, namun terkumpul di satu tipe
Relaxed bindingterbataspenuh, termasuk tanda hubung
Validasi Bean Validationtidakbisa dengan @Validated
Dukungan IDE metadatatidaktersedia lewat processor

Profil menyeleksi bean, sedangkan property menyuplai nilai. Keduanya bertemu di berkas application.properties beserta variannya per profil — topik yang kita bahas dalam-dalam pada Chapter Application Properties setelah kita memahami cara Spring Boot memangkas seluruh konfigurasi boilerplate ini lewat auto-configuration.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ProfilLingkungan.java

Unduh berkas
java
import java.util.List;

public class ProfilLingkungan {

    // Definisi bean kandidat: profil kosong berarti berlaku untuk semua
    // lingkungan (default), meniru anotasi @Profile pada method @Bean.
    record DefinisiBean(String nama, List<String> profil, String url, String keterangan) {
        boolean cocokUntuk(String profilAktif) {
            return profil.isEmpty() || profil.contains(profilAktif);
        }
    }

    private static final List<DefinisiBean> KANDIDAT = List.of(
            new DefinisiBean("dataSourceDev", List.of("dev"),
                    "jdbc:h2:mem:devdb", "basis data in-memory, cepat, data dibuang tiap restart"),
            new DefinisiBean("dataSourceProd", List.of("prod"),
                    "jdbc:postgresql://db.example.com/produksi", "basis data produksi, koneksi terkelola"),
            new DefinisiBean("dataSourceDefault", List.of(),
                    "jdbc:h2:mem:default", "cadangan ketika tidak ada profil yang cocok"));

    public static void main(String[] args) {
        pilihDanCetak("dev");
        pilihDanCetak("prod");
        pilihDanCetak("staging");
    }

    static void pilihDanCetak(String profilAktif) {
        System.out.println("spring.profiles.active = " + profilAktif);

        DefinisiBean terpilih = null;
        for (DefinisiBean k : KANDIDAT) {
            if (!k.profil().isEmpty() && k.cocokUntuk(profilAktif)) {
                terpilih = k;
                break;
            }
        }
        if (terpilih == null) {
            terpilih = KANDIDAT.get(KANDIDAT.size() - 1);
            System.out.println("  tidak ada profil cocok -> definisi default dipakai");
        }

        System.out.println("  bean    : " + terpilih.nama());
        System.out.println("  url     : " + terpilih.url());
        System.out.println("  catatan : " + terpilih.keterangan());
        System.out.println();
    }
}