Developing Enterprise Web Apps for Apache Tomcat
Master Jakarta Servlet 6.0 architecture, JNDI database connection pooling, zero-leak resource management, and asynchronous non-blocking I/O.
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.
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:
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.
Never use unmanaged static thread pools. Use a ServletContextListener to call executor.shutdown() when the context undeploys.
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/