App Development
Featured Sample Webapp Jakarta EE 10 / Servlet 6.0

Mau Enterprise Portal & Telemetry Vault

A production-ready reference application demonstrating live JVM telemetry, JNDI database connection pool health checking, RESTful inventory CRUD servlets, and a 100% leak-free ServletContextListener lifecycle.

Download Ready WAR (.war) Download Maven Source (.zip)

1 Jakarta Servlet 6.0 Architecture & Annotation Mapping

Starting in Tomcat 10+ (Jakarta EE 9/10/11), all servlets use the jakarta.servlet.* namespace instead of legacy javax.servlet.*. Declare endpoints using the modern @WebServlet annotation without needing cumbersome XML bindings for every class.

Standard HTTP Servlet Implementation

package com.example.servlet;

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;

/**
 * Modern RESTful Endpoint in Jakarta Servlet 6.0
 */
@WebServlet(name = "TelemetryServlet", urlPatterns = {"/api/telemetry"}, asyncSupported = true)
public class TelemetryServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");

        long freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024);
        long maxMemoryMb = Runtime.getRuntime().maxMemory() / (1024 * 1024);

        try (PrintWriter out = response.getWriter()) {
            out.write(String.format("{\"status\":\"UP\",\"freeHeapMb\":%d,\"maxHeapMb\":%d}",
                    freeMemoryMb, maxMemoryMb));
        }
    }
}

2 Database Connection Pooling & JNDI Resource Management

Never open direct DriverManager.getConnection() calls per request — doing so creates severe TCP socket churn and high latency. Instead, use Tomcat's built-in Tomcat JDBC Connection Pool via JNDI.

Step A: Define JNDI Pool in META-INF/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context useHttpOnly="true">
    <!-- Enterprise Tomcat JDBC Connection Pool Resource -->
    <Resource name="jdbc/AppDB"
              auth="Container"
              type="javax.sql.DataSource"
              factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
              initialSize="5"
              maxTotal="50"
              maxIdle="15"
              minIdle="5"
              maxWaitMillis="5000"
              testOnBorrow="true"
              validationQuery="SELECT 1"
              validationInterval="30000"
              removeAbandoned="true"
              removeAbandonedTimeout="60"
              logAbandoned="true"
              username="app_user"
              password="secure_password"
              driverClassName="org.postgresql.Driver"
              url="jdbc:postgresql://localhost:5432/production_db" />
</Context>

Step B: Safe JNDI Lookup & Leak-Free try-with-resources Execution

Always wrap Connection, PreparedStatement, and ResultSet in a single Java try-with-resources block. When the block exits, Tomcat automatically returns the physical socket back to the pool, completely eliminating connection leaks.

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class InventoryDAO {

    private final DataSource dataSource;

    public InventoryDAO() throws NamingException {
        // Fast, cached JNDI InitialContext Lookup
        Context initCtx = new InitialContext();
        Context envCtx = (Context) initCtx.lookup("java:comp/env");
        this.dataSource = (DataSource) envCtx.lookup("jdbc/AppDB");
    }

    public int getItemStock(String sku) throws SQLException {
        String sql = "SELECT quantity FROM items WHERE sku = ?";

        // GOLDEN RULE: try-with-resources guarantees automatic return to pool
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {
            
            ps.setString(1, sku); // Prepared statement prevents SQL Injection

            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return rs.getInt("quantity");
                }
            }
        }
        return 0;
    }
}

3 Zero-Leak Architecture: Preventing Tomcat Metaspace & Heap OOM

When web applications are redeployed without stopping Tomcat, lingering references cause WebappClassLoader Pinning, leading to java.lang.OutOfMemoryError: Metaspace. Follow these 3 critical rules:

1. ThreadLocal Leak Prevention

Tomcat worker threads (http-nio-exec-*) are reused across requests. If a servlet stores data in a ThreadLocal without calling .remove() in a finally block, the webapp ClassLoader remains pinned forever.

2. Background Executor Shutdown

Never use unmanaged static thread pools. Use a ServletContextListener to call executor.shutdown() when the context undeploys.

3. JDBC Driver Deregistration

If JDBC driver JARs reside in WEB-INF/lib, deregister them in contextDestroyed to release DriverManager's static references.

Production ServletContextListener Implementation

package com.example.listener;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@WebListener
public class AppLifecycleListener implements ServletContextListener {

    private ExecutorService backgroundExecutor;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // Initialize managed daemon thread pool
        backgroundExecutor = Executors.newFixedThreadPool(2, r -> {
            Thread t = new Thread(r, "App-Daemon-Worker");
            t.setDaemon(true);
            return t;
        });
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // 1. Gracefully shut down thread pools
        if (backgroundExecutor != null) {
            backgroundExecutor.shutdownNow();
        }

        // 2. Cleanly deregister JDBC Drivers registered by this ClassLoader
        ClassLoader webappCl = Thread.currentThread().getContextClassLoader();
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            if (driver.getClass().getClassLoader() == webappCl) {
                try {
                    DriverManager.deregisterDriver(driver);
                } catch (SQLException ignored) {}
            }
        }
    }
}

4 High-Concurrency Asynchronous Non-Blocking Processing

When handling slow external REST APIs or long-running database queries, traditional synchronous servlets hold a Tomcat HTTP worker thread (e.g. 1 of 200 threads). With asyncSupported = true, the HTTP thread is immediately returned to Tomcat's acceptor pool while the response is fulfilled asynchronously.

@WebServlet(urlPatterns = {"/api/async-report"}, asyncSupported = true)
public class AsyncReportServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        // Start non-blocking AsyncContext
        final AsyncContext asyncCtx = request.startAsync();
        asyncCtx.setTimeout(10000); // 10s timeout

        CompletableFuture.runAsync(() -> {
            try {
                // Perform heavy operation on worker pool without blocking Tomcat acceptor
                String reportJson = generateHeavyReport();
                
                HttpServletResponse res = (HttpServletResponse) asyncCtx.getResponse();
                res.setContentType("application/json;charset=UTF-8");
                res.getWriter().write(reportJson);
            } catch (Exception e) {
                ((HttpServletResponse) asyncCtx.getResponse()).setStatus(500);
            } finally {
                asyncCtx.complete(); // Completes request and flushes output
            }
        });
    }
}

5 Building with Maven & Deploying to Tomcat

Maven pom.xml Standard Configuration

<dependencies>
    <!-- Jakarta Servlet 6.0 API provided by Tomcat at runtime -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <finalName>myapp</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <release>21</release>
            </configuration>
        </plugin>
    </plugins>
</build>

One-Liner Compilation & Deployment Commands

# 1. Compile and package the WAR archive
mvn clean package

# 2. Deploy to local Tomcat instance (Linux / POSIX)
cp target/myapp.war /opt/tomcat/webapps/

# 2. Deploy to local Tomcat instance (Windows PowerShell)
Copy-Item target\myapp.war C:\Apps\Tomcat\webapps\

# 3. Access in browser
http://localhost:8080/myapp/