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:
@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:
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 cocokMari kita bedah aturan yang diimplementasikan simulasi ini:
record DefinisiBean(...)dengancocokUntuk(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@Profileyang selalu tersedia.- Loop pencocokan di
pilihDanCetak: kandidat diperiksa berurutan dan yang pertama cocok menang; urutan deklarasi menentukan prioritas, sama seperti urutan method@Beandalam satu kelas@Configuration. - 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 baristidak 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:
@ConfigurationProperties(prefix = "app")
public record PropertiAplikasi(
String nama,
boolean maintenance,
int batasUnggahMb,
List<String> hostDiizinkan
) { }app.nama=Layanan Pesanan
app.maintenance=false
app.batasi-unggah-mb=5Perhatikan 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 |
|---|---|---|
| Bentuk | satu field per properti | satu tipe per kelompok |
| Pemeriksaan tipe | saat runtime | saat runtime, namun terkumpul di satu tipe |
| Relaxed binding | terbatas | penuh, termasuk tanda hubung |
| Validasi Bean Validation | tidak | bisa dengan @Validated |
| Dukungan IDE metadata | tidak | tersedia 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 #
- Spring Boot Reference: Externalized Configuration
- Spring Boot Reference: Profiles
- Spring Framework Reference: The IoC Container
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ProfilLingkungan.java
Unduh berkasimport 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();
}
}