Chapter sebelumnya memakai environment variable sebagai satu-satunya pintu masuk konfigurasi. Pendekatan itu melindungi kredensial, tetapi kurang nyaman ketika pengaturan berjumlah puluhan: tidak ada berkas yang bisa dibuka editor, tidak ada diff yang bisa direview rekan tim, dan kesalahan ketik baru terasa saat aplikasi menyala. Praktik produksi memadukan kedua dunia lewat externalized configuration: konfigurasi disimpan di berkas yang hidup di luar artefak JAR, sementara environment variable tetap disediakan sebagai penimpa darurat untuk nilai yang berbeda tiap mesin. Chapter ini membangun mesin penggabung tiga lapis tersebut dengan API java.util.Properties murni.
Tiga Lapis Sumber Konfigurasi #
Setiap kunci konfigurasi dicari berurutan dari lapis dengan prioritas terendah ke tertinggi:
| Lapis | Lokasi | Isi khas | Diatur oleh |
|---|---|---|---|
| 1. Default | di dalam kode | nilai aman pengembangan | programmer |
| 2. Berkas | config/application.properties di sebelah JAR | pengaturan lingkungan | operator |
| 3. Environment variable | lingkungan proses | kredensial, nilai khusus mesin | platform/orkestrator |
Lapis 2 membuat konfigurasi bisa dibaca manusia dan dikelola bersama; lapis 3 tetap menang karena sifatnya menimpa — kredensial tidak pernah menetap di berkas yang ikut repository. Aturan mainnya sederhana: lapis tinggi boleh menimpa lapis rendah, tetapi tidak pernah sebaliknya.
Berkas Properties di Sebelah JAR #
Operator meletakkan config/application.properties berdampingan dengan toko-fat.jar:
app.database.url=jdbc:postgresql://db.internal:5432/kasir
app.log.level=INFO
app.pool.max=10Aplikasi membacanya dengan java.util.Properties — API JDK yang mem-parsing format kunci=nilai per baris:
Properties berkas = new Properties();
try (InputStream masuk = Files.newInputStream(
Path.of("config", "application.properties"))) {
berkas.load(masuk);
}Path relatif dibaca dari working directory proses, bukan dari dalam JAR — itulah makna kata eksternal. Ekosistem Java menyediakan pustaka yang lebih kaya di atas pola yang sama: Spring Boot membaca application.properties/application.yml plus profil (application-prod.properties), sementara Typesafe Config menawarkan format HOCON berhierarki.
Penggabungan Berlapis dalam Satu Tabel #
Program berikut merangkai ketiga lapis menjadi satu tabel gabungan. Lapis 3 disimulasikan lewat argumen program berformat KUNCI=nilai agar demo bisa berjalan di mana saja, dengan System.getenv tetap diperiksa untuk kunci yang tidak disimulasikan:
import java.io.StringReader;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
/** Konfigurasi berlapis: default di dalam kode < berkas eksternal < environment variable. */
public class LapisKonfigurasi {
/** Lapis 1: default yang aman untuk pengembangan. */
static final Map<String, String> DEFAULT = Map.of(
"app.name", "aplikasi-kasir",
"app.env", "dev",
"app.database.url", "jdbc:sqlite:data/kasir-dev.db",
"app.database.driver", "org.postgresql.Driver",
"app.log.level", "DEBUG",
"app.pool.max", "5");
/** Lapis 2: isi config/application.properties yang diletakkan operator
* di sebelah JAR — pada program ini disematkan sebagai string agar mandiri. */
static final String ISI_BERKAS = """
app.database.url=jdbc:postgresql://db.internal:5432/kasir
app.log.level=INFO
app.pool.max=10
""";
/** Lapis 3: environment variable proses. Argumen program dengan format
* KUNCI=nilai menyimulasikan env var agar demo bisa dijalankan tanpa
* menyetel lingkungan sungguhan; System.getenv tetap diperiksa. */
static Map<String, String> envSimulasi = new LinkedHashMap<>();
/** Pemetaan binding longgar: app.pool.max -> APP_POOL_MAX. */
static String bacaEnv(String kunciKonfigurasi) {
String namaVar = kunciKonfigurasi.toUpperCase().replace('.', '_');
if (envSimulasi.containsKey(namaVar)) {
return envSimulasi.get(namaVar);
}
return System.getenv(namaVar);
}
public static void main(String[] args) throws Exception {
for (String a : args) {
int pos = a.indexOf('=');
if (pos > 0) {
envSimulasi.put(a.substring(0, pos), a.substring(pos + 1));
}
}
Properties berkas = new Properties();
berkas.load(new StringReader(ISI_BERKAS));
System.out.println("== Lapisan sumber (urutan prioritas menaik) ==");
System.out.println("1. default di dalam kode : " + DEFAULT.size() + " kunci");
System.out.println("2. berkas eksternal : " + berkas.size() + " kunci");
System.out.println("3. environment variable : " + envSimulasi + " (disimulasikan lewat argumen)");
System.out.println();
System.out.println("== Tabel konfigurasi gabungan ==");
TreeMap<String, String[]> gabungan = new TreeMap<>(); // kunci -> {nilai, sumber}
for (var e : DEFAULT.entrySet()) {
gabungan.put(e.getKey(), new String[]{e.getValue(), "default"});
}
for (var e : berkas.entrySet()) {
gabungan.put((String) e.getKey(), new String[]{(String) e.getValue(), "berkas"});
}
int dariEnv = 0, tertimpaBerkas = 0;
for (String kunci : gabungan.keySet()) {
String nilaiEnv = bacaEnv(kunci);
if (nilaiEnv != null) {
if (gabungan.get(kunci)[1].equals("berkas")) {
tertimpaBerkas++;
}
gabungan.put(kunci, new String[]{nilaiEnv, "env"});
dariEnv++;
}
}
for (var e : gabungan.entrySet()) {
String[] v = e.getValue();
System.out.printf(" %-22s = %-45s [%s]%n", e.getKey(), v[0], v[1]);
}
System.out.println();
System.out.println("kunci dari env : " + dariEnv);
System.out.println("menimpa nilai dari berkas : " + tertimpaBerkas);
}
}Output yang Dihasilkan:
== Lapisan sumber (urutan prioritas menaik) ==
1. default di dalam kode : 6 kunci
2. berkas eksternal : 3 kunci
3. environment variable : {APP_ENV=prod, APP_POOL_MAX=20} (disimulasikan lewat argumen)
== Tabel konfigurasi gabungan ==
app.database.driver = org.postgresql.Driver [default]
app.database.url = jdbc:postgresql://db.internal:5432/kasir [berkas]
app.env = prod [env]
app.log.level = INFO [berkas]
app.name = aplikasi-kasir [default]
app.pool.max = 20 [env]
kunci dari env : 2
menimpa nilai dari berkas : 1Mari kita bedah mekanisme penggabungannya:
DEFAULT: enam kunci nilai dasar — lapis paling bawah.Map.ofdipilih karena isinya memang konstan sejak kompilasi.ISI_BERKAS+Properties.load(new StringReader(...)): parsing format properties dari string; pada aplikasi nyata,StringReaderdigantikanInputStreamberkas eksternal seperti kerangka di atas. Text block Java membuat isi berkas tampil apa adanya.bacaEnv(kunci): pemetaan binding longgar —app.pool.maxdiubah menjadiAPP_POOL_MAXlewattoUpperCase()dan penggantian titik menjadi garis bawah, konvensi yang sama dipakai Spring Boot. Nilai disimulasikan dari argumen dulu,System.getenvmenjadi pelengkap.- Urutan penulisan ke
gabungan: default ditulis lebih dulu, entri berkas menimpa kunci senama, lalu lingkaranforterakhir menerapkan env. KarenaTreeMap, tabel tercetak terurut abjad kunci — laporan yang stabil untuk dibandingkan antar mesin. - Kolom
[sumber]: jejak asal-usul setiap nilai. Saat insiden, kolom kecil ini menjawab pertanyaan pertama pengaturan mana yang aktif —app.pool.maxtercatat[env]karena operator menimpa nilai10dari berkas dengan20. - Statistik akhir: dua kunci datang dari env, satu di antaranya menimpa nilai berkas — bukti bahwa urutan prioritas benar-benar dieksekusi, bukan sekadar dokumentasi.
Konfigurasi kini hidup di luar artefak, dan satu JAR bisa menempati lingkungan mana pun. Tempat berikutnya yang akan kita tempati adalah lingkungan paling populer untuk menjalankan JAR tersebut: container Docker — mulai dari konsep dasarnya.
Referensi #
- Oracle: API java.util.Properties
- The Twelve-Factor App: Config
- Spring Boot: Externalized Configuration
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LapisKonfigurasi.java
Unduh berkasimport java.io.StringReader;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
/** Konfigurasi berlapis: default di dalam kode < berkas eksternal < environment variable. */
public class LapisKonfigurasi {
/** Lapis 1: default yang aman untuk pengembangan. */
static final Map<String, String> DEFAULT = Map.of(
"app.name", "aplikasi-kasir",
"app.env", "dev",
"app.database.url", "jdbc:sqlite:data/kasir-dev.db",
"app.database.driver", "org.postgresql.Driver",
"app.log.level", "DEBUG",
"app.pool.max", "5");
/** Lapis 2: isi config/application.properties yang diletakkan operator
* di sebelah JAR — pada program ini disematkan sebagai string agar mandiri. */
static final String ISI_BERKAS = """
app.database.url=jdbc:postgresql://db.internal:5432/kasir
app.log.level=INFO
app.pool.max=10
""";
/** Lapis 3: environment variable proses. Argumen program dengan format
* KUNCI=nilai menyimulasikan env var agar demo bisa dijalankan tanpa
* menyetel lingkungan sungguhan; System.getenv tetap diperiksa. */
static Map<String, String> envSimulasi = new LinkedHashMap<>();
/** Pemetaan binding longgar: app.pool.max -> APP_POOL_MAX. */
static String bacaEnv(String kunciKonfigurasi) {
String namaVar = kunciKonfigurasi.toUpperCase().replace('.', '_');
if (envSimulasi.containsKey(namaVar)) {
return envSimulasi.get(namaVar);
}
return System.getenv(namaVar);
}
public static void main(String[] args) throws Exception {
for (String a : args) {
int pos = a.indexOf('=');
if (pos > 0) {
envSimulasi.put(a.substring(0, pos), a.substring(pos + 1));
}
}
Properties berkas = new Properties();
berkas.load(new StringReader(ISI_BERKAS));
System.out.println("== Lapisan sumber (urutan prioritas menaik) ==");
System.out.println("1. default di dalam kode : " + DEFAULT.size() + " kunci");
System.out.println("2. berkas eksternal : " + berkas.size() + " kunci");
System.out.println("3. environment variable : " + envSimulasi + " (disimulasikan lewat argumen)");
System.out.println();
System.out.println("== Tabel konfigurasi gabungan ==");
TreeMap<String, String[]> gabungan = new TreeMap<>(); // kunci -> {nilai, sumber}
for (var e : DEFAULT.entrySet()) {
gabungan.put(e.getKey(), new String[]{e.getValue(), "default"});
}
for (var e : berkas.entrySet()) {
gabungan.put((String) e.getKey(), new String[]{(String) e.getValue(), "berkas"});
}
int dariEnv = 0, tertimpaBerkas = 0;
for (String kunci : gabungan.keySet()) {
String nilaiEnv = bacaEnv(kunci);
if (nilaiEnv != null) {
if (gabungan.get(kunci)[1].equals("berkas")) {
tertimpaBerkas++;
}
gabungan.put(kunci, new String[]{nilaiEnv, "env"});
dariEnv++;
}
}
for (var e : gabungan.entrySet()) {
String[] v = e.getValue();
System.out.printf(" %-22s = %-45s [%s]%n", e.getKey(), v[0], v[1]);
}
System.out.println();
System.out.println("kunci dari env : " + dariEnv);
System.out.println("menimpa nilai dari berkas : " + tertimpaBerkas);
}
}