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:

LapisLokasiIsi khasDiatur oleh
1. Defaultdi dalam kodenilai aman pengembanganprogrammer
2. Berkasconfig/application.properties di sebelah JARpengaturan lingkunganoperator
3. Environment variablelingkungan proseskredensial, nilai khusus mesinplatform/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:

properties config/application.properties milik operator
app.database.url=jdbc:postgresql://db.internal:5432/kasir
app.log.level=INFO
app.pool.max=10

Aplikasi membacanya dengan java.util.Properties — API JDK yang mem-parsing format kunci=nilai per baris:

java Kerangka pembacaan berkas di luar JAR
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:

text
== 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 : 1

Mari kita bedah mekanisme penggabungannya:

  1. DEFAULT: enam kunci nilai dasar — lapis paling bawah. Map.of dipilih karena isinya memang konstan sejak kompilasi.
  2. ISI_BERKAS + Properties.load(new StringReader(...)): parsing format properties dari string; pada aplikasi nyata, StringReader digantikan InputStream berkas eksternal seperti kerangka di atas. Text block Java membuat isi berkas tampil apa adanya.
  3. bacaEnv(kunci): pemetaan binding longgar — app.pool.max diubah menjadi APP_POOL_MAX lewat toUpperCase() dan penggantian titik menjadi garis bawah, konvensi yang sama dipakai Spring Boot. Nilai disimulasikan dari argumen dulu, System.getenv menjadi pelengkap.
  4. Urutan penulisan ke gabungan: default ditulis lebih dulu, entri berkas menimpa kunci senama, lalu lingkaran for terakhir menerapkan env. Karena TreeMap, tabel tercetak terurut abjad kunci — laporan yang stabil untuk dibandingkan antar mesin.
  5. Kolom [sumber]: jejak asal-usul setiap nilai. Saat insiden, kolom kecil ini menjawab pertanyaan pertama pengaturan mana yang aktif — app.pool.max tercatat [env] karena operator menimpa nilai 10 dari berkas dengan 20.
  6. 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 #

Berkas Sumber

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

LapisKonfigurasi.java

Unduh berkas
java
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);
    }
}