Setiap request HTTP yang tiba di aplikasi Spring Boot berbasis servlet melewati satu pintu: DispatcherServlet. Pola ini bernama front controller — satu komponen menerima seluruh permintaan, lalu mendelegasikan pekerjaan ke bagian-bagian yang tepat. Dari luar, Spring MVC terasa seperti anotasi yang bekerja otomatis pada method controller; dari dalam, yang terjadi adalah pipeline teratur yang bisa kita lacak baris demi baris. Pada Chapter ini kita bedah alur lengkapnya, peran tiap komponen, dan mekanisme content negotiation yang menentukan respons JSON atau HTML.
Alur Request di Balik Layar #
Saat Tomcat tertanam menerima request, servlet container meneruskannya ke DispatcherServlet. Dari situ alurnya berjalan melalui komponen-komponen berikut:
- HandlerMapping memeriksa method dan path request terhadap tabel pemetaan anotasi
@GetMapping,@PostMapping, dan sejenisnya. Hasilnya satu handler — atau kosong, yang berujung respons 404. - HandlerAdapter memanggil method controller yang terpilih. Adapter inilah yang menyiapkan argumen method: memecah path variable, mengurai query param, mendeserialisasi body JSON menjadi objek.
- Nilai kembalian controller diterjemahkan. Untuk
@RestController,HttpMessageConverter(Jackson untuk JSON) menulis objek langsung ke body respons. Untuk controller yang mengembalikan nama view,ViewResolvermencari template dan model diisi sebelum HTML dirender.
Pemisahan peran ini menjelaskan fleksibilitas Spring MVC: kita bisa mengganti Jackson dengan pustaka lain, menambah filter, atau mengubah resolusi view tanpa menyentuh kode controller.
Controller dalam Praktik #
Bentuk nyata handler pada Spring Boot 3.4/3.5 terlihat seperti berikut — ditampilkan apa adanya:
@RestController
@RequestMapping("/api/produk")
public class ProdukController {
private final ProdukService service;
public ProdukController(ProdukService service) {
this.service = service;
}
@GetMapping("/{id}")
public Produk detail(@PathVariable Long id) {
return service.cari(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}Anotasi @RestController menggabungkan @Controller dan @ResponseBody: hasil method ditulis ke body respons sebagai objek ter-serialisasi, bukan nama template. Detail anotasi routing dan binding-nya kita bahas pada dua Chapter berikutnya; fokus kita di sini adalah mesin yang menggerakkan anotasi tersebut.
Content Negotiation #
Satu endpoint bisa melayani JSON untuk aplikasi mobile dan HTML untuk browser. DispatcherServlet menentukan format lewat content negotiation: prioritas awal adalah header Accept dari klien, lalu sufiks path, lalu parameter ?format= bila diaktifkan. Jika klien mengirim Accept: application/json, MappingJackson2HttpMessageConverter yang dipilih untuk menulis respons; jika text/html, resolusi view yang mengambil alih. Hasilnya: controller tetap satu, representasinya banyak.
DispatcherServlet Mini #
Alur lima langkah di atas layak dilihat bekerja secara mekanis. Program Java murni berikut membangun front controller mini: tabel handler berbentuk Map<String, Function<Permintaan, Respons>>, dengan pencarian handler, pemanggilan, dan fallback 404:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
public class DispatcherMini {
record Permintaan(String method, String path) {
String kunci() {
return method + " " + path;
}
}
record Respons(int status, String tipeKonten, String badan) { }
private final Map<String, Function<Permintaan, Respons>> petaHandler;
DispatcherMini(Map<String, Function<Permintaan, Respons>> petaHandler) {
this.petaHandler = petaHandler;
}
public static void main(String[] args) {
// HandlerMapping mini: kunci "METHOD /path" -> handler.
Map<String, Function<Permintaan, Respons>> petaHandler = new LinkedHashMap<>();
petaHandler.put("GET /produk", req -> new Respons(200, "application/json",
"{\"id\":1,\"nama\":\"Kopi Gayo 250g\"}"));
petaHandler.put("GET /pesanan", req -> new Respons(200, "application/json",
"{\"id\":77,\"status\":\"DIKIRIM\"}"));
petaHandler.put("POST /pesanan", req -> new Respons(201, "application/json",
"{\"id\":78,\"status\":\"BARU\"}"));
DispatcherMini dispatcher = new DispatcherMini(petaHandler);
dispatcher.layani(new Permintaan("GET", "/produk"));
dispatcher.layani(new Permintaan("POST", "/pesanan"));
dispatcher.layani(new Permintaan("DELETE", "/pesanan/78"));
}
// Front controller: seluruh permintaan lewat satu titik ini.
void layani(Permintaan permintaan) {
System.out.println("1) DispatcherServlet menerima " + permintaan.kunci());
Function<Permintaan, Respons> handler = petaHandler.get(permintaan.kunci());
System.out.println("2) HandlerMapping mencari handler -> "
+ (handler != null ? "ditemukan" : "tidak ada"));
Respons respons;
if (handler == null) {
// Tidak ada handler: fallback 404 ala ResponseStatusException.
System.out.println("3) handler tidak ditemukan -> respons 404 dibuat");
respons = new Respons(404, "application/problem+json",
"{\"title\":\"Not Found\",\"status\":404}");
} else {
System.out.println("3) HandlerAdapter memanggil method controller");
respons = handler.apply(permintaan);
System.out.println("4) HttpMessageConverter menulis body "
+ respons.tipeKonten());
}
System.out.println("-> respons: " + respons.status() + " "
+ respons.tipeKonten() + " " + respons.badan());
System.out.println();
}
}Output yang Dihasilkan:
1) DispatcherServlet menerima GET /produk
2) HandlerMapping mencari handler -> ditemukan
3) HandlerAdapter memanggil method controller
4) HttpMessageConverter menulis body application/json
-> respons: 200 application/json {"id":1,"nama":"Kopi Gayo 250g"}
1) DispatcherServlet menerima POST /pesanan
2) HandlerMapping mencari handler -> ditemukan
3) HandlerAdapter memanggil method controller
4) HttpMessageConverter menulis body application/json
-> respons: 201 application/json {"id":78,"status":"BARU"}
1) DispatcherServlet menerima DELETE /pesanan/78
2) HandlerMapping mencari handler -> tidak ada
3) handler tidak ditemukan -> respons 404 dibuat
-> respons: 404 application/problem+json {"title":"Not Found","status":404}Mari kita bedah komponen simulasi ini:
record Permintaan(String method, String path)dengankunci(): permintaan diwakili tiga nilai dan diringkas menjadi kunci"GET /produk".HandlerMappingnyata bekerja denganRequestMappingInfoyang jauh lebih kaya — mendukung path variable dan parameter — tetapi prinsip pencocokannya sama: satu kunci permintaan dicocokkan ke tabel.Map<String, Function<Permintaan, Respons>> petaHandler: tabel pemetaan diinisialisasi dengan tiga handler. Di Spring nyata, tabel ini diisi otomatis saat startup dari hasil pemindaian anotasi; di sini kita mengisinya manual agar terlihat.layani(Permintaan): method mencetak empat langkah pipeline untuk setiap request — penerimaan, pencarian handler, pemanggilan, penulisan body. Baris4) HttpMessageConverter menulis bodyhanya muncul bila handler ada, menirui konverter yang hanya bekerja pada hasil controller.- Cabang
handler == null: permintaanDELETE /pesanan/78tidak menemukan handler, sehingga fallback membuat respons 404 bertipeapplication/problem+json— format standar RFC 7807 yang juga dipakai Spring Boot untuk respons kesalahan bawaan.
Dua permintaan pertama menunjukkan pipeline penuh; permintaan ketiga menunjukkan jalur gagal yang tetap tertangani rapi oleh front controller — tidak ada request yang bocor tanpa respons.
Referensi #
- Spring Framework Reference: Web Servlet
- Spring Framework Reference: Annotated Controllers
- Spring Guides: Building a RESTful Web Service
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
DispatcherMini.java
Unduh berkasimport java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
public class DispatcherMini {
record Permintaan(String method, String path) {
String kunci() {
return method + " " + path;
}
}
record Respons(int status, String tipeKonten, String badan) { }
private final Map<String, Function<Permintaan, Respons>> petaHandler;
DispatcherMini(Map<String, Function<Permintaan, Respons>> petaHandler) {
this.petaHandler = petaHandler;
}
public static void main(String[] args) {
// HandlerMapping mini: kunci "METHOD /path" -> handler.
Map<String, Function<Permintaan, Respons>> petaHandler = new LinkedHashMap<>();
petaHandler.put("GET /produk", req -> new Respons(200, "application/json",
"{\"id\":1,\"nama\":\"Kopi Gayo 250g\"}"));
petaHandler.put("GET /pesanan", req -> new Respons(200, "application/json",
"{\"id\":77,\"status\":\"DIKIRIM\"}"));
petaHandler.put("POST /pesanan", req -> new Respons(201, "application/json",
"{\"id\":78,\"status\":\"BARU\"}"));
DispatcherMini dispatcher = new DispatcherMini(petaHandler);
dispatcher.layani(new Permintaan("GET", "/produk"));
dispatcher.layani(new Permintaan("POST", "/pesanan"));
dispatcher.layani(new Permintaan("DELETE", "/pesanan/78"));
}
// Front controller: seluruh permintaan lewat satu titik ini.
void layani(Permintaan permintaan) {
System.out.println("1) DispatcherServlet menerima " + permintaan.kunci());
Function<Permintaan, Respons> handler = petaHandler.get(permintaan.kunci());
System.out.println("2) HandlerMapping mencari handler -> "
+ (handler != null ? "ditemukan" : "tidak ada"));
Respons respons;
if (handler == null) {
// Tidak ada handler: fallback 404 ala ResponseStatusException.
System.out.println("3) handler tidak ditemukan -> respons 404 dibuat");
respons = new Respons(404, "application/problem+json",
"{\"title\":\"Not Found\",\"status\":404}");
} else {
System.out.println("3) HandlerAdapter memanggil method controller");
respons = handler.apply(permintaan);
System.out.println("4) HttpMessageConverter menulis body "
+ respons.tipeKonten());
}
System.out.println("-> respons: " + respons.status() + " "
+ respons.tipeKonten() + " " + respons.badan());
System.out.println();
}
}