Protocol HTTP sudah kita baca; pertanyaan berikutnya adalah bagaimana kode Java kita dipanggil ketika permintaan tiba. Jawaban klasik Java untuk pertanyaan itu adalah servlet: sebuah objek Java yang hidup di dalam web container dan method yang dijalankan container setiap kali permintaan HTTP cocok dengan pola URL servlet tersebut. Semua framework web Java modern — Spring MVC, Jakarta REST, Javalin — pada dasarnya berdiri di atas mekanisme ini, jadi memahami servlet berarti memahami fondasi yang tidak berubah selama dua puluh tahun lebih. Chapter ini mengenali anatomi servlet, peran web container, lalu menyimulasikan keduanya dalam program Java murni.

Peran Web Container #

Servlet tidak berdiri sendiri. Web container (servlet container) seperti Apache Tomcat atau Eclipse Jetty yang menjalankan servlet dan menangani seluruh pekerjaan tingkat protokol: menerima koneksi TCP, mengurai request line dan header, mengelola thread per permintaan, serta menulis balasan ke socket. Container juga mengelola hidup servlet — membuat instance, memanggil method inisialisasi, dan mematikannya saat aplikasi berhenti. Kode kita hanya menerima objek permintaan dan balasan yang sudah berbentuk objek Java.

Dalam kode nyata, servlet dideklarasikan dengan anotasi dan mewarisi HttpServlet:

java Servlet nyata dengan Jakarta Servlet 6.1
import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/produk")
public class DaftarProdukServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<ul><li>buku</li><li>pena</li><li>tas</li></ul>");
    }
}

Container membaca anotasi @WebServlet("/produk") saat aplikasi dimuat, membuat satu instance DaftarProdukServlet, lalu mengarahkan setiap permintaan GET /produk ke method doGet instance tersebut. Method lain seperti doPost dan doDelete menangani verb HTTP yang berbeda.

Kontrak Servlet dalam Miniatur #

Karena container sungguhan tidak terpasang di lingkungan belajar kita, kita bangun tiruannya sendiri. WadahMini memodelkan tiga ide inti servlet: pemetaan pola URL ke objek pemroses, pemeriksaan method yang didukung, dan penggunaan kembali instance yang sama untuk banyak permintaan:

java WadahMini.java
import java.util.LinkedHashMap;
import java.util.Map;

public class WadahMini {

    interface ServletMini {
        boolean menerima(String metode);

        String isi(String jalur);
    }

    static class HalamanDepan implements ServletMini {
        @Override
        public boolean menerima(String metode) {
            return metode.equals("GET");
        }

        @Override
        public String isi(String jalur) {
            return "selamat datang di toko Budi";
        }
    }

    static class DaftarProduk implements ServletMini {
        private int panggilan = 0;

        @Override
        public boolean menerima(String metode) {
            return metode.equals("GET");
        }

        @Override
        public String isi(String jalur) {
            panggilan++;
            return "daftar produk (panggilan ke-" + panggilan
                    + " pada instance yang sama): buku, pena, tas";
        }
    }

    static class Wadah {
        private final Map<String, ServletMini> rute = new LinkedHashMap<>();

        void daftarkan(String pola, ServletMini servlet) {
            rute.put(pola, servlet);
        }

        String tangani(String metode, String jalur) {
            ServletMini servlet = rute.get(jalur);
            if (servlet == null) {
                return "404 Not Found: tidak ada servlet pada " + jalur;
            }
            if (!servlet.menerima(metode)) {
                return "405 Method Not Allowed: servlet tidak menerima " + metode;
            }
            return "200 OK: " + servlet.isi(jalur);
        }
    }

    public static void main(String[] args) {
        Wadah wadah = new Wadah();
        wadah.daftarkan("/", new HalamanDepan());
        wadah.daftarkan("/produk", new DaftarProduk());

        System.out.println("== Wadah memetakan pola URL ke servlet ==");
        System.out.println("/       -> HalamanDepan");
        System.out.println("/produk -> DaftarProduk");

        System.out.println();
        System.out.println("== Permintaan ditangani wadah ==");
        String[][] permintaan = {
                {"GET", "/"}, {"GET", "/produk"}, {"POST", "/produk"},
                {"GET", "/produk"}, {"GET", "/pelanggan"}};
        for (String[] p : permintaan) {
            System.out.printf("%-5s %-12s -> %s%n", p[0], p[1], wadah.tangani(p[0], p[1]));
        }
    }
}

Output yang Dihasilkan:

text
== Wadah memetakan pola URL ke servlet ==
/       -> HalamanDepan
/produk -> DaftarProduk

== Permintaan ditangani wadah ==
GET   /            -> 200 OK: selamat datang di toko Budi
GET   /produk      -> 200 OK: daftar produk (panggilan ke-1 pada instance yang sama): buku, pena, tas
POST  /produk      -> 405 Method Not Allowed: servlet tidak menerima POST
GET   /produk      -> 200 OK: daftar produk (panggilan ke-2 pada instance yang sama): buku, pena, tas
GET   /pelanggan   -> 404 Not Found: tidak ada servlet pada /pelanggan

Mari kita bedah bagian inti tiruan container ini:

  1. Baris 6-10 (interface ServletMini): kontrak pemroses permintaan versi kita, sepadan dengan jakarta.servlet.Servlet yang mendefinisikan method init, service, dan destroy. Versi mini menyederhanakan kontrak menjadi dua method agar fokus tetap pada dispatch.
  2. Baris 41 (Map<String, ServletMini> rute): tabel routing adalah peta pola URL ke servlet. Container sungguhan mendukung pola lebih kaya (/produk/*, *.do) namun prinsipnya sama: satu struktur data menentukan pemroses mana yang dipanggil.
  3. Baris 48-51 (rute.get(jalur)): pencarian servlet gagal menghasilkan balasan 404 — perilaku yang kita kenal dari Chapter HTTP Fundamentals.
  4. Baris 52-54 (menerima(metode)): bila servlet tidak mendukung method yang diminta, wadah membalas 405. Pada HttpServlet asli, pemeriksaan ini terjadi di method service() yang meneruskan ke doGet/doPost yang sesuai.
  5. Baris 25 dan 34 (private int panggilan): penghitung sengaja diletakkan sebagai variabel instance. Output membuktikan panggilan ke-1 dan ke-2 dilayani objek yang sama — container memang hanya membuat satu instance per deklarasi servlet, bukan satu instance per permintaan.

Satu Instance untuk Semua Permintaan #

Perilaku “satu instance, banyak permintaan” adalah keputusan desain yang disengaja untuk efisiensi — membuat objek baru per permintaan akan memboroskan memori pada aplikasi sibuk. Konsekuensinya serius: dua permintaan dari dua pengguna berbeda dapat diproses dua thread berbeda pada objek yang sama secara bersamaan. Data yang tersimpan di variabel instance karenanya rawan terlihat antar pengguna, dan shared state tanpa sinkronisasi dapat menghasilkan nilai yang tidak konsisten. Kita akan membahas urutan hidup servlet dan jebakan state ini secara mendalam pada chapter berikutnya.

Referensi #

Berkas Sumber

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

WadahMini.java

Unduh berkas
java
import java.util.LinkedHashMap;
import java.util.Map;

public class WadahMini {

    interface ServletMini {
        boolean menerima(String metode);

        String isi(String jalur);
    }

    static class HalamanDepan implements ServletMini {
        @Override
        public boolean menerima(String metode) {
            return metode.equals("GET");
        }

        @Override
        public String isi(String jalur) {
            return "selamat datang di toko Budi";
        }
    }

    static class DaftarProduk implements ServletMini {
        private int panggilan = 0;

        @Override
        public boolean menerima(String metode) {
            return metode.equals("GET");
        }

        @Override
        public String isi(String jalur) {
            panggilan++;
            return "daftar produk (panggilan ke-" + panggilan
                    + " pada instance yang sama): buku, pena, tas";
        }
    }

    static class Wadah {
        private final Map<String, ServletMini> rute = new LinkedHashMap<>();

        void daftarkan(String pola, ServletMini servlet) {
            rute.put(pola, servlet);
        }

        String tangani(String metode, String jalur) {
            ServletMini servlet = rute.get(jalur);
            if (servlet == null) {
                return "404 Not Found: tidak ada servlet pada " + jalur;
            }
            if (!servlet.menerima(metode)) {
                return "405 Method Not Allowed: servlet tidak menerima " + metode;
            }
            return "200 OK: " + servlet.isi(jalur);
        }
    }

    public static void main(String[] args) {
        Wadah wadah = new Wadah();
        wadah.daftarkan("/", new HalamanDepan());
        wadah.daftarkan("/produk", new DaftarProduk());

        System.out.println("== Wadah memetakan pola URL ke servlet ==");
        System.out.println("/       -> HalamanDepan");
        System.out.println("/produk -> DaftarProduk");

        System.out.println();
        System.out.println("== Permintaan ditangani wadah ==");
        String[][] permintaan = {
                {"GET", "/"}, {"GET", "/produk"}, {"POST", "/produk"},
                {"GET", "/produk"}, {"GET", "/pelanggan"}};
        for (String[] p : permintaan) {
            System.out.printf("%-5s %-12s -> %s%n", p[0], p[1], wadah.tangani(p[0], p[1]));
        }
    }
}