Web Server #
Di balik setiap aplikasi web — dari toko online hingga dashboard monitoring — ada sebuah proses yang mendengarkan port TCP, menerima request HTTP, memprosesnya, dan mengirimkan response kembali. Itulah yang dilakukan web server. Di ekosistem Java, ada tiga pendekatan utama: Servlet API yang merupakan standar fondasi selama dua dekade, Spring Boot yang menjadi pilihan dominan untuk aplikasi enterprise modern, dan Jetty yang bisa di-embed langsung ke dalam aplikasi Java biasa. Artikel ini membahas cara kerja masing-masing — dari servlet sederhana hingga REST API lengkap dengan routing, request parsing, response formatting, filter, dan konfigurasi untuk produksi.
Gambaran Umum #
Setiap request HTTP melewati lapisan-lapisan sebelum kode aplikasimu dieksekusi:
flowchart LR
A["Browser / API Client"] -->|"HTTP Request"| B["Web Server\n(Tomcat / Jetty / Netty)"]
B --> C["Filter / Middleware\n(auth, logging, CORS)"]
C --> D["Servlet / Controller\n(logika aplikasi)"]
D --> E["Response\n(JSON / HTML / file)"]
E --> A| Pendekatan | Cocok untuk | Server | Konfigurasi |
|---|---|---|---|
| Servlet API | Aplikasi enterprise lama, belajar dasar HTTP | Tomcat, Jetty, WildFly | web.xml atau anotasi |
| Spring Boot | Aplikasi modern, REST API, microservice | Tomcat/Jetty embedded | Auto-configuration |
| Jetty embedded | Tool internal, aplikasi standalone ringan | Jetty embedded | Kode Java |
Servlet API #
Servlet adalah komponen Java yang menangani request HTTP secara langsung. Ini adalah lapisan paling dasar — semua framework Java web (termasuk Spring) pada akhirnya berjalan di atas Servlet API. Memahami servlet berarti memahami cara kerja HTTP di Java.
Dependensi #
<!-- Maven — Jakarta Servlet API (untuk Tomcat 10+) -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope> <!-- disediakan oleh server, tidak ikut ke WAR -->
</dependency>
Membuat Servlet Dasar #
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
// @WebServlet mendaftarkan servlet ke path /hello tanpa perlu web.xml
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
// doGet: tangani request HTTP GET
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// Baca parameter dari query string: /hello?nama=Budi
String nama = req.getParameter("nama");
if (nama == null || nama.isBlank()) nama = "Dunia";
// Set tipe konten response
res.setContentType("text/html; charset=UTF-8");
res.setCharacterEncoding("UTF-8");
// Tulis body response
PrintWriter out = res.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><body>");
out.println("<h1>Halo, " + escapedHtml(nama) + "!</h1>");
out.println("</body></html>");
}
// doPost: tangani request HTTP POST
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// Baca body form dari POST
String nama = req.getParameter("nama");
String email = req.getParameter("email");
// Set response sebagai JSON
res.setContentType("application/json; charset=UTF-8");
res.setStatus(HttpServletResponse.SC_CREATED); // 201
res.getWriter().println("""
{"status": "ok", "nama": "%s", "email": "%s"}
""".formatted(nama, email));
}
private String escapedHtml(String input) {
return input
.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
}
}
Membaca Request #
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// Path dan method
String method = req.getMethod(); // GET, POST, PUT, dll.
String path = req.getRequestURI(); // /api/produk/123
String query = req.getQueryString(); // id=123&sort=asc
// Parameter query string
String id = req.getParameter("id");
String[] tags = req.getParameterValues("tag"); // ?tag=a&tag=b → ["a", "b"]
// Header
String contentType = req.getHeader("Content-Type");
String userAgent = req.getHeader("User-Agent");
String bearer = req.getHeader("Authorization"); // "Bearer <token>"
// Informasi klien
String ip = req.getRemoteAddr();
String host = req.getServerName();
int port = req.getServerPort();
// Path variable dari URL (dengan pattern /produk/*)
String pathInfo = req.getPathInfo(); // "/123" untuk URL /produk/123
// Session
var sesi = req.getSession(false); // false = jangan buat baru jika belum ada
if (sesi != null) {
String userId = (String) sesi.getAttribute("userId");
}
}
Menulis Response #
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// Status code
res.setStatus(200); // OK
res.setStatus(HttpServletResponse.SC_OK); // lebih readable
res.sendError(404, "Produk tidak ditemukan"); // set status + body error
// Header response
res.setHeader("Cache-Control", "no-cache, no-store");
res.setHeader("X-Request-Id", java.util.UUID.randomUUID().toString());
res.addHeader("Set-Cookie", "sessionId=abc123; HttpOnly; Secure");
// Redirect
res.sendRedirect("/login"); // 302 redirect
res.sendRedirect("https://example.com"); // redirect ke URL lain
// Body JSON
res.setContentType("application/json; charset=UTF-8");
res.getWriter().println("{\"id\": 1, \"nama\": \"Laptop\"}");
}
Filter — Middleware untuk Servlet #
Filter adalah komponen yang berjalan sebelum dan sesudah servlet — cocok untuk logging, autentikasi, CORS, atau kompresi.
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.time.Instant;
// Terapkan ke semua URL
@WebFilter("/*")
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpServletResponse httpRes = (HttpServletResponse) res;
long mulai = System.currentTimeMillis();
String path = httpReq.getRequestURI();
// Tambahkan CORS header
httpRes.setHeader("Access-Control-Allow-Origin", "*");
httpRes.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// Handle preflight OPTIONS request
if ("OPTIONS".equalsIgnoreCase(httpReq.getMethod())) {
httpRes.setStatus(HttpServletResponse.SC_OK);
return;
}
// Lanjutkan ke servlet berikutnya
chain.doFilter(req, res);
// Log setelah servlet selesai
long durasi = System.currentTimeMillis() - mulai;
System.out.printf("[%s] %s %s → %d (%dms)%n",
Instant.now(), httpReq.getMethod(), path, httpRes.getStatus(), durasi);
}
}
Menjalankan di Tomcat #
# Paket menjadi WAR
mvn package -Dpackaging=war
# Deploy ke Tomcat (salin ke webapps/)
cp target/aplikasi.war $TOMCAT_HOME/webapps/
# Akses di: http://localhost:8080/aplikasi/hello
Spring Boot #
Spring Boot adalah cara paling produktif untuk membangun web server di Java saat ini. Ia menyematkan Tomcat secara otomatis, mengonfigurasi banyak hal dengan convention over configuration, dan menyediakan ekosistem lengkap — dari security hingga database. Kamu tidak perlu server eksternal: cukup jalankan main() dan server sudah berjalan.
Dependensi dan Setup #
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
// Titik masuk aplikasi
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Aplikasi {
public static void main(String[] args) {
SpringApplication.run(Aplikasi.class, args);
// Tomcat berjalan di port 8080
}
}
REST Controller #
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import java.util.*;
@RestController // = @Controller + @ResponseBody
@RequestMapping("/api/produk") // semua endpoint berawalan /api/produk
public class ProdukController {
// Simulasi data (di aplikasi nyata, ini dari database)
private final Map<Long, Produk> db = new java.util.concurrent.ConcurrentHashMap<>();
private long idCounter = 1;
// GET /api/produk — daftar semua produk
@GetMapping
public List<Produk> getSemuaProduk(
@RequestParam(required = false) String cari, // query param opsional
@RequestParam(defaultValue = "0") int halaman,
@RequestParam(defaultValue = "10") int ukuran) {
return db.values().stream()
.filter(p -> cari == null || p.nama().contains(cari))
.skip((long) halaman * ukuran)
.limit(ukuran)
.toList();
}
// GET /api/produk/42 — satu produk berdasarkan ID
@GetMapping("/{id}")
public ResponseEntity<Produk> getProduk(@PathVariable Long id) {
Produk p = db.get(id);
if (p == null) {
return ResponseEntity.notFound().build(); // 404
}
return ResponseEntity.ok(p); // 200 + body JSON
}
// POST /api/produk — buat produk baru
@PostMapping
public ResponseEntity<Produk> buatProduk(@RequestBody ProdukRequest req) {
// Validasi manual (di produksi, pakai @Valid + Bean Validation)
if (req.nama() == null || req.nama().isBlank()) {
return ResponseEntity.badRequest().build(); // 400
}
Produk baru = new Produk(idCounter++, req.nama(), req.harga());
db.put(baru.id(), baru);
// 201 Created + Location header
return ResponseEntity
.created(java.net.URI.create("/api/produk/" + baru.id()))
.body(baru);
}
// PUT /api/produk/42 — update produk
@PutMapping("/{id}")
public ResponseEntity<Produk> updateProduk(
@PathVariable Long id,
@RequestBody ProdukRequest req) {
if (!db.containsKey(id)) return ResponseEntity.notFound().build();
Produk updated = new Produk(id, req.nama(), req.harga());
db.put(id, updated);
return ResponseEntity.ok(updated);
}
// DELETE /api/produk/42 — hapus produk
@DeleteMapping("/{id}")
public ResponseEntity<Void> hapusProduk(@PathVariable Long id) {
if (db.remove(id) == null) return ResponseEntity.notFound().build();
return ResponseEntity.noContent().build(); // 204
}
}
// Record untuk model (Java 16+)
record Produk(Long id, String nama, double harga) {}
record ProdukRequest(String nama, double harga) {}
Penanganan Exception Global #
Daripada menangani exception di setiap controller, gunakan @RestControllerAdvice untuk memusatkan penanganan error.
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Tangani exception kustom
@ExceptionHandler(ProdukTidakDitemukanException.class)
public ResponseEntity<ErrorResponse> tanganiTidakDitemukan(
ProdukTidakDitemukanException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"NOT_FOUND",
ex.getMessage(),
request.getDescription(false)
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// Tangani semua exception yang tidak terduga
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> tanganiUmum(Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
500, "INTERNAL_ERROR", "Terjadi kesalahan internal", request.getDescription(false)
);
return ResponseEntity.status(500).body(error);
}
}
record ErrorResponse(int status, String kode, String pesan, String path) {}
class ProdukTidakDitemukanException extends RuntimeException {
public ProdukTidakDitemukanException(Long id) {
super("Produk dengan ID " + id + " tidak ditemukan");
}
}
Konfigurasi application.properties #
# Port server (default 8080)
server.port=8080
# Context path — semua URL berawalan /api
server.servlet.context-path=/
# Timeout request
server.connection-timeout=5s
# Ukuran maksimum request body (default 1MB)
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
# Logging level
logging.level.root=INFO
logging.level.com.example=DEBUG
# Jackson — format JSON
spring.jackson.serialization.indent-output=true
spring.jackson.default-property-inclusion=NON_NULL
# Aktifkan endpoint actuator untuk health check
management.endpoints.web.exposure.include=health,info,metrics
Filter di Spring Boot #
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component // Spring otomatis mendaftarkan ini sebagai filter
public class RequestLoggingFilter extends jakarta.servlet.Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpServletResponse httpRes = (HttpServletResponse) res;
long mulai = System.currentTimeMillis();
chain.doFilter(req, res);
long durasi = System.currentTimeMillis() - mulai;
System.out.printf("%s %s → %d (%dms)%n",
httpReq.getMethod(), httpReq.getRequestURI(), httpRes.getStatus(), durasi);
}
}
Menjalankan Spring Boot #
# Via Maven
mvn spring-boot:run
# Via JAR (setelah build)
mvn clean package
java -jar target/aplikasi-1.0.0.jar
# Dengan profil tertentu
java -jar aplikasi.jar --spring.profiles.active=production
# Akses: http://localhost:8080/api/produk
Jetty Embedded #
Jetty memungkinkan membuat web server yang berjalan sepenuhnya dari kode Java — tanpa Tomcat, tanpa WAR, tanpa deployment ke server eksternal. Seluruh server ada dalam satu JAR yang bisa dijalankan dengan java -jar. Ini ideal untuk microservice ringan, tool internal, atau ketika kamu ingin kontrol penuh atas konfigurasi server.
Dependensi #
<!-- Maven — Jetty 12 -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.7</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<version>12.0.7</version>
</dependency>
Server Minimal #
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.ee10.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
public class JettyMain {
public static void main(String[] args) throws Exception {
// Buat server di port 8080
Server server = new Server(8080);
// Context handler: tangani semua request di path "/"
ServletContextHandler ctx = new ServletContextHandler();
ctx.setContextPath("/");
// Daftarkan servlet ke path
ctx.addServlet(new ServletHolder(new HelloServlet()), "/hello");
ctx.addServlet(new ServletHolder(new ApiServlet()), "/api/*");
server.setHandler(ctx);
// Mulai server
server.start();
System.out.println("Server Jetty berjalan di http://localhost:8080");
// Blokir sampai server dihentikan (Ctrl+C)
server.join();
}
}
// Servlet sederhana
class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.setContentType("application/json");
res.getWriter().println("{\"pesan\": \"Halo dari Jetty!\"}");
}
}
Konfigurasi Thread Pool dan Timeout #
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
public class JettyTerkonfigurasi {
public static void main(String[] args) throws Exception {
// Konfigurasi thread pool
QueuedThreadPool pool = new QueuedThreadPool();
pool.setMinThreads(5);
pool.setMaxThreads(50);
pool.setIdleTimeout(60_000); // hapus thread idle setelah 60 detik
Server server = new Server(pool);
// Konfigurasi connector (port, timeout)
ServerConnector connector = new ServerConnector(server);
connector.setPort(8080);
connector.setIdleTimeout(30_000); // putus koneksi idle setelah 30 detik
connector.setAcceptQueueSize(100); // antrian koneksi masuk
server.addConnector(connector);
// ... tambahkan handler dan start
server.start();
server.join();
}
}
Handler Kustom (Tanpa Servlet) #
Untuk kasus yang sangat sederhana, Jetty juga mendukung Handler langsung tanpa Servlet API:
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandler;
import jakarta.servlet.http.*;
import java.io.IOException;
public class HandlerKustom extends AbstractHandler {
@Override
public void handle(String target, Request baseReq,
HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.setContentType("application/json; charset=utf-8");
res.setStatus(HttpServletResponse.SC_OK);
String response = switch (target) {
case "/ping" -> "{\"status\": \"ok\"}";
case "/versi" -> "{\"versi\": \"1.0.0\"}";
default -> "{\"error\": \"endpoint tidak ditemukan\"}";
};
res.getWriter().println(response);
baseReq.setHandled(true); // tandai sudah ditangani
}
}
Perbandingan Langsung #
Untuk membangun endpoint GET /api/produk yang mengembalikan JSON, berikut perbandingan ketiga pendekatan:
Servlet API #
@WebServlet("/api/produk")
public class ProdukServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.setContentType("application/json");
res.getWriter().println("[{\"id\":1,\"nama\":\"Laptop\"}]");
}
}
Spring Boot #
@RestController
@RequestMapping("/api")
public class ProdukController {
@GetMapping("/produk")
public List<Map<String, Object>> produk() {
return List.of(Map.of("id", 1, "nama", "Laptop"));
}
}
Jetty Embedded #
ctx.addServlet(new ServletHolder(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.setContentType("application/json");
res.getWriter().println("[{\"id\":1,\"nama\":\"Laptop\"}]");
}
}), "/api/produk");
Spring Boot paling ringkas karena menangani serialisasi JSON otomatis. Servlet dan Jetty membutuhkan serialisasi manual atau library tambahan seperti Jackson.
Kapan Menggunakan Tiap Pendekatan #
Gunakan SERVLET API jika:
✓ Belajar dasar HTTP dan cara kerja web server Java
✓ Perlu deploy ke app server yang sudah ada (Tomcat, JBoss, WebLogic)
✓ Memaintain aplikasi legacy yang sudah memakai Servlet
✗ Hindari untuk aplikasi baru — terlalu verbose dibanding alternatif
Gunakan SPRING BOOT jika:
✓ Membangun REST API atau microservice baru
✓ Butuh ekosistem lengkap: security, database, caching, testing
✓ Tim sudah familiar dengan Spring
✓ Auto-configuration menghemat waktu setup
✓ Hampir selalu — ini pilihan default untuk Java web development modern
Gunakan JETTY EMBEDDED jika:
✓ Butuh server ringan yang di-embed dalam aplikasi standalone
✓ Tool internal atau aplikasi desktop yang punya fitur web kecil
✓ Kontrol penuh atas konfigurasi server tanpa overhead Spring
✓ Distribusi sebagai single JAR tanpa dependensi eksternal
Tips produksi untuk semua pendekatan:
✓ Selalu set timeout untuk request dan koneksi
✓ Tambahkan filter logging untuk observability
✓ Tangani exception secara terpusat — jangan biarkan stack trace bocor ke klien
✓ Set header keamanan: Content-Security-Policy, X-Frame-Options, dll.
✓ Gunakan HTTPS di produksi — konfigurasi SSL/TLS di server atau reverse proxy
Ringkasan #
- Servlet API adalah fondasi — semua framework Java web berjalan di atasnya.
HttpServlet,doGet(),doPost(),HttpServletRequest, danHttpServletResponseadalah building block yang perlu dipahami.- Spring Boot adalah pilihan default untuk aplikasi baru —
@RestController+@GetMapping/@PostMapping+@RequestBody+ResponseEntitymembentuk pola REST API yang bersih dan idiomatis.ResponseEntityuntuk kontrol penuh atas response — bisa atur status code, header, dan body secara eksplisit. GunakanResponseEntity.ok(),.created(),.notFound(),.badRequest().@RestControllerAdviceuntuk penanganan error terpusat — daripada try-catch di setiap controller, tangkap exception spesifik di satu tempat dan kembalikan format error yang konsisten.- Filter untuk cross-cutting concerns — logging, autentikasi, CORS, dan kompresi adalah urusan filter, bukan controller. Pisahkan keduanya agar controller tetap fokus pada logika bisnis.
- Jetty embedded untuk distribusi single JAR — cocok untuk tool dan microservice yang perlu dijalankan di mana saja tanpa instalasi server.
- Atur timeout — tanpa timeout, satu request yang lambat bisa memblokir thread pool dan mematikan server. Set
server.connection-timeoutdi Spring Boot atausetIdleTimeoutdi Jetty.- Jangan bocorkan detail error ke klien — stack trace mengandung informasi sensitif. Tangkap semua exception dan kembalikan pesan error yang aman dan informatif.