Web Server

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
PendekatanCocok untukServerKonfigurasi
Servlet APIAplikasi enterprise lama, belajar dasar HTTPTomcat, Jetty, WildFlyweb.xml atau anotasi
Spring BootAplikasi modern, REST API, microserviceTomcat/Jetty embeddedAuto-configuration
Jetty embeddedTool internal, aplikasi standalone ringanJetty embeddedKode 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("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;");
    }
}

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");
    }
}
@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, dan HttpServletResponse adalah building block yang perlu dipahami.
  • Spring Boot adalah pilihan default untuk aplikasi baru@RestController + @GetMapping/@PostMapping + @RequestBody + ResponseEntity membentuk pola REST API yang bersih dan idiomatis.
  • ResponseEntity untuk kontrol penuh atas response — bisa atur status code, header, dan body secara eksplisit. Gunakan ResponseEntity.ok(), .created(), .notFound(), .badRequest().
  • @RestControllerAdvice untuk 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-timeout di Spring Boot atau setIdleTimeout di Jetty.
  • Jangan bocorkan detail error ke klien — stack trace mengandung informasi sensitif. Tangkap semua exception dan kembalikan pesan error yang aman dan informatif.

← Sebelumnya: Web Socket   Berikutnya: Unit Test →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact