Tomcat 11 Deep Dive: Virtual Threads, Servlet 6.1, EL 6.0, and Migration Guide

This article explores Tomcat 11's major upgrades including Java 17 baseline, Jakarta EE 11 alignment, virtual threads for high concurrency, Servlet 6.1 API improvements, EL 6.0 record support, removed legacy features, and provides hands-on Docker and embedded deployment examples with code.

java1234
java1234
java1234
Tomcat 11 Deep Dive: Virtual Threads, Servlet 6.1, EL 6.0, and Migration Guide

Introduction to Tomcat

Tomcat is Apache's open-source lightweight Servlet container, originating from Sun's Servlet/JSP reference implementation in 1999. Unlike full Jakarta EE servers (WebLogic, WildFly), Tomcat implements only core web specs: Servlet, JSP (Jakarta Pages), EL, WebSocket. It acts as a front desk: HTTP requests arrive at the Connector (Coyote), then Engine-to-Wrapper chain (Catalina) routes them to the target Servlet.

Three actively maintained version lines exist:

9.0.x — Java EE 8, Servlet 4.0, javax.* packages, minimum Java 8

10.1.x — Jakarta EE 10, Servlet 6.0, jakarta.* packages, minimum Java 11

11.0.x — Jakarta EE 11, Servlet 6.1, jakarta.* packages, minimum Java 17

Tomcat 11.0.27 is current; Spring Boot 4.x defaults to Tomcat 11.

Tomcat 11 New Features

2.1 Baseline Upgrade: Java 17 & Jakarta EE 11

Tomcat 11 requires Java 17 (vs. Java 11 for 10.1). This lets Tomcat internals use modern language features and integrate with Java 21/25 capabilities. Implemented specs: Servlet 6.1, Pages 4.0, EL 6.0, WebSocket 2.2, Authentication 3.1, Annotations 3.0. Migration from 10.1 is smooth (same jakarta.* namespace); from 9.x requires package migration.

2.2 Virtual Threads: One-Config Concurrency Boost

Traditional model: thread pool (default maxThreads=200), one thread per request. Blocking I/O (DB, remote calls) idles threads, exhausting the pool.

Virtual threads (Java 21+) are lightweight, JVM-scheduled, yield carrier threads on I/O wait. Enables synchronous code to handle high concurrency.

Enable in conf/server.xml two ways:

<!-- Option 1: direct switch on Connector -->
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" useVirtualThreads="true" redirectPort="8443" />

<!-- Option 2: shared virtual-thread Executor -->
<Executor name="virtualExecutor" className="org.apache.catalina.core.StandardVirtualThreadExecutor" namePrefix="tomcat-virt-" />
<Connector port="8080" protocol="HTTP/1.1" executor="virtualExecutor" connectionTimeout="20000" />

Spring Boot: spring.threads.virtual.enabled=true.

Verification Servlet prints current thread; output shows

VirtualThread[#52,http-nio-8080-virt-0]/runnable@ForkJoinPool-1-worker-1

and true for isVirtual().

Caveats:

Solves waiting, not CPU-bound work.

Downstream resources (DB connection pool) become new bottleneck; size pools accordingly or add rate limiting.

Java version matters: Tomcat 11 runs on Java 17, but virtual threads need Java 21+. Java 21 pins virtual threads in synchronized blocks (fixed in Java 24 via JEP 491); production should target Java 25 LTS.

2.3 Servlet 6.1: Long-Awaited Small Fixes

1. Redirect with status code. Old sendRedirect() only sent 302; 301/307/308 required manual setStatus() + setHeader("Location"). Now:

resp.sendRedirect("/api/v2" + path, HttpServletResponse.SC_PERMANENT_REDIRECT)

(308). Example: migrate /old-api/** to /api/v2/** preserving method. curl -i -X POST /old-api/users returns 308 with Location: /api/v2/users. 308/307 preserve request method (unlike 301/302 which browsers historically convert POST→GET).

2. Charset object for encoding. resp.setCharacterEncoding(StandardCharsets.UTF_8) — IDE autocomplete, no typo risk.

3. ServletOutputStream.write(ByteBuffer). Directly write ByteBuffer from NIO FileChannel, eliminating byte-array copy boilerplate. Example streams CSV via 8 KB buffer.

4. Other changes. New status constants: 308, 421, 422 ( SC_UNPROCESSABLE_CONTENT), 426. Parameter parsing errors now throw exceptions instead of silently swallowing; FailedRequestFilter removed as obsolete.

2.4 EL 6.0: JSP Finally Understands Records

Java 16 records lacked getter methods; EL only recognized JavaBean getters. EL 6.0 adds default-enabled RecordELResolver — ${book.title} works on record Book(String title, String author, String[] tags). Arrays also support .length.

Demo: BookServlet puts

new Book("Tomcat 11 实战笔记", "张三", new String[]{"Java","Web","Tomcat"})

into request, forwards to book.jsp rendering ${book.title}, ${book.author}, ${book.tags.length} → "书名:Tomcat 11 实战笔记", "作者:张三", "标签数量:3".

Additional: OptionalELResolver for java.util.Optional (disabled by default, manual registration). EL no longer hard-depends on java.desktop module, enabling slimmer jlink runtime images.

2.5 Subtractions: Removed Legacy Baggage

SecurityManager support removed. Deprecated in Java 17, permanently disabled in Java 24 (JEP 486), dropped from Jakarta EE 11. Rarely used in production.

HTTP/2 Server Push removed. Servlet 6.1 made it optional; major browsers dropped support; <link rel="preload"> preferred.

All deprecated APIs purged. Tomcat 10.1 deprecations, JSP 3.1/EL 5.0 deprecations gone. Custom Valves/Realms using internal classes must recompile.

2.6 Stricter Defaults & OpenSSL via FFM

maxParameterCount

default lowered from 10,000 to 1,000 (DoS mitigation); adjust if forms exceed.

Cookie parsing now strictly follows RFC 6265 — quoted values keep quotes as part of value.

Byte-to-char decoding failures throw exceptions instead of producing replacement characters.

HTTPS: Java FFM API (Foreign Function & Memory) can call system OpenSSL directly (documented for Java 25+), eliminating tomcat-native JNI library — container-friendly.

Hands-On: Ten Minutes to Tomcat 11

3.1 Docker One-Liner

docker run -d --name tomcat11 -p 8080:8080 tomcat:11.0-jdk21
docker cp ./demo.war tomcat11:/usr/local/tomcat/webapps/
docker logs -f tomcat11

Note: official image's webapps is empty (security); default welcome page and manager apps live in webapps.dist — copy back if needed.

3.2 Embedded Tomcat 11 Complete Example

Project structure:

tomcat11-demo
├── pom.xml
├── data
│   └── report.csv
└── src/main
    ├── java/com/example/tomcat11
    │   ├── Tomcat11App.java        // startup
    │   ├── ThreadInfoServlet.java  // virtual thread verify
    │   ├── RedirectServlet.java    // 308 redirect
    │   ├── DownloadServlet.java    // ByteBuffer download
    │   ├── Book.java               // record
    │   └── BookServlet.java        // record + EL
    └── webapp
        └── book.jsp

Key pom.xml deps:

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <tomcat.version>11.0.27</tomcat.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>${tomcat.version}</version>
  </dependency>
  <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>${tomcat.version}</version>
  </dependency>
</dependencies>

Startup class Tomcat11App:

Tomcat tomcat = new Tomcat();
tomcat.setBaseDir(new File("target/tomcat").getAbsolutePath());
tomcat.setPort(8080);
// Enable virtual threads (JDK 21+)
Connector connector = tomcat.getConnector();
connector.setProperty("useVirtualThreads", "true");
// Webapp root for JSP
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
// Register servlets
Tomcat.addServlet(ctx, "threadInfo", new ThreadInfoServlet());
ctx.addServletMappingDecoded("/thread", "threadInfo");
Tomcat.addServlet(ctx, "redirect", new RedirectServlet());
ctx.addServletMappingDecoded("/old-api/*", "redirect");
Tomcat.addServlet(ctx, "download", new DownloadServlet());
ctx.addServletMappingDecoded("/download", "download");
Tomcat.addServlet(ctx, "book", new BookServlet());
ctx.addServletMappingDecoded("/book", "book");
tomcat.start();
System.out.println("Tomcat 11 started: http://localhost:8080/thread");
tomcat.getServer().await();

Run main; console shows Starting Servlet engine: [Apache Tomcat/11.0.27]. Test endpoints: GET /thread — virtual thread + Charset overload POST /old-api/users — sendRedirect with 308 GET /download —

ServletOutputStream.write(ByteBuffer)
GET /book

— EL reads record, array

length
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Virtual ThreadsMigration GuideJava 17Embedded TomcatServlet 6.1Jakarta EE 11EL 6.0Tomcat 11
java1234
Written by

java1234

Former senior programmer at a Fortune Global 500 company, dedicated to sharing Java expertise. Visit Feng's site: Java Knowledge Sharing, www.java1234.com

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.