Chapter sebelumnya menutup dengan fakta penting: web container hanya membuat satu instance untuk setiap deklarasi servlet. Pertanyaan lanjutannya adalah bagaimana instance itu dibuat, siapa yang memanggil methodnya, dan kapan instance tersebut dibuang. Seluruh urutan tersebut dikelola container lewat siklus hidup servlet: init() dipanggil satu kali sebelum servlet siap, service() dipanggil untuk setiap permintaan, dan destroy() dipanggil satu kali saat servlet diberhentikan. Memahami urutan ini bukan pengetahuan opsional — pola state yang aman atau rawan masalah pada servlet sepenuhnya ditentukan oleh siklus hidup tersebut.
Tiga Fase Hidup Servlet #
Siklus hidup servlet ditetapkan oleh Jakarta Servlet Specification dan dijalankan oleh container:
| Fase | Method | Dipanggil kapan | Kewajiban kita |
|---|---|---|---|
| Pemuatan | init(ServletConfig) | sekali, sebelum permintaan pertama | siapkan resource mahal: koneksi pool, konfigurasi |
| Pelayanan | service(req, resp) | setiap permintaan, bisa paralel | proses permintaan tanpa shared state bebas |
| Pemberhentian | destroy() | sekali, saat aplikasi dimatikan atau di-deploy ulang | lepaskan resource: tutup pool, simpan statistik |
Method init() menerima ServletConfig yang membawa parameter inisialisasi dan akses ke ServletContext — objek lingkungan aplikasi tempat servlet tersebut hidup. Bila init() melempar ServletException, container membatalkan pemuatan dan servlet tidak akan menerima permintaan apa pun; permintaan yang datang akan dibalas galat. Elemen load-on-startup pada pemetaan menentukan bahwa servlet dimuat saat aplikasi dinyalakan, bukan ditunda sampai permintaan pertama — berguna untuk inisialisasi yang lambat seperti membangun cache.
Antara init dan destroy, method service() dijalankan berkali-kali. Pada HttpServlet, service() membaca method HTTP lalu meneruskan ke doGet(), doPost(), dan sejenisnya. Container mengelola thread pool, sehingga dua permintaan dapat masuk bersamaan pada objek yang sama — detail yang membuat kolom ketiga tabel di atas menjadi sangat serius.
Jebakan Variabel Instance #
Mari kita buktikan bahaya shared state dengan simulasi siklus hidup lengkap. SiklusServlet menjalankan dua versi servlet layar tamu: versi rawan yang menyimpan nama pengguna terakhir di variabel instance, dan versi aman yang menyimpannya di variabel lokal:
public class SiklusServlet {
static class LayarTamuRawan {
// simulasi servlet: SATU instance melayani BANYAK permintaan
private int jumlahPanggilan = 0;
private String penggunaTerakhir = "(belum ada)";
void init() {
System.out.println("[init] dipanggil sekali, servlet siap melayani");
}
String service(String pengguna) {
jumlahPanggilan++;
System.out.println("[service] halo " + pengguna
+ " (permintaan ke-" + jumlahPanggilan + ")");
penggunaTerakhir = pengguna;
return penggunaTerakhir;
}
void destroy() {
System.out.println("[destroy] dipanggil sekali saat wadah berhenti, total permintaan = "
+ jumlahPanggilan);
}
}
static class LayarTamuAman {
private int jumlahPanggilan = 0;
void init() {
System.out.println("[init] dipanggil sekali, servlet siap melayani");
}
String service(String pengguna) {
jumlahPanggilan++;
String sapaan = "halo " + pengguna; // state per permintaan = variabel lokal
System.out.println("[service] " + sapaan
+ " (permintaan ke-" + jumlahPanggilan + ")");
return sapaan;
}
void destroy() {
System.out.println("[destroy] dipanggil sekali saat wadah berhenti, total permintaan = "
+ jumlahPanggilan);
}
}
public static void main(String[] args) {
System.out.println("== Versi rawan: state pernah menempel di instance ==");
LayarTamuRawan rawan = new LayarTamuRawan();
rawan.init();
rawan.service("Budi");
rawan.service("Siti");
rawan.service("Dewi");
System.out.println("[kondisi] variabel instance penggunaTerakhir = "
+ rawan.penggunaTerakhir + " (milik permintaan sebelumnya!)");
rawan.destroy();
System.out.println();
System.out.println("== Versi aman: state hidup di variabel lokal ==");
LayarTamuAman aman = new LayarTamuAman();
aman.init();
aman.service("Budi");
aman.service("Siti");
aman.service("Dewi");
aman.destroy();
}
}Output yang Dihasilkan:
== Versi rawan: state pernah menempel di instance ==
[init] dipanggil sekali, servlet siap melayani
[service] halo Budi (permintaan ke-1)
[service] halo Siti (permintaan ke-2)
[service] halo Dewi (permintaan ke-3)
[kondisi] variabel instance penggunaTerakhir = Dewi (milik permintaan sebelumnya!)
[destroy] dipanggil sekali saat wadah berhenti, total permintaan = 3
== Versi aman: state hidup di variabel lokal ==
[init] dipanggil sekali, servlet siap melayani
[service] halo Budi (permintaan ke-1)
[service] halo Siti (permintaan ke-2)
[service] halo Dewi (permintaan ke-3)
[destroy] dipanggil sekali saat wadah berhenti, total permintaan = 3Bedah baris-per-baris untuk kedua versi tersebut:
- Baris 5-6 (
jumlahPanggilandanpenggunaTerakhir): dua variabel instance dengan nasib berbeda. Penghitung jumlah panggilan boleh menjadi variabel instance — nilai kumulatif memang dimaksudkan lintas permintaan. SebaliknyapenggunaTerakhirmilik satu permintaan tertentu, sehingga penempatannya di instance adalah kesalahan desain. - Baris 12-18 (
serviceversi rawan): setiap permintaan menimpapenggunaTerakhir. Baris[kondisi]pada output membuktikan setelah tiga permintaan, nilai yang tersimpan adalah “Dewi” — sisa permintaan terakhir. Pada container sungguhan dengan thread paralel, pembacaan dan penulisan beruntun dari banyak thread bahkan dapat menghasilkan gabungan yang tidak termasuk permintaan mana pun. - Baris 33-38 (
serviceversi aman):sapaandideklarasikan sebagai variabel lokal di dalam method. Setiap pemanggilanservice()mendapat salinan stack framenya sendiri, sehingga data satu permintaan tidak mungkin terlihat oleh permintaan lain. Inilah aturan emas servlet: data per permintaan hidup di variabel lokal atau di objek request, bukan di instance. - Baris 47-56 (
mainversi rawan): urutan pemanggilan meniru container:init()satu kali,service()tiga kali,destroy()satu kali. Urutan ini persis yang digariskan spesifikasi.
destroy dan Batas Siklus #
Method destroy() dipanggil container ketika aplikasi dimatikan atau di-deploy ulang, dan container menjamin tidak ada lagi service() yang berjalan setelahnya (permintaan yang sedang diproses diberi waktu menyelesaikan diri lebih dulu). Di sinilah tempat melepas sumber daya yang dipegang sejak init(): menutup pool koneksi, mem-batalkan thread latar, atau mem-flush statistik ke penyimpanan. Container modern menjalankan urutan init-service-destroy berulang kali dalam satu proses karena auto-deployment — siklus yang sama berlaku setiap kali versi baru aplikasi naik.
Siklus hidup menjelaskan pula mengapa servlet API kaku soal waktu: objek HttpServletRequest hanya valid selama pemanggilan service(). Menyimpan objek request ke variabel instance untuk dipakai nanti akan berakhir dengan data usang. Kini saatnya melihat isi objek itu sendiri — bagaimana parameter, header, dan body permintaan dibaca, serta bagaimana balasan ditulis — topik Chapter Request dan Response.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
SiklusServlet.java
Unduh berkaspublic class SiklusServlet {
static class LayarTamuRawan {
// simulasi servlet: SATU instance melayani BANYAK permintaan
private int jumlahPanggilan = 0;
private String penggunaTerakhir = "(belum ada)";
void init() {
System.out.println("[init] dipanggil sekali, servlet siap melayani");
}
String service(String pengguna) {
jumlahPanggilan++;
System.out.println("[service] halo " + pengguna
+ " (permintaan ke-" + jumlahPanggilan + ")");
penggunaTerakhir = pengguna;
return penggunaTerakhir;
}
void destroy() {
System.out.println("[destroy] dipanggil sekali saat wadah berhenti, total permintaan = "
+ jumlahPanggilan);
}
}
static class LayarTamuAman {
private int jumlahPanggilan = 0;
void init() {
System.out.println("[init] dipanggil sekali, servlet siap melayani");
}
String service(String pengguna) {
jumlahPanggilan++;
String sapaan = "halo " + pengguna; // state per permintaan = variabel lokal
System.out.println("[service] " + sapaan
+ " (permintaan ke-" + jumlahPanggilan + ")");
return sapaan;
}
void destroy() {
System.out.println("[destroy] dipanggil sekali saat wadah berhenti, total permintaan = "
+ jumlahPanggilan);
}
}
public static void main(String[] args) {
System.out.println("== Versi rawan: state pernah menempel di instance ==");
LayarTamuRawan rawan = new LayarTamuRawan();
rawan.init();
rawan.service("Budi");
rawan.service("Siti");
rawan.service("Dewi");
System.out.println("[kondisi] variabel instance penggunaTerakhir = "
+ rawan.penggunaTerakhir + " (milik permintaan sebelumnya!)");
rawan.destroy();
System.out.println();
System.out.println("== Versi aman: state hidup di variabel lokal ==");
LayarTamuAman aman = new LayarTamuAman();
aman.init();
aman.service("Budi");
aman.service("Siti");
aman.service("Dewi");
aman.destroy();
}
}