Tampilan
Kenapa penting
Kode yang sama harus berjalan di laptop, di pipeline CI, dan di production — dengan database, port, URL payment gateway, dan kredensial yang berbeda. Kalau nilai-nilai itu ditulis langsung di kode, setiap pindah environment berarti mengubah dan mem-build ulang aplikasi, dan password production ikut masuk ke Git. Spring Boot memisahkan kode dari konfigurasi: satu artefak .jar, banyak environment. Di pekerjaan sehari-hari kamu akan sering membaca application.yml, menambah properti untuk integrasi baru, dan men-debug "kenapa di staging nilainya beda" — bab ini memberi peta untuk semua itu.
Konsep inti
application.properties dan application.yml
Spring Boot otomatis membaca src/main/resources/application.properties atau application.yml. Isinya sama, hanya formatnya berbeda:
properties
server.port=8080
spring.application.name=order-service
payment.gateway.base-url=https://sandbox.pay.example.com
payment.gateway.timeout=5syaml
server:
port: 8080
spring:
application:
name: order-service
payment:
gateway:
base-url: https://sandbox.pay.example.com
timeout: 5sYAML lebih ringkas untuk properti bertingkat, tetapi sensitif terhadap indentasi (pakai spasi, bukan tab). Pilih satu format per project. Bila keduanya ada di lokasi yang sama, .properties didahulukan — sumber kebingungan yang sering terjadi.
Urutan sumber konfigurasi
Spring Boot tidak hanya membaca file. Ia menggabungkan banyak property source, dan sumber berprioritas lebih tinggi menimpa yang lebih rendah. Versi ringkas yang paling sering kamu temui (rendah → tinggi):
| Prioritas | Sumber | Contoh |
|---|---|---|
| 1 | application.yml di dalam jar | nilai default untuk semua environment |
| 2 | application-{profile}.yml di dalam jar | application-prod.yml |
| 3 | application.yml / application-{profile}.yml di luar jar | file di folder kerja server |
| 4 | Environment variable OS | SERVER_PORT=8081 |
| 5 | Java system property | java -Dserver.port=8082 -jar app.jar |
| 6 | Argumen command line | java -jar app.jar --server.port=9090 |
| 7 | Properti khusus test (@TestPropertySource, @SpringBootTest) | hanya saat test |
Logikanya mirip mengisi Map berurutan: nilai yang ditulis belakangan menang. Program kecil ini meniru cara Boot menentukan nilai efektif:
java
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class PropertyPrecedence {
record Source(String name, Map<String, String> values) {}
public static void main(String[] args) {
// Urutan dari prioritas TERENDAH ke TERTINGGI, meniru Spring Boot
List<Source> sources = List.of(
new Source("application.yml", Map.of("server.port", "8080", "payment.timeout", "5s")),
new Source("application-prod.yml", Map.of("payment.timeout", "3s")),
new Source("env var", Map.of("server.port", "8081")),
new Source("command line", Map.of("server.port", "9090")));
Map<String, String> effective = new LinkedHashMap<>();
Map<String, String> origin = new LinkedHashMap<>();
for (Source source : sources) {
for (Map.Entry<String, String> e : source.values().entrySet()) {
effective.put(e.getKey(), e.getValue()); // sumber belakangan menimpa yang lebih awal
origin.put(e.getKey(), source.name());
}
}
for (String key : List.of("server.port", "payment.timeout")) {
System.out.println(key + " = " + effective.get(key) + " (dari " + origin.get(key) + ")");
}
}
}Output:
text
server.port = 9090 (dari command line)
payment.timeout = 3s (dari application-prod.yml)Inilah alasan pola umum di industri: file di repo berisi default yang aman, sedangkan nilai khusus environment (terutama secret) disuntikkan lewat environment variable oleh platform deploy (Docker, Kubernetes, CI).
Relaxed binding
Nama properti yang sama bisa ditulis dalam beberapa bentuk, dan Spring Boot tetap mengenalinya. Bentuk kanonis adalah kebab-case huruf kecil: payment.gateway.base-url.
| Tempat | Bentuk |
|---|---|
.properties / .yml | payment.gateway.base-url |
| Environment variable | PAYMENT_GATEWAY_BASEURL |
| Argumen command line | --payment.gateway.base-url=... |
| Komponen record Java | baseUrl |
Aturan env var: ganti . dengan _, hapus -, jadikan huruf besar. Maka spring.datasource.url menjadi SPRING_DATASOURCE_URL dan spring.profiles.active menjadi SPRING_PROFILES_ACTIVE.
@Value: cepat untuk satu nilai
Untuk satu-dua nilai, @Value dengan placeholder ${...} sudah cukup. Teks setelah : adalah nilai default:
java
// Potongan
@Service
public class InvoiceNumberGenerator {
private final String prefix;
public InvoiceNumberGenerator(@Value("${invoice.prefix:INV}") String prefix) {
this.prefix = prefix;
}
}Selalu tulis nama di placeholder dalam bentuk kanonis kebab-case agar relaxed binding berlaku. Kelemahan @Value: nama properti tersebar sebagai string di banyak class, tidak ada validasi, dan salah ketik baru ketahuan saat runtime.
@ConfigurationProperties dengan record
Untuk sekelompok properti dengan prefix yang sama, ikat semuanya ke satu record bertipe kuat. Karena record punya satu konstruktor, Boot memakai constructor binding secara otomatis:
java
// Potongan — butuh dependency spring-boot-starter-validation untuk anotasi jakarta.validation
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "payment.gateway")
public record PaymentGatewayProperties(
@NotNull URI baseUrl,
@NotBlank String apiKey,
@DefaultValue("5s") Duration timeout,
@DefaultValue("3") @Min(0) @Max(10) int maxRetries) {}Beberapa hal yang kamu dapat sekaligus:
- Konversi tipe —
"5s"menjadiDuration,"https://..."menjadiURI,"10MB"bisa menjadiDataSize. - Default —
@DefaultValuedipakai bila properti tidak diset. - Validasi saat startup — dengan
@Validated, nilai yang melanggar constraint membuat aplikasi gagal start dengan pesan yang menyebut properti mana yang salah. Lebih baik gagal saat deploy daripada gagal saat pelanggan bayar.
Record ini belum menjadi bean sampai didaftarkan. Cara termudah: pasang @ConfigurationPropertiesScan di class utama, atau @EnableConfigurationProperties(PaymentGatewayProperties.class) di class @Configuration:
java
// Potongan
@SpringBootApplication
@ConfigurationPropertiesScan
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}Setelah itu record bisa di-inject lewat constructor seperti bean lain (bab IoC/DI).
| Aspek | @Value | @ConfigurationProperties |
|---|---|---|
| Cocok untuk | 1–2 nilai lepas | sekelompok properti satu prefix |
| Tipe | per field | record bertipe kuat, bisa bertingkat |
| Validasi | tidak ada | @Validated + Bean Validation |
| Relaxed binding | terbatas (pakai kebab-case) | penuh |
Profile: konfigurasi per environment
Profile adalah label untuk mengaktifkan sekumpulan konfigurasi. Konvensinya dev, test, dan prod. File application-{profile}.yml hanya dibaca bila profile itu aktif, dan nilainya menimpa application.yml:
yaml
# application.yml — default untuk semua environment
payment:
gateway:
base-url: https://sandbox.pay.example.com
api-key: ${PAYMENT_API_KEY:}
logging:
level:
root: INFOyaml
# application-dev.yml
logging:
level:
com.example.order: DEBUGyaml
# application-prod.yml
payment:
gateway:
base-url: https://api.pay.example.com
timeout: 3sCara mengaktifkan profile:
bash
# argumen command line
java -jar order-service.jar --spring.profiles.active=prod
# environment variable (umum di Docker/Kubernetes)
SPRING_PROFILES_ACTIVE=prod java -jar order-service.jar
# saat development dengan Maven
./mvnw spring-boot:run -Dspring-boot.run.profiles=devKamu juga bisa menulis beberapa profile dalam satu file YAML, dipisah ---, dengan spring.config.activate.on-profile. Di level bean, anotasi @Profile("dev") membuat bean hanya dibuat saat profile itu aktif — misalnya gateway palsu untuk development:
java
// Potongan
@Configuration
public class PaymentClientConfig {
@Bean
@Profile("dev")
PaymentClient fakePaymentClient() {
return new FakePaymentClient(); // selalu sukses, tanpa memanggil gateway asli
}
@Bean
@Profile("!dev")
PaymentClient httpPaymentClient(PaymentGatewayProperties props) {
return new HttpPaymentClient(props);
}
}Di test, @ActiveProfiles("test") mengaktifkan profile test — dibahas di bab testing web.
Profile untuk perbedaan konfigurasi, bukan logika bisnis
Profile dipakai untuk membedakan infrastruktur (URL, kredensial, level log, adapter palsu). Jangan menulis if (profile == prod) di logika bisnis — perilaku yang berbeda antara dev dan prod membuat bug lolos dari test.
Secret tidak boleh masuk repo
API key, password database, dan secret JWT tidak boleh ditulis di application*.yml yang di-commit. Pola yang aman:
- Di file, tulis placeholder:
api-key: ${PAYMENT_API_KEY}. Nilai aslinya diset sebagai env var oleh platform deploy. - Untuk laptop sendiri, simpan nilai lokal di file yang di-gitignore dan impor dengan
spring.config.import. Awalanoptional:membuat aplikasi tetap start bila file itu tidak ada:
yaml
spring:
config:
import: optional:file:./config/application-local.yml- Di Kubernetes, secret sering di-mount sebagai file per kunci;
spring.config.import: optional:configtree:/etc/secrets/membaca setiap file di folder itu sebagai properti. Detail Kubernetes ada di level Microservices.
Secret yang sudah ter-commit dianggap bocor
Menghapus secret di commit berikutnya tidak cukup — ia tetap ada di riwayat Git. Segera rotasi (ganti) secret tersebut di penyedia layanannya.
Contoh di dunia backend
Service checkout memanggil payment gateway. Properti dikelompokkan dalam record tervalidasi, lalu di-inject ke client:
java
// Potongan — memakai PaymentGatewayProperties di atas; didaftarkan sebagai bean lewat PaymentClientConfig
public class HttpPaymentClient implements PaymentClient {
private static final Logger log = LoggerFactory.getLogger(HttpPaymentClient.class);
private final PaymentGatewayProperties props;
public HttpPaymentClient(PaymentGatewayProperties props) {
this.props = props;
// Jangan pernah me-log apiKey — cukup informasi yang aman
log.info("Payment gateway {} (timeout {}, retry {})",
props.baseUrl(), props.timeout(), props.maxRetries());
}
@Override
public PaymentResult charge(String orderId, long amount) {
// panggilan HTTP ke props.baseUrl() dengan header Authorization dari props.apiKey()
// — klien HTTP dibahas di bab-bab berikutnya
throw new UnsupportedOperationException("belum diimplementasi");
}
}Deploy ke production cukup menyetel environment variable, tanpa mengubah satu baris kode:
bash
export SPRING_PROFILES_ACTIVE=prod
export PAYMENT_API_KEY='diisi-dari-secret-manager'
export PAYMENT_GATEWAY_MAXRETRIES=5
java -jar order-service.jarHasilnya: base-url dan timeout dari application-prod.yml, api-key dari env var, max-retries ditimpa env var menjadi 5. Kalau PAYMENT_API_KEY lupa diset, placeholder menghasilkan string kosong, @NotBlank gagal, dan aplikasi menolak start — tim langsung tahu sebelum ada transaksi yang gagal.
Pertanyaan interview
1. Bagaimana Spring Boot menentukan nilai sebuah properti bila diset di beberapa tempat?
Boot menyusun daftar property source berurutan dan sumber berprioritas lebih tinggi menimpa yang lebih rendah. Secara garis besar: application.yml, lalu file khusus profile, lalu file di luar jar, environment variable, system property, dan argumen command line di urutan tertinggi (properti test lebih tinggi lagi). Karena itu default ditaruh di file, sedangkan nilai per environment disuntikkan lewat env var.
2. Apa keuntungan @ConfigurationProperties dibanding @Value?
Properti dengan prefix yang sama dikumpulkan di satu record bertipe kuat, mendukung konversi (Duration, URI), default lewat @DefaultValue, relaxed binding penuh, dan validasi saat startup dengan @Validated. @Value cocok untuk satu nilai sederhana, tetapi string nama properti tersebar dan tidak tervalidasi.
3. Bagaimana mengelola secret di aplikasi Spring Boot?
Jangan simpan di file yang di-commit. Tulis placeholder ${NAMA_ENV} di konfigurasi dan suntikkan nilainya lewat environment variable, secret yang di-mount (configtree:), atau secret manager. Validasi bahwa secret wajib tidak kosong saat startup, jangan me-log nilainya, dan rotasi segera bila pernah ter-commit.
Jebakan umum
- Menaruh
application.propertiesdanapplication.ymlsekaligus —.propertiesmenang, perubahan di YAML seolah diabaikan. - Nama env var salah bentuk, misalnya
PAYMENT_GATEWAY_BASE-URLataupayment.gateway.base-urldi shell. Ikuti aturan:.→_, hapus-, huruf besar. - Indentasi YAML memakai tab atau tidak konsisten — properti masuk ke prefix yang salah atau file gagal di-parse.
- Mengaktifkan profile dari dalam
application-dev.yml—spring.profiles.activedi dokumen khusus profile ditolak dan aplikasi gagal start. - Secret sebagai nilai default, misalnya
${PAYMENT_API_KEY:sk_live_abc}— secret ikut ter-commit. - Lupa
@Validated— anotasi@NotBlankdi record diam saja, salah konfigurasi baru ketahuan di runtime.
Ringkasan
- Konfigurasi ditulis di
application.yml/.properties; satu artefak jar dipakai untuk semua environment. - Banyak sumber digabung; prioritas naik dari file → env var → system property → argumen command line.
- Relaxed binding:
payment.gateway.base-url=PAYMENT_GATEWAY_BASEURL= komponen recordbaseUrl. @Valueuntuk nilai lepas;@ConfigurationProperties+ record +@Validateduntuk kelompok properti yang aman.- Profile (
application-{profile}.yml,spring.profiles.active,@Profile) memisahkan konfigurasi per environment. - Secret lewat env var atau file di luar repo (
spring.config.import), divalidasi saat startup, tidak pernah di-log.