Aplikasi kasir kita kini berbentuk JAR yang siap dijalankan — tetapi coba perhatikan string koneksi database yang tertanam di dalamnya. Mesin pengembangan memakai SQLite lokal, server staging memakai PostgreSQL uji, dan produksi memakai PostgreSQL asli dengan kredensial rahasia. Tiga nilai berbeda untuk satu konstanta berarti tiga build berbeda, dan setiap build ulang adalah kesempatan baru untuk kesalahan versi. Prinsip build once, deploy many menuntut konfigurasi hidup di luar artefak: ditetapkan operator melalui lingkungan tempat aplikasi berjalan. Chapter ini membangun rantai pencarian konfigurasi berbasis environment variable — mekanisme yang sama dipakai platform Docker, systemd, dan seluruh platform cloud.
Masalah Konfigurasi Tertanam di Kode #
Bayangkan class Pengaturan dengan String DB_URL = "jdbc:postgresql://db.internal:5432/kasir". Nilai itu benar di satu mesin saja. Kebutuhan tiap lingkungan memang berbeda:
| Pengaturan | Pengembangan | Staging | Produksi |
|---|---|---|---|
| Database | SQLite lokal | PostgreSQL uji | PostgreSQL klaster |
| Level log | DEBUG | INFO | INFO |
| Ukuran pool | 5 | 10 | 20 |
| Kredensial | tak penting | tim uji | rahasia terkelola |
Menyimpan nilai produksi di dalam kode juga berarti kredensial ikut terbaca siapa pun yang membaca repository — pola yang dihindari praktik keamanan modern. Solusinya satu arah: kode hanya membaca nama variabel, sedangkan nilai datang dari lingkungan.
Membaca Environment Variable #
Java menyediakan dua bentuk System.getenv: bentuk tanpa argumen mengembalikan seluruh peta lingkungan proses, bentuk System.getenv("NAMA") mengembalikan satu nilai atau null bila tidak diatur. Nilai null inilah yang kita jadikan sinyal untuk beralih ke nilai default. Program berikut merangkai keduanya menjadi rantai pencarian tiga skenario:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
/** Rantai pencarian konfigurasi: environment variable lebih dulu, default menyusul. */
public class KonfigurasiEnv {
/** Nilai konfigurasi yang sudah terikat beserta sumbernya. */
record Nilai(String kunci, String nilai, String sumber) {}
/** Daftar kunci yang dibaca aplikasi beserta nilai defaultnya. */
static final Map<String, String> DEFAULT = Map.of(
"APP_ENV", "dev",
"APP_DATABASE_URL", "jdbc:sqlite:data/kasir-dev.db",
"APP_LOG_LEVEL", "DEBUG",
"APP_POOL_SIZE", "5");
/** Cari satu kunci: environment variable dulu, lalu default. */
static Nilai cari(String kunci, Function<String, String> env) {
String dariEnv = env.apply(kunci);
if (dariEnv != null) {
return new Nilai(kunci, dariEnv, "env");
}
return new Nilai(kunci, DEFAULT.get(kunci), "default");
}
/** Profil turunan: satu nilai APP_ENV memutuskan banyak pengaturan lain. */
static String profil(String appEnv) {
return switch (appEnv) {
case "prod" -> "produksi -> pool=20, log=INFO";
case "staging" -> "uji -> pool=10, log=INFO";
default -> "pengembangan -> pool=5, log=DEBUG";
};
}
static void skenario(String judul, Function<String, String> env) {
System.out.println("== " + judul + " ==");
for (String kunci : new String[]{"APP_ENV", "APP_DATABASE_URL", "APP_LOG_LEVEL"}) {
Nilai n = cari(kunci, env);
System.out.printf(" %-18s = %-45s (sumber: %s)%n", kunci, n.nilai(), n.sumber());
}
Nilai envApp = cari("APP_ENV", env);
System.out.println(" profil = " + profil(envApp.nilai()));
System.out.println();
}
public static void main(String[] args) {
// Skenario 1 memakai System.getenv sungguhan; di lingkungan uji ini
// tidak ada APP_* yang diatur sehingga seluruh nilai jatuh ke default.
skenario("Skenario 1: server tanpa environment variable", System.getenv()::get);
// Skenario 2-3 mensimulasikan environment yang diatur operator.
Map<String, String> envProd = new LinkedHashMap<>();
envProd.put("APP_ENV", "prod");
envProd.put("APP_DATABASE_URL", "jdbc:postgresql://db.internal:5432/kasir");
skenario("Skenario 2: server produksi", envProd::get);
Map<String, String> envCampur = new LinkedHashMap<>();
envCampur.put("APP_DATABASE_URL", "jdbc:postgresql://db.internal:5432/kasir");
skenario("Skenario 3: env diatur sebagian", envCampur::get);
}
}Output yang Dihasilkan:
== Skenario 1: server tanpa environment variable ==
APP_ENV = dev (sumber: default)
APP_DATABASE_URL = jdbc:sqlite:data/kasir-dev.db (sumber: default)
APP_LOG_LEVEL = DEBUG (sumber: default)
profil = pengembangan -> pool=5, log=DEBUG
== Skenario 2: server produksi ==
APP_ENV = prod (sumber: env)
APP_DATABASE_URL = jdbc:postgresql://db.internal:5432/kasir (sumber: env)
APP_LOG_LEVEL = DEBUG (sumber: default)
profil = produksi -> pool=20, log=INFO
== Skenario 3: env diatur sebagian ==
APP_ENV = dev (sumber: default)
APP_DATABASE_URL = jdbc:postgresql://db.internal:5432/kasir (sumber: env)
APP_LOG_LEVEL = DEBUG (sumber: default)
profil = pengembangan -> pool=5, log=DEBUGMari kita amati alurnya baris demi baris:
DEFAULT: peta nilai cadangan per kunci — satu-satunya tempat nilai tetap yang wajar di dalam kode, karena isinya konvensi aman untuk lingkungan pengembangan.cari(kunci, env): method pencarian satu kunci. ParameterenvbertipeFunction<String,String>sehingga sumber lingkungan bisa disuntik:System.getenv()::getuntuk proses nyata, atau peta simulasi untuk skenario uji.- Skenario 1: memakai
System.getenvsungguhan. Di mesin uji ini tidak ada variabelAPP_*yang diatur, sehingga ketiga kunci dilaporkan berasal daridefault— jalur yang sama yang diambil aplikasi saat operator lupa mengatur variabel. - Skenario 2: seluruh variabel diatur — kedua kunci dilaporkan berasal dari
env, dan satu berkas JAR yang sama kini berperilaku sebagai aplikasi produksi tanpa kompilasi ulang. profil(...): satu variabelAPP_ENVmemutuskan paket pengaturan turunan (pool, level log). Menetapkan 30 variabel satu per satu rawan salah; satu variabel profil yang menurunkan sisanya lebih sulit keliru.- Skenario 3: lingkungan setengah terisi —
APP_DATABASE_URLdari env, sisanya default. Sumber tiap nilai tercatat per baris, sehingga konfigurasi campuran seperti ini mudah diaudit.
Menetapkan Environment Variable #
Nilai lingkungan ditetapkan di luar aplikasi, sesuai platform tempatnya berjalan:
export APP_ENV=prod
export APP_DATABASE_URL="jdbc:postgresql://db.internal:5432/kasir"
export APP_DATABASE_PASSWORD="YOUR_DATABASE_PASSWORD"
java -jar toko-fat.jar[Service]
Environment="APP_ENV=prod"
EnvironmentFile=/etc/kasirapp/kasir.env
ExecStart=/usr/bin/java -jar /opt/kasirapp/toko-fat.jarKonvensi penamaan yang lazim: huruf kapital dengan awalan nama aplikasi (APP_, KASIR_), dipisah garis bawah. Awalan mencegah bertabrakan dengan variabel milik sistem operasi atau aplikasi lain yang berbagi mesin.
Environment variable kuat untuk nilai yang diatur operator sekali sebelum aplikasi menyala, tetapi kurang nyaman saat konfigurasi berjumlah banyak dan ingin diperiksa sebagai berkas. Chapter berikutnya memperluas rantai pencarian ini dengan berkas konfigurasi eksternal dan urutan prioritasnya: default, berkas, lalu environment variable sebagai penimpa terakhir.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KonfigurasiEnv.java
Unduh berkasimport java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
/** Rantai pencarian konfigurasi: environment variable lebih dulu, default menyusul. */
public class KonfigurasiEnv {
/** Nilai konfigurasi yang sudah terikat beserta sumbernya. */
record Nilai(String kunci, String nilai, String sumber) {}
/** Daftar kunci yang dibaca aplikasi beserta nilai defaultnya. */
static final Map<String, String> DEFAULT = Map.of(
"APP_ENV", "dev",
"APP_DATABASE_URL", "jdbc:sqlite:data/kasir-dev.db",
"APP_LOG_LEVEL", "DEBUG",
"APP_POOL_SIZE", "5");
/** Cari satu kunci: environment variable dulu, lalu default. */
static Nilai cari(String kunci, Function<String, String> env) {
String dariEnv = env.apply(kunci);
if (dariEnv != null) {
return new Nilai(kunci, dariEnv, "env");
}
return new Nilai(kunci, DEFAULT.get(kunci), "default");
}
/** Profil turunan: satu nilai APP_ENV memutuskan banyak pengaturan lain. */
static String profil(String appEnv) {
return switch (appEnv) {
case "prod" -> "produksi -> pool=20, log=INFO";
case "staging" -> "uji -> pool=10, log=INFO";
default -> "pengembangan -> pool=5, log=DEBUG";
};
}
static void skenario(String judul, Function<String, String> env) {
System.out.println("== " + judul + " ==");
for (String kunci : new String[]{"APP_ENV", "APP_DATABASE_URL", "APP_LOG_LEVEL"}) {
Nilai n = cari(kunci, env);
System.out.printf(" %-18s = %-45s (sumber: %s)%n", kunci, n.nilai(), n.sumber());
}
Nilai envApp = cari("APP_ENV", env);
System.out.println(" profil = " + profil(envApp.nilai()));
System.out.println();
}
public static void main(String[] args) {
// Skenario 1 memakai System.getenv sungguhan; di lingkungan uji ini
// tidak ada APP_* yang diatur sehingga seluruh nilai jatuh ke default.
skenario("Skenario 1: server tanpa environment variable", System.getenv()::get);
// Skenario 2-3 mensimulasikan environment yang diatur operator.
Map<String, String> envProd = new LinkedHashMap<>();
envProd.put("APP_ENV", "prod");
envProd.put("APP_DATABASE_URL", "jdbc:postgresql://db.internal:5432/kasir");
skenario("Skenario 2: server produksi", envProd::get);
Map<String, String> envCampur = new LinkedHashMap<>();
envCampur.put("APP_DATABASE_URL", "jdbc:postgresql://db.internal:5432/kasir");
skenario("Skenario 3: env diatur sebagian", envCampur::get);
}
}