Add jakarta-ee-11 module: Jakarta EE 10 vs 11 API comparison against Spring Boot 4.1.1

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gth99spB1CzQmBFofXmU4Z
This commit is contained in:
Claude
2026-09-28 08:40:07 +00:00
parent acd63e658b
commit e4f927d7af
14 changed files with 342 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# jakarta-ee-11
Companion module for the ankurm.com post *Jakarta EE 11 for Spring Developers: What Actually Matters*.
Reads the Jakarta EE 10 and 11 API jars from Maven Central with `javap`, reads Spring Boot 4.1.1's own BOM and configuration metadata, and runs one tiny Boot application to ask the embedded Tomcat which Servlet level it implements. No claim in the post comes from prose.
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| JDK | 25 |
| Maven | 3.9 |
## Run
```bash
./scripts/run-all.sh # regenerates everything under output/
```
## Output
| File | Shows |
|---|---|
| `output/01-boot-managed-jakarta-apis.txt` | Jakarta versions Spring Boot 4.1.1 manages, and four artifacts it does not |
| `output/02-api-diff.txt` | Class and member counts, EE 10 vs EE 11, for Servlet, Persistence, Validation, Annotations, Concurrency |
| `output/03-entitymanager-3.2.txt` | New `EntityManager` methods in Persistence 3.2 |
| `output/04-data-and-concurrency.txt` | Jakarta Data 1.0.2 repository types; `@ManagedExecutorDefinition` in 3.0.0 vs 3.1.1 |
| `output/05-servlet-level.txt` | The running application's Servlet level (written by `ServletLevelTest`) |
| `output/06-spring-data-and-jakarta-data.txt` | Spring Data 4.1.1 jars contain no `jakarta.data` types |
| `output/07-virtual-threads-property.txt` | Boot's `spring.threads.virtual.enabled` property, from shipped metadata |
@@ -0,0 +1,32 @@
# 01-boot-managed-jakarta-apis (spring-boot-dependencies 4.1.1)
hibernate-validator.version = 9.1.3.Final
hibernate.version = 7.4.5.Final
jakarta-activation.version = 2.1.4
jakarta-annotation.version = 3.0.0
jakarta-inject.version = 2.0.1
jakarta-jms.version = 3.1.0
jakarta-json-bind.version = 3.0.2
jakarta-json.version = 2.1.3
jakarta-mail.version = 2.1.5
jakarta-management.version = 1.1.4
jakarta-persistence.version = 3.2.0
jakarta-servlet-jsp-jstl.version = 3.0.2
jakarta-servlet.version = 6.1.0
jakarta-transaction.version = 2.0.1
jakarta-validation.version = 3.1.1
jakarta-websocket.version = 2.2.0
jakarta-ws-rs.version = 4.0.0
jakarta-xml-bind.version = 4.0.5
jakarta-xml-soap.version = 3.0.2
jakarta-xml-ws.version = 4.0.3
jetty.version = 12.1.12
spring-data-bom.version = 2026.0.1
spring-framework.version = 7.0.9
tomcat.version = 11.0.24
--- artifacts NOT in the BOM (0 = Spring Boot does not manage them)
jakarta.data-api 0
jakarta.enterprise.concurrent-api 0
jakarta.enterprise.cdi-api 0
jakarta.security.enterprise-api 0
jakarta.faces-api 0
jakarta.ws.rs-api 1
+45
View File
@@ -0,0 +1,45 @@
# 02-api-diff (javap -public over every class of the two jars; members are signature strings, so a changed signature counts as one removed and one added)
--- Servlet: jakarta.servlet-api 6.0.0 -> 6.1.0
classes added : 1 removed: 1
members added : 26 removed: 3
+ class jakarta.servlet.http.HttpSession$Accessor
- class jakarta.servlet.http.Cookie$1
--- Persistence: jakarta.persistence-api 3.1.0 -> 3.2.0
classes added : 24 removed: 0
members added : 251 removed: 39
+ class jakarta.persistence.CheckConstraint
+ class jakarta.persistence.ConnectionConsumer
+ class jakarta.persistence.ConnectionFunction
+ class jakarta.persistence.EnumeratedValue
+ class jakarta.persistence.FindOption
+ class jakarta.persistence.Graph
+ class jakarta.persistence.LockOption
+ class jakarta.persistence.PersistenceConfiguration
+ class jakarta.persistence.PersistenceUnitTransactionType
+ class jakarta.persistence.RefreshOption
+ class jakarta.persistence.SchemaManager
+ class jakarta.persistence.SchemaValidationException
--- Validation: jakarta.validation-api 3.0.0 -> 3.1.0
classes added : 9 removed: 0
members added : 1 removed: 1
+ class jakarta.validation.bootstrap.package-info
+ class jakarta.validation.constraints.package-info
+ class jakarta.validation.constraintvalidation.package-info
+ class jakarta.validation.executable.package-info
+ class jakarta.validation.groups.package-info
+ class jakarta.validation.metadata.package-info
+ class jakarta.validation.package-info
+ class jakarta.validation.spi.package-info
+ class jakarta.validation.valueextraction.package-info
--- Annotations: jakarta.annotation-api 2.1.1 -> 3.0.0
classes added : 2 removed: 1
members added : 0 removed: 1
+ class jakarta.annotation.package-info
+ class jakarta.annotation.security.package-info
- class jakarta.annotation.ManagedBean
--- Concurrency: jakarta.enterprise.concurrent-api 3.0.0 -> 3.1.1
classes added : 3 removed: 0
members added : 15 removed: 0
+ class jakarta.enterprise.concurrent.Schedule
+ class jakarta.enterprise.concurrent.package-info
+ class jakarta.enterprise.concurrent.spi.package-info
@@ -0,0 +1,18 @@
# 03-entitymanager-3.2 (methods on jakarta.persistence.EntityManager that 3.1.0 did not have)
public abstract <C, T> T callWithConnection(ConnectionFunction<C, T>);
public abstract <C> void runWithConnection(ConnectionConsumer<C>);
public abstract <T> Query createNativeQuery(String, Class<T>);
public abstract <T> T find(Class<T>, Object, FindOption...);
public abstract <T> T find(EntityGraph<T>, Object, FindOption...);
public abstract <T> T getReference(T);
public abstract <T> TypedQuery<T> createQuery(TypedQueryReference<T>);
public abstract <T> TypedQuery<T> createQuery(criteria.CriteriaSelect<T>);
public abstract CacheRetrieveMode getCacheRetrieveMode();
public abstract CacheStoreMode getCacheStoreMode();
public abstract Query createQuery(criteria.CriteriaDelete<?>);
public abstract Query createQuery(criteria.CriteriaUpdate<?>);
public abstract StoredProcedureQuery createStoredProcedureQuery(String, Class<?>...);
public abstract void lock(Object, LockModeType, LockOption...);
public abstract void refresh(Object, RefreshOption...);
public abstract void setCacheRetrieveMode(CacheRetrieveMode);
public abstract void setCacheStoreMode(CacheStoreMode);
@@ -0,0 +1,27 @@
# 04-data-and-concurrency
--- jakarta.data-api 1.0.2: types in jakarta.data.repository
BasicRepository
By
CrudRepository
DataRepository
Delete
Find
Insert
OrderBy
Param
Query
Repository
Save
Update
--- jakarta.enterprise.concurrent-api 3.0.0: attributes of @ManagedExecutorDefinition
String name();
String context();
long hungTaskThreshold();
int maxAsync();
--- jakarta.enterprise.concurrent-api 3.1.1: attributes of @ManagedExecutorDefinition
String name();
Class<?>[] qualifiers();
String context();
long hungTaskThreshold();
int maxAsync();
boolean virtual();
@@ -0,0 +1,2 @@
# 05-servlet-level (Spring Boot 4.1.1, embedded Tomcat, JDK 25)
GET /servlet-level -> 6.1 (container: Apache Tomcat/11.0.24)
@@ -0,0 +1,3 @@
# 06-spring-data-and-jakarta-data (classes mentioning jakarta/data inside Spring Data jars from Spring Data 2026.0.1)
spring-data-commons 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0
spring-data-jpa 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0
@@ -0,0 +1,2 @@
# 07-virtual-threads-property (spring-boot-autoconfigure 4.1.1 configuration metadata)
spring.threads.virtual.enabled (java.lang.Boolean, default False): Whether to use virtual threads.
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>jakarta-ee-11</artifactId>
<version>1.0.0</version>
<name>jakarta-ee-11</name>
<description>Which Jakarta EE 11 specifications a Spring Boot 4.1 application actually uses</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Facts about the Jakarta EE 11 API jars, read from Maven Central artifacts and Spring Boot's own BOM. No running application.
set -uo pipefail
cd "$(dirname "$0")/.."
R="$HOME/.m2/repository"; W=$(mktemp -d); CEN=https://repo1.maven.org/maven2
get() { mvn -B -q dependency:get -Dartifact="$1" >/dev/null 2>&1; }
jar() { echo "$R/$(echo "$1" | cut -d: -f1 | tr . /)/$2/$3/$2-$3.jar"; }
# --- 01: what Spring Boot 4.1.1 manages -------------------------------------------------------------------
for i in 1 2 3 4 5; do code=$(curl -s -o "$W/bom.pom" -w '%{http_code}' $CEN/org/springframework/boot/spring-boot-dependencies/4.1.1/spring-boot-dependencies-4.1.1.pom); [ "$code" = 200 ] && break; sleep $((i*3)); done
{
echo "# 01-boot-managed-jakarta-apis (spring-boot-dependencies 4.1.1)"
grep -E '<(jakarta-[a-z-]+|tomcat|jetty|hibernate|hibernate-validator|spring-data-bom|spring-framework)\.version>' "$W/bom.pom" | sed 's/^ *//; s/<\([^>]*\)>\(.*\)<\/.*/\1 = \2/' | sort
echo "--- artifacts NOT in the BOM (0 = Spring Boot does not manage them)"
for a in jakarta.data-api jakarta.enterprise.concurrent-api jakarta.enterprise.cdi-api jakarta.security.enterprise-api jakarta.faces-api jakarta.ws.rs-api; do
printf ' %-36s %s\n' "$a" "$(grep -c "<artifactId>$a</artifactId>" "$W/bom.pom")"
done
} > output/01-boot-managed-jakarta-apis.txt
# --- 02: API differences between the EE 10 and EE 11 versions of four specs ---------------------------------
diffspec() { # name group artifact old new
get "$2:$3:$4"; get "$2:$3:$5"
for v in $4 $5; do
j=$(jar "$2" "$3" "$v"); rm -rf "$W/x-$v"; mkdir -p "$W/x-$v"; unzip -q -o "$j" '*.class' -d "$W/x-$v" 2>/dev/null
( cd "$W/x-$v" && find . -name '*.class' ! -name 'module-info.class' | sed 's#^\./##; s#\.class$##; s#/#.#g' | sort > "$W/cls-$v.txt"
javap -public -cp . $(cat "$W/cls-$v.txt" | grep -v 'package-info') 2>/dev/null | grep -E '^\s+(public|protected)|^(public|protected)' | sed 's/^ *//' | sort -u > "$W/mem-$v.txt" )
done
echo "--- $1: $3 $4 -> $5"
echo " classes added : $(comm -13 "$W/cls-$4.txt" "$W/cls-$5.txt" | wc -l) removed: $(comm -23 "$W/cls-$4.txt" "$W/cls-$5.txt" | wc -l)"
echo " members added : $(comm -13 "$W/mem-$4.txt" "$W/mem-$5.txt" | wc -l) removed: $(comm -23 "$W/mem-$4.txt" "$W/mem-$5.txt" | wc -l)"
comm -13 "$W/cls-$4.txt" "$W/cls-$5.txt" | sed 's/^/ + class /' | head -12
comm -23 "$W/cls-$4.txt" "$W/cls-$5.txt" | sed 's/^/ - class /' | head -12
}
{
echo "# 02-api-diff (javap -public over every class of the two jars; members are signature strings, so a changed signature counts as one removed and one added)"
diffspec Servlet jakarta.servlet jakarta.servlet-api 6.0.0 6.1.0
diffspec Persistence jakarta.persistence jakarta.persistence-api 3.1.0 3.2.0
diffspec Validation jakarta.validation jakarta.validation-api 3.0.0 3.1.0
diffspec Annotations jakarta.annotation jakarta.annotation-api 2.1.1 3.0.0
diffspec Concurrency jakarta.enterprise.concurrent jakarta.enterprise.concurrent-api 3.0.0 3.1.1
} > output/02-api-diff.txt
# --- 03: the new Persistence 3.2 EntityManager methods, by name --------------------------------------------
{
echo "# 03-entitymanager-3.2 (methods on jakarta.persistence.EntityManager that 3.1.0 did not have)"
for v in 3.1.0 3.2.0; do javap -public -cp "$(jar jakarta.persistence jakarta.persistence-api $v)" jakarta.persistence.EntityManager 2>/dev/null | grep '(' | sed 's/^ *//; s/jakarta.persistence.//g; s/java.lang.//g; s/java.util.//g' | sort > "$W/em-$v.txt"; done
comm -13 "$W/em-3.1.0.txt" "$W/em-3.2.0.txt"
} > output/03-entitymanager-3.2.txt
# --- 04: Jakarta Data 1.0 API surface and the virtual-thread switch in Concurrency 3.1 ---------------------
get jakarta.data:jakarta.data-api:1.0.2
{
echo "# 04-data-and-concurrency"
echo "--- jakarta.data-api 1.0.2: types in jakarta.data.repository"
unzip -l "$(jar jakarta.data jakarta.data-api 1.0.2)" | awk '{print $4}' | grep -E '^jakarta/data/repository/[A-Z][A-Za-z]*\.class$' | sed 's#jakarta/data/repository/##; s#\.class##' | sort | sed 's/^/ /'
for v in 3.0.0 3.1.1; do
echo "--- jakarta.enterprise.concurrent-api $v: attributes of @ManagedExecutorDefinition"
javap -public -cp "$(jar jakarta.enterprise.concurrent jakarta.enterprise.concurrent-api $v)" jakarta.enterprise.concurrent.ManagedExecutorDefinition 2>/dev/null | grep '()' | sed 's/^ *//; s/public abstract //; s/java.lang.//g' | sed 's/^/ /'
done
} > output/04-data-and-concurrency.txt
# --- 06: does the Spring Data version Boot manages contain any Jakarta Data code? ---------------------------
{
echo "# 06-spring-data-and-jakarta-data (classes mentioning jakarta/data inside Spring Data jars from Spring Data 2026.0.1)"
for a in spring-data-commons spring-data-jpa; do
v=$(curl -s $CEN/org/springframework/data/$a/maven-metadata.xml | grep -o '<version>4\.[0-9.]*</version>' | tail -1 | sed 's/<[^>]*>//g')
get org.springframework.data:$a:$v
j="$R/org/springframework/data/$a/$v/$a-$v.jar"
n=$(unzip -l "$j" 2>/dev/null | grep -c 'jakarta/data/')
m=$(unzip -p "$j" 2>/dev/null META-INF/MANIFEST.MF | grep -c 'jakarta.data')
echo " $a $v: entries under jakarta/data/: $n; manifest lines mentioning jakarta.data: $m"
done
} > output/06-spring-data-and-jakarta-data.txt
# --- 07: Spring Boot's own switch for virtual threads (read from the shipped configuration metadata) ----------
get org.springframework.boot:spring-boot-autoconfigure:4.1.1
{
echo "# 07-virtual-threads-property (spring-boot-autoconfigure 4.1.1 configuration metadata)"
unzip -p "$(jar org.springframework.boot spring-boot-autoconfigure 4.1.1)" META-INF/spring-configuration-metadata.json \
| python3 -c "import json,sys; [print(p['name'], '(' + p.get('type','?') + ', default ' + str(p.get('defaultValue','none')) + '):', p.get('description','')) for p in json.load(sys.stdin)['properties'] if p['name']=='spring.threads.virtual.enabled']"
} > output/07-virtual-threads-property.txt
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Regenerates every file under output/. Needs JDK 25, Maven 3.9 and network access to Maven Central.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B test # 05-servlet-level.txt
./scripts/capture-facts.sh # 01-04, 06, 07
ls -1 output
@@ -0,0 +1,7 @@
package com.ankurm.jee11;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Jee11Application {
}
@@ -0,0 +1,16 @@
package com.ankurm.jee11;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/** Asks the servlet container which Servlet specification level it implements. */
@RestController
class ServletLevelController {
@GetMapping("/servlet-level")
String level(HttpServletRequest request) {
var ctx = request.getServletContext();
return ctx.getMajorVersion() + "." + ctx.getMinorVersion() + " (container: " + ctx.getServerInfo() + ")";
}
}
@@ -0,0 +1,29 @@
package com.ankurm.jee11;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.web.client.RestClient;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ServletLevelTest {
@LocalServerPort
int port;
@Test
void containerReportsServlet61() throws IOException {
String body = RestClient.create("http://localhost:" + port).get().uri("/servlet-level").retrieve().body(String.class);
Files.createDirectories(Path.of("output"));
Files.write(Path.of("output/05-servlet-level.txt"), List.of(
"# 05-servlet-level (Spring Boot 4.1.1, embedded Tomcat, JDK " + Runtime.version().feature() + ")",
"GET /servlet-level -> " + body));
assertThat(body).startsWith("6.1");
}
}