pip/pip-472.md
In 2017, Oracle transferred Java EE to the Eclipse Foundation, where it was renamed Jakarta EE. As part of that transfer, Oracle did not grant Eclipse the right to evolve the javax.* namespace; from Jakarta EE 9 onward (released 2020), every Jakarta EE specification moved its package prefix from javax.* to jakarta.*. The two namespaces are source- and binary-incompatible: an application can use either, but not both, for the same specification.
Concretely, the following Pulsar-relevant specs moved from javax.* (Jakarta EE 8 and earlier) to jakarta.* (Jakarta EE 9+):
| Specification | Old namespace | New namespace |
|---|---|---|
| Common Annotations (JSR 250) | javax.annotation | jakarta.annotation |
| Servlet | javax.servlet | jakarta.servlet |
| RESTful Web Services (JAX-RS) | javax.ws.rs | jakarta.ws.rs |
| Bean Validation | javax.validation | jakarta.validation |
| XML Binding (JAXB) | javax.xml.bind | jakarta.xml.bind |
| Activation Framework | javax.activation | jakarta.activation |
| Dependency Injection (JSR 330) | javax.inject | jakarta.inject |
| WebSocket | javax.websocket | jakarta.websocket |
Independently of Jakarta EE, several javax.* packages are part of the JDK itself (not Jakarta EE specifications) and stay in javax.* permanently. They are out of scope for any "migrate to jakarta" effort:
javax.crypto (JCE)javax.naming (JNDI)javax.net, javax.net.ssl (JSSE)javax.management (JMX)javax.security.auth.* (JAAS — except for a few callbacks moved into Jakarta Security)javax.sql (DataSource APIs)javax.tools (Compiler API)javax.xml.parsers, javax.xml.transform, javax.xml.xpath, javax.xml.stream (JAXP — separate from JAXB)javax.transaction.xaPulsar's build is Gradle, with all dependency versions managed centrally in gradle/libs.versions.toml. The minimum runtime is Java 21 (JDK 25 is also supported). The latest snapshot version is 5.0.0-M1-SNAPSHOT and Pulsar 5.0 is the next LTS line per PIP-162.
Today the codebase imports javax.* from approximately 1,620 Java files. Removing the JDK packages listed above leaves three real migration surfaces:
| Package | Approx. files | Status |
|---|---|---|
javax.ws.rs | 660 | Migrate to jakarta.ws.rs |
javax.servlet | 368 | Pulsar's own code migrates to jakarta.servlet; usage in the AdditionalServlet plugin SPI surface is intentionally retained on javax.servlet (see High-Level Design — Phase 1) |
javax.annotation (JSR 250 portion) | 48 | Migrate to jakarta.annotation |
Pulsar's existing usage of javax.security.* (in pulsar-broker-auth-sasl and similar) is exclusively javax.security.auth.callback, javax.security.auth.login, javax.security.auth.Subject, and javax.security.sasl.* — all JDK-resident JAAS / SASL APIs that stay in javax.* and have no Jakarta counterpart. They are therefore out of scope.
A partial migration is already visible in git history (e.g. commits replacing javax.ws.rs-api with jakarta.ws.rs-api, replacing javax.annotation-api with jakarta.annotation-api, and shading jakarta.annotation into shaded client JARs), and gradle/libs.versions.toml already declares Jakarta-named API artifacts. However, those Jakarta-named artifacts are pinned to Jakarta EE 8 versions, which still use the javax.* namespace — so the artifact rename has happened but the namespace has not.
Upstream ecosystem has moved. Jersey 3.x, Jetty 11+, Swagger Core 2.x, Hibernate Validator 7+, Spring 6, MicroProfile 5+ — all of them require jakarta.*. Pulsar can no longer pick up upstream bug fixes, performance improvements, security advisories, or compatibility with newer Java releases without leaving the javax.* namespace.
Pulsar is already partially mid-migration. The Jakarta-named API artifacts are declared but pinned to javax-namespace versions, the partial replacement of individual artifacts is happening one PR at a time, and Jetty 12 is in use but configured with its ee8-* (javax-servlet compat) modules rather than ee10-*. The result is a half-migrated state that is harder to reason about and harder to extend than either endpoint. A coordinated migration ends this drift.
Pulsar 5.0 LTS is the right window. Migration touches public surfaces (admin client REST DTOs, plugin/SDK annotations, function/connector base contexts). Once 5.0 ships and is supported as LTS, breaking those surfaces becomes very expensive: a major-version bump is the only legitimate path. Doing the migration before 5.0 cuts means the LTS line carries clean Jakarta APIs for its full lifecycle.
javax.* is functionally frozen. Reference implementations in the javax.* namespace receive only critical security fixes; new features, performance work, and dependency updates land only in jakarta.*. Carrying javax.* through a multi-year LTS window means accumulating risk.
Direct javax.servlet:javax.servlet-api:3.1.0 dependency. Pulsar declares this dependency directly, which constrains every downstream module to Servlet 3.1 even though the deployed Jetty 12 runtime supports Servlet 6.0. Replacing the dependency unlocks newer Servlet APIs.
javax.* packages used in Pulsar's own code (javax.ws.rs, javax.annotation JSR-250 portion, javax.validation, javax.xml.bind, javax.activation, javax.inject where used, javax.websocket where used). javax.servlet is intentionally retained in the AdditionalServlet plugin SPI surface (see High-Level Design) and therefore not eliminated codebase-wide. Approximate scope: ~1,100 files (1,620 total javax.* imports minus ~476 in JDK-resident javax.* packages that stay in javax.* forever, minus the javax.servlet files that remain because of the AdditionalServlet retention).AdditionalServlet SPI so plugins can register jakarta.servlet handlers (preferred, routed to Jetty's ee10 environment) in addition to the existing javax.servlet registration path (legacy, routed to ee8). Both registration styles coexist; existing plugins keep working without recompilation.gradle/libs.versions.toml to Jakarta-namespace versions of every affected library (Jersey 2.42 → 3.1.10, Swagger Core 1.6.2 → 2.2.27, add jetty-ee10-* alongside the retained jetty-ee8-*, add jakarta.servlet:jakarta.servlet-api:6.0.0 alongside the retained javax.servlet:javax.servlet-api, bump the jakarta-* API versions to their Jakarta EE 10 namespace versions, swap jackson-jaxrs-json-provider for jackson-jakarta-rs-json-provider).javax.* API classes (pulsar-client-shaded, pulsar-client-admin-shaded, pulsar-client-all-shaded, pulsar-functions/localrun-shaded, etc.).javax.* packages: javax.crypto, javax.naming, javax.net, javax.net.ssl, javax.management, javax.security.auth.* (the JAAS portions), javax.sql, javax.tools, javax.xml.parsers, javax.xml.transform, javax.xml.xpath, javax.xml.stream, javax.transaction.xa. These are not Jakarta EE specifications and have no jakarta.* counterpart.javax.servlet support from the AdditionalServlet plugin SPI. The SPI is extended to accept jakarta.servlet handlers (the preferred path going forward), but the existing javax.servlet handler registration continues to be supported so that existing AdditionalServlet plugins keep working without recompilation. Removing the javax.servlet registration path is a separate decision for a future PIP if desired.javax.* ↔ jakarta.* runtime compatibility shim (e.g. Eclipse Transformer at JAR load time). See Alternatives for why this is rejected.The migration is executed in four phases. Each phase is mergeable on its own; the codebase remains buildable and shippable between phases.
build-logic for the mechanical import-rename portion of the migration. The recipe of choice is org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta from rewrite-migrate-java. Eclipse Transformer's source-mode is an alternative.javax.ws.rs, javax.servlet, javax.annotation (JSR 250 portion), or javax.validation imports from being introduced after Phase 1 lands. The lint is feature-flagged so it can be enabled per-module as the migration progresses.pulsar-client-shaded, pulsar-client-admin-shaded, pulsar-client-all-shaded, pulsar-functions/localrun-shaded, jclouds-shaded, etc.) and document their current javax.* shading rules so Phase 3 has a checklist.The web stack has two interlocked layers, each with its own coexistence story:
Servlet container (Jetty 12). Jetty 12 supports running multiple Servlet API generations side by side, each in its own environment. Pulsar's broker, proxy, and websocket components move from jetty-ee8-* (Servlet 4 / javax.servlet) to jetty-ee10-* (Servlet 6 / jakarta.servlet). The jetty-ee8-* artifacts are retained alongside ee10 so that both environments share the same Jetty 12 server instance and connector pool. The AdditionalServlet SPI (org.apache.pulsar.broker.web.plugin.servlet.AdditionalServlet*) is extended so that plugins can register either jakarta.servlet handlers (preferred, routed to the ee10 environment) or javax.servlet handlers (legacy path, routed to the ee8 environment); both styles can coexist within a single broker. This is the Jetty 12 design intent for migration scenarios and is what unblocks gradual ecosystem migration without breaking existing AdditionalServlet plugins.
JAX-RS provider (Jersey). Jersey 2 (javax.ws.rs) and Jersey 3 (jakarta.ws.rs) cannot share a single REST tier. The broker REST tier moves atomically to Jersey 3. Every JAX-RS resource class registered with the broker REST application — across pulsar-broker, pulsar-broker-common, pulsar-client-admin, pulsar-websocket, and pulsar-proxy — is updated in the same coordinated change to import jakarta.ws.rs.*. Modules outside the broker REST tier are unaffected by the Jersey switch and keep their javax.* imports until Phase 2.
In gradle/libs.versions.toml:
| Variable | Before | After | Note |
|---|---|---|---|
jersey | 2.42 | 3.1.10 | |
swagger | 1.6.2 | 2.2.27 | Group changes to io.swagger.core.v3:swagger-*; annotations move from io.swagger.annotations.* to io.swagger.v3.oas.annotations.* |
jakarta-ws-rs | 2.1.6 | 3.1.0 | Jakarta EE 10 |
jakarta-annotation | 1.3.5 | 2.1.1 | Jakarta EE 10 |
jakarta-validation | 2.0.2 | 3.0.2 | Jakarta EE 10 |
jakarta-xml-bind | 2.3.3 | 4.0.2 | Jakarta EE 10 |
jakarta-activation | 1.2.2 | 2.1.3 | Jakarta EE 10 |
javax-servlet | 3.1.0 | unchanged | Retained for the ee8 environment used by AdditionalServlet plugins |
new jakarta-servlet | — | 6.0.0 | Servlet 6 / Jakarta EE 10 |
The PIP targets Jakarta EE 10 consistently across these specs (rather than mixing in EE 11) because EE 10 has the broadest tooling and runtime support today and matches the maturity of the third-party libraries Pulsar consumes. EE 11 is a candidate for a future follow-up.
Plus aliases:
jetty-ee10-* library aliases for every jetty-ee8-* artifact currently in use. The jetty-ee8-* aliases are retained alongside ee10 so AdditionalServlet plugins keep working. Pulsar's own broker/proxy/websocket Jetty wiring uses ee10.jackson-jaxrs-json-provider (com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider) with jackson-jakarta-rs-json-provider (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider) at the existing Jackson 2.21.3 version (Jackson publishes both variants under the same release).jakarta.servlet:jakarta.servlet-api:6.0.0 as a new servlet-api alias alongside the retained javax.servlet:javax.servlet-api:3.1.0.Phase 1 lands as one coordinated PR that:
gradle/libs.versions.toml per the table above.WebService, ProxyServer, and WebSocketService to start the ee10 environment alongside ee8.AdditionalServlet SPI so plugins can register jakarta.servlet handlers in addition to existing javax.servlet handlers; existing plugin entry points stay binary-compatible.pulsar-broker + 34 in pulsar-broker-common + 30 in pulsar-client-admin + 17 in pulsar-websocket + 18 in pulsar-proxy). The mechanical javax.ws.rs.* → jakarta.ws.rs.* and Swagger 1 → 2 annotation renames in those files are driven by OpenRewrite.Phase 1 is large but atomic — Jersey cannot be half-migrated.
javax.*After Phase 1, the broker REST tier (Jersey, Swagger, the javax.ws.rs resource classes in pulsar-broker, pulsar-broker-common, pulsar-client-admin, pulsar-websocket, and pulsar-proxy) is on jakarta. What remains is the non-REST javax.* usage scattered across the rest of the codebase: javax.annotation (JSR 250), javax.validation, javax.xml.bind, javax.activation, javax.inject, and javax.websocket where used.
These are independent of the JAX-RS / Jersey switch and can be migrated one module at a time. A series of mechanical PRs runs the OpenRewrite recipe over each module:
pulsar-broker/src/test, pulsar-broker-common/src/test, etc.) — JAX-RS test code follows the production code.pulsar-client-api and remaining client modules (small file counts; mostly @Generated, @PostConstruct, validation annotations).javax.annotation / javax.validation (NAR runtime is unaffected; this is a source rename only).pulsar-docs-tools).Each PR:
javax.* imports for that module.javax.ws.rs, javax.annotation-api, javax.validation, javax.xml.bind, javax.activation entries (all now Jakarta-namespaced); add equivalent jakarta.* entries where shading is still required. The javax.servlet shading rules remain because the ee8 environment is retained for AdditionalServlet plugin support.Exclude javax.annotation rules in build files).| Group:Artifact | Current | Target | Notes |
|---|---|---|---|
org.glassfish.jersey.core:jersey-server (and all org.glassfish.jersey.* artifacts) | 2.42 | 3.1.10 | Group ID unchanged; major bump moves namespace to jakarta.ws.rs |
io.swagger:swagger-annotations, io.swagger:swagger-core | 1.6.2 | — | Replace with io.swagger.core.v3:swagger-annotations, io.swagger.core.v3:swagger-jaxrs2 at 2.2.27. Annotation package renames from io.swagger.annotations.* to io.swagger.v3.oas.annotations.* |
org.eclipse.jetty.ee8:jetty-ee8-* | 12.1.8 | unchanged | Retained alongside jetty-ee10-* to preserve AdditionalServlet plugin support |
new org.eclipse.jetty.ee10:jetty-ee10-* | — | 12.1.8 | Pulsar's own broker/proxy/websocket Jetty wiring uses ee10 |
javax.servlet:javax.servlet-api | 3.1.0 | unchanged | Retained for the ee8 environment used by AdditionalServlet plugins |
new jakarta.servlet:jakarta.servlet-api | — | 6.0.0 | Servlet 6 / Jakarta EE 10 |
jakarta.ws.rs:jakarta.ws.rs-api | 2.1.6 | 3.1.0 | 2.1.x is Jakarta EE 8 (javax namespace); 3.1.x is Jakarta EE 10 (jakarta namespace) |
jakarta.annotation:jakarta.annotation-api | 1.3.5 | 2.1.1 | 2.x is Jakarta EE 10 namespace |
jakarta.validation:jakarta.validation-api | 2.0.2 | 3.0.2 | 3.x is Jakarta EE 10 namespace |
jakarta.xml.bind:jakarta.xml.bind-api | 2.3.3 | 4.0.2 | 4.x is Jakarta EE 10 namespace |
jakarta.activation:jakarta.activation-api | 1.2.2 | 2.1.3 | 2.x is Jakarta EE 10 namespace |
com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider | 2.21.3 | — | Replace with com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.21.3 (same Jackson version) |
org.springframework:* | 6.2.12 | unchanged | Already Jakarta-native |
org.apache.bookkeeper:* | 4.17.3 | track | Pulsar community to drive a Jakarta migration in BookKeeper 4.18 (see Alternatives) |
OpenRewrite recipe:
org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta
This recipe handles the bulk of the namespace renames in source files, including imports and fully-qualified references. Manual review is required for:
Class.forName("javax.ws.rs.core.Response")).META-INF/services/).javax.*.A grep pass at the end of Phase 2 finds any residual javax.* outside the JDK-allowed list.
Shaded modules currently rewrite some javax.* classes into Pulsar's shaded namespace to avoid classpath conflicts. After Phase 1 the shading rules need to:
jakarta.* API classes that are bundled.javax.* rules that no longer apply.javax.* classes (javax.crypto, javax.net.ssl, etc.) un-shaded — they were never bundled and remain on the JDK.Pulsar already runs Checkstyle in CI. The migration uses Checkstyle's ImportControl (or IllegalImport) module to prevent regressions, since extending an existing tool is cheaper than introducing a new Gradle plugin. The Jakarta-EE-spec packages below are added to the existing Checkstyle configuration and disallowed across the codebase, with two well-defined exceptions:
javax.servlet is allowed only in code paths that implement the AdditionalServlet SPI (where javax-servlet support is intentionally retained).javax.annotation.processing is always allowed (it is a JDK package, not Jakarta EE).Disallowed packages for Pulsar's own code:
javax.ws.rsjavax.servlet (except in the AdditionalServlet plugin SPI implementation classes)javax.annotation (excluding javax.annotation.processing)javax.validationjavax.xml.bindjavax.activationjavax.injectjavax.websocketThe check is enabled per-module as that module is swept (Phase 2), then turned on codebase-wide in Phase 3.
The migration's compile-time impact on downstream Java code is narrower than the file count suggests, because most extension points live in their own classloaders or are explicitly preserved.
Affected — these consumers must change imports and recompile:
pulsar-client-admin that import REST DTOs and any of their JAX-RS / Bean Validation annotations directly from Pulsar's published JARs. Annotations like @PathParam, @QueryParam, @Encoded, the Response builder, and Bean Validation @NotNull / @Size / @Min / @Max move from javax.* to jakarta.*.AuthenticationProvider, AuthorizationProvider) and any other plugin that contributes JAX-RS resources or sub-resources to the broker REST tier — these are loaded into the broker's REST classpath and must use jakarta.ws.rs.*.io.swagger.annotations.* (Swagger 1.x) to io.swagger.v3.oas.annotations.* (Swagger 2.x).Unaffected — these continue to work without recompilation:
AdditionalServlet plugins: existing plugins that register javax.servlet handlers continue to work unchanged — the SPI's javax.servlet registration path is retained and routed to Jetty's ee8 environment. New plugins (and existing plugins that want to migrate) can register jakarta.servlet handlers via the extended SPI; those go to the ee10 environment. Both styles coexist.NarClassLoader). The connector / function base contexts (BaseContext, FunctionContext, SinkContext, SourceContext) currently do not expose Jakarta-EE-spec types in their signatures, so existing 4.x NARs continue to work on a 5.0 broker as long as they don't directly call the affected Pulsar REST/auth SPIs. A connector / function NAR that internally uses javax.ws.rs for its own HTTP client is also unaffected, because nothing forces those NARs to share Pulsar's JAX-RS provider.Wire protocol and REST API are unchanged. Pulsar's binary protocol, REST API URLs, request bodies, response bodies, and status codes are identical before and after the migration. A 4.x client speaking to a 5.0 broker, or vice versa, behaves the same as it does today.
No changes.
No new or removed configuration. A small number of Jersey/Jetty internal configuration keys may differ between Jersey 2.42/Jetty ee8 and Jersey 3.1/Jetty ee10; any user-visible difference will be documented in the upgrade guide.
No CLI changes.
No metric changes.
No new monitoring is required. Existing health checks, latency metrics, and request-rate metrics on the broker, proxy, and WebSocket components continue to function unchanged.
During the migration window, operators are advised to watch for any classpath errors of the form NoClassDefFoundError: javax.ws.rs.* or NoClassDefFoundError: jakarta.ws.rs.* in broker logs after upgrade — these indicate a third-party plugin compiled against the wrong namespace.
ee10 modules) both receive active security maintenance; Jersey 2.x and Jetty's ee8 family are increasingly only fixed for serious CVEs. Migration improves Pulsar's exposure to security fixes.javax.security.auth.*) and JSSE (javax.net.ssl) remain on javax.* (they are JDK SPIs, not Jakarta EE).javax.* namespace will not load on a 5.0 broker, which is the desired (failing-loud) behaviour rather than a silent classpath conflict.AdditionalServlet plugin authors: existing javax.servlet-based plugins keep working without recompilation. New or migrating plugins can target jakarta.servlet through the extended SPI, which is the preferred path going forward.javax.ws.rs.* / javax.annotation.* / javax.validation.* / io.swagger.annotations.* imports to the corresponding jakarta.* / io.swagger.v3.oas.annotations.* packages and recompile against Pulsar 5.0.pulsar-client-admin: Java applications that import REST DTOs / JAX-RS annotations from Pulsar's published JARs in-process must update their javax.* imports to jakarta.* and recompile.Wire protocol and REST API are unchanged, so a 5.0 broker can be replaced with a 4.x broker without rollback steps.
Geo-replication runs broker-to-broker over Pulsar's binary protocol, which this PIP does not change. A mixed-version geo-replicated cluster (some sites on 4.x, others on 5.0) replicates correctly. There are no special steps.
Stay on javax.* indefinitely.
Rejected. The upstream ecosystem has moved; staying on javax.* means no new Jersey, Jetty, Swagger, Spring, Hibernate Validator features, performance work, or security fixes can be picked up. The cost grows over time.
Defer the migration to Pulsar 6.0.
Rejected. Pulsar 5.0 is LTS. Carrying javax.* through 5.0 means the LTS line will require a major-version bump just to migrate. Doing it now keeps the LTS line clean for its full lifecycle.
Run-time javax.* ↔ jakarta.* bytecode transformation (Eclipse Transformer at load time).
Rejected. Adds startup cost and operational complexity, masks classpath problems behind a translation layer that is itself a class of bugs, and does not help downstream consumers (their compile-time signatures still resolve against whichever side of the namespace the Pulsar artifacts publish). It also doesn't simplify the build.
Big-bang migration in a single feature branch. Rejected. The scope is ~1,100 files. A long-lived feature branch against master, which evolves daily, becomes unmaintainable to rebase. Phasing keeps master always-shippable and lets the work parallelise across contributors.
A javax.* → jakarta.* source compatibility shim layer.
Rejected. Maintaining a permanent shim that re-exports jakarta.* types as javax.* (or vice versa) defeats the goal of removing the dual classpath, doubles the public surface, and is itself a source of subtle bugs.
Migrate Apache BookKeeper as part of this PIP.
Out of scope, but actively coordinated. BookKeeper is a separate Apache project with its own community and release cadence, and a Jakarta migration in BookKeeper cannot land via a Pulsar PIP. The Pulsar and BookKeeper communities are closely linked, however, and the Pulsar community should drive a Jakarta migration in BookKeeper 4.18 through the BookKeeper community's normal process so that Pulsar 5.0 can ship on a BookKeeper release whose own classpath is also Jakarta-clean. If BookKeeper has not migrated by the time Pulsar 5.0 ships, BookKeeper's classpath will still contain javax.* symbols (BookKeeper's own code), but Pulsar's own code is clean either way.
The migration is heavily mechanical — most of the source-code change is import-rename driven by an automated recipe — but the dependency-upgrade and JAX-RS namespace switch in Phase 1 require careful coordination because Jersey 2 and Jersey 3 cannot coexist in the same REST tier. Phase 1 should be executed by contributors familiar with Jersey, Jetty, and Swagger initialisation in pulsar-broker. The Jetty 12 ee8/ee10 dual-environment configuration that preserves the AdditionalServlet plugin SPI is the key architectural decision that lets Phase 1 land without forcing all third-party AdditionalServlet plugins to recompile in lockstep.