Siklus hidup servlet sudah jelas; sekarang kita masuk ke isi pekerjaan sehari-hari: membaca data yang dikirim klien dan menulis balasan yang dikirim kembali. Di satu sisi ada HttpServletRequest yang membungkus seluruh permintaan — parameter, header, cookie, dan body. Di sisi lain ada HttpServletResponse yang kita isi mulai dari status code sampai byte terakhir body. Dua arah ini punya aturan encoding yang berbeda: data masuk tiba dalam bentuk ter-encode, data keluar harus ditulis dengan charset yang kita umumkan. Chapter ini menguraikan kedua arah tersebut dan membuktikan mekanismenya dengan pengurai parameter yang berjalan di Java murni.
Membaca Permintaan #
Sebagian besar data permintaan web berukuran kecil dan sudah diurai container untuk kita. Pada servlet nyata, pembacaannya berbentuk seperti ini:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// parameter: dari query string ATAU body form-urlencoded
String nama = req.getParameter("nama"); // nilai pertama
String[] minat = req.getParameterValues("minat"); // semua nilai
// header
String agen = req.getHeader("User-Agent");
// body mentah (JSON/XML): baca sebagai stream
String body = new String(req.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
}Method getParameter menyatukan dua sumber berbeda: query string pada URL (visible di address bar) dan body bertipe application/x-www-form-urlencoded (dikirim form HTML biasa). Parameter bernama sama boleh muncul berulang — pada contoh di atas minat dua kali — sehingga ada dua method akses: satu nilai pertama, satu nilai lengkap. Body bertipe lain (JSON, XML, unggahan berkas) tidak diurai container; kita membacanya mentah dari getInputStream() atau getReader() lalu menguraikannya sendiri atau menyerahkannya ke pustaka seperti JSON-B.
Aturan Encoding Form Urlencoded #
Format a=1&b=dua membawa karakter yang dilarang di URL, sehingga nilai disandikan: spasi menjadi %20 (atau + pada konteks form), @ menjadi %40, dan seterusnya. Kelas java.net.URLEncoder menuliskannya, URLDecoder membacanya kembali — keduanya sudah kita pakai pada Chapter URI dan URL. Poin yang sering terlewat: decoding adalah tanggung jawab sisi pembaca. Container melakukan decoding untuk getParameter, tetapi saat mengurai query string sendiri (misalnya di filter atau interceptor), kita wajib memanggil URLDecoder sebelum memakai nilainya.
Menulis Balasan #
Balasan ditulis melalui tiga pintu HttpServletResponse: status code, header, dan body. Urutan penting — header harus selesai sebelum byte pertama body ditulis, karena header secara teknis berada di depan body di dalam pesan HTTP:
resp.setStatus(200);
resp.setContentType("application/json; charset=UTF-8");
resp.setHeader("X-Dibuat-Oleh", "toko-budi");
PrintWriter out = resp.getWriter();
out.write("{\"sapa\":\"Halo\"}");
// dua pintu khusus yang jarang kita tulis manual:
// resp.sendRedirect("/login"); -> balasan 302 + header Location
// resp.sendError(404); -> halaman galat bawaan containersetContentType wajib menyertakan charset sebelum getWriter() dipanggil — PrintWriter mengambil encoding dari content type tersebut, dan menyetelnya setelah penulisan dimulai tidak berefek.
Simulasi Membaca dan Menulis Tanpa Container #
Program berikut meniru kedua arah tersebut di Java murni. BacaTulisHttp mengurai query string dan body form dengan URLDecoder, menunjukkan perilaku parameter berulang, lalu membangun balasan dengan Content-Length yang dihitung dari byte; penanda akhir baris \r\n dicetak sebagai teks terlihat:
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class BacaTulisHttp {
static Map<String, List<String>> uraiParameter(String mentah) {
Map<String, List<String>> hasil = new LinkedHashMap<>();
if (mentah == null || mentah.isEmpty()) {
return hasil;
}
for (String pasangan : mentah.split("&")) {
int samaDengan = pasangan.indexOf('=');
String kunci = samaDengan >= 0
? pasangan.substring(0, samaDengan) : pasangan;
String nilai = samaDengan >= 0
? pasangan.substring(samaDengan + 1) : "";
kunci = URLDecoder.decode(kunci, StandardCharsets.UTF_8);
nilai = URLDecoder.decode(nilai, StandardCharsets.UTF_8);
hasil.computeIfAbsent(kunci, k -> new ArrayList<>()).add(nilai);
}
return hasil;
}
static String getParameter(Map<String, List<String>> parameter, String nama) {
List<String> nilai = parameter.get(nama);
return nilai == null ? null : nilai.get(0);
}
public static void main(String[] args) {
String queryString = "nama=Budi%20Santoso&minat=buku&minat=musik&kota=Bandung";
String bodyForm = "email=budi%40example.com&pesan=selamat%20pagi+toko";
System.out.println("== Parameter dari query string ==");
uraiParameter(queryString).forEach(
(kunci, nilai) -> System.out.println(kunci + " = " + nilai));
System.out.println();
System.out.println("== Parameter dari body form ==");
uraiParameter(bodyForm).forEach(
(kunci, nilai) -> System.out.println(kunci + " = " + nilai));
System.out.println();
System.out.println("== getParameter mengambil nilai pertama ==");
System.out.println("nama = " + getParameter(uraiParameter(queryString), "nama"));
System.out.println("minat = " + getParameter(uraiParameter(queryString), "minat"));
System.out.println();
System.out.println("== Response yang ditulis server (penanda \\r\\n ditampilkan eksplisit) ==");
String isi = "{\"sapa\":\"Halo Budi Santoso\"}";
byte[] byteIsi = isi.getBytes(StandardCharsets.UTF_8);
StringBuilder respons = new StringBuilder();
respons.append("HTTP/1.1 200 OK\r\n");
respons.append("Content-Type: application/json; charset=utf-8\r\n");
respons.append("Content-Length: ").append(byteIsi.length).append("\r\n");
respons.append("\r\n");
respons.append(isi);
System.out.print(respons.toString().replace("\r\n", "\\r\\n\n"));
System.out.println();
}
}Output yang Dihasilkan:
== Parameter dari query string ==
nama = [Budi Santoso]
minat = [buku, musik]
kota = [Bandung]
== Parameter dari body form ==
email = [[email protected]]
pesan = [selamat pagi toko]
== getParameter mengambil nilai pertama ==
nama = Budi Santoso
minat = buku
== Response yang ditulis server (penanda \r\n ditampilkan eksplisit) ==
HTTP/1.1 200 OK\r\n
Content-Type: application/json; charset=utf-8\r\n
Content-Length: 28\r\n
\r\n
{"sapa":"Halo Budi Santoso"}Bedah bagian-bagian penting program tersebut:
- Baris 10-26 (
uraiParameter): fungsi pengurai berperan sebagaigetParameterMapversi kita. String dipecah dengansplit("&"), tiap pasangan dipisah pada tanda sama dengan, lalu kunci dan nilai masing-masing di-decode. - Baris 16-20 (
indexOf('=')): pemisahan memakaiindexOf, bukansplit("="), karena nilai bisa berisi tanda sama dengan lagi (misalnya token base64) — hanya kemunculan pertama yang adalah pemisah. - Baris 21-22 (
URLDecoder.decode(..., StandardCharsets.UTF_8)): decoding dilakukan dengan charset eksplisit.%20menjadi spasi dan%40menjadi@pada output; pada body form, tanda+juga diterjemahkan menjadi spasi — perilakuURLDecoderyang memang dirancang untuk format form. - Baris 23 (
computeIfAbsent(...).add(nilai)): kunci yang muncul dua kali (minat) menghasilkan daftar nilai, bukan satu nilai yang tertimpa. StrukturMap<String, List<String>>inilah bentuk asligetParameterMap(). - Baris 28-31 (
getParameter): method bantu mengambil nilai pertama, persis perilakugetParameterdi servlet API — output membuktikanminatmengembalikanbukumeskipun nilainya juga memuatmusik. - Baris 55-61 (
byteIsidanContent-Length): panjang body dihitung dari jumlah byte UTF-8, bukan jumlah karakter — pelajaran yang sama dengan Chapter HTTP Fundamentals, kini dari sisi penulis balasan. Baris 61 mencetak penanda\r\nsebagai teks eksplisit sehingga baris kosong pemisah header dan body terlihat pada output.
Dua arah baca-tulis kini beres, namun ada persoalan yang belum tersentuh: protokol HTTP tidak mengingat siapa pun. getParameter membaca data yang dikirim, tetapi tidak ada data yang datang sendiri dari sesi sebelumnya. Untuk itu kita membutuhkan mekanisme memori di atas protokol — cookie di sisi klien dan session di sisi server — topik Chapter Sessions dan Cookies.
Referensi #
- Jakarta Servlet 6.1 Specification
- Jakarta Servlet 6.1 API Documentation
- Oracle: Class URLDecoder (Java SE 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BacaTulisHttp.java
Unduh berkasimport java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class BacaTulisHttp {
static Map<String, List<String>> uraiParameter(String mentah) {
Map<String, List<String>> hasil = new LinkedHashMap<>();
if (mentah == null || mentah.isEmpty()) {
return hasil;
}
for (String pasangan : mentah.split("&")) {
int samaDengan = pasangan.indexOf('=');
String kunci = samaDengan >= 0
? pasangan.substring(0, samaDengan) : pasangan;
String nilai = samaDengan >= 0
? pasangan.substring(samaDengan + 1) : "";
kunci = URLDecoder.decode(kunci, StandardCharsets.UTF_8);
nilai = URLDecoder.decode(nilai, StandardCharsets.UTF_8);
hasil.computeIfAbsent(kunci, k -> new ArrayList<>()).add(nilai);
}
return hasil;
}
static String getParameter(Map<String, List<String>> parameter, String nama) {
List<String> nilai = parameter.get(nama);
return nilai == null ? null : nilai.get(0);
}
public static void main(String[] args) {
String queryString = "nama=Budi%20Santoso&minat=buku&minat=musik&kota=Bandung";
String bodyForm = "email=budi%40example.com&pesan=selamat%20pagi+toko";
System.out.println("== Parameter dari query string ==");
uraiParameter(queryString).forEach(
(kunci, nilai) -> System.out.println(kunci + " = " + nilai));
System.out.println();
System.out.println("== Parameter dari body form ==");
uraiParameter(bodyForm).forEach(
(kunci, nilai) -> System.out.println(kunci + " = " + nilai));
System.out.println();
System.out.println("== getParameter mengambil nilai pertama ==");
System.out.println("nama = " + getParameter(uraiParameter(queryString), "nama"));
System.out.println("minat = " + getParameter(uraiParameter(queryString), "minat"));
System.out.println();
System.out.println("== Response yang ditulis server (penanda \\r\\n ditampilkan eksplisit) ==");
String isi = "{\"sapa\":\"Halo Budi Santoso\"}";
byte[] byteIsi = isi.getBytes(StandardCharsets.UTF_8);
StringBuilder respons = new StringBuilder();
respons.append("HTTP/1.1 200 OK\r\n");
respons.append("Content-Type: application/json; charset=utf-8\r\n");
respons.append("Content-Length: ").append(byteIsi.length).append("\r\n");
respons.append("\r\n");
respons.append(isi);
System.out.print(respons.toString().replace("\r\n", "\\r\\n\n"));
System.out.println();
}
}