Skip to content

[improve][misc] PIP-472: Migrate from javax.* to jakarta.* APIs#25912

Merged
lhotari merged 10 commits into
apache:masterfrom
lhotari:lh-javax-to-jakarta-migration
Jun 1, 2026
Merged

[improve][misc] PIP-472: Migrate from javax.* to jakarta.* APIs#25912
lhotari merged 10 commits into
apache:masterfrom
lhotari:lh-javax-to-jakarta-migration

Conversation

@lhotari
Copy link
Copy Markdown
Member

@lhotari lhotari commented Jun 1, 2026

PIP: PIP-472

Motivation

Implements PIP-472: Migrate from javax.* to jakarta.* APIs.

The upstream ecosystem (Jersey 3.x, Jetty 11+/12 ee10, Swagger Core 2.x, Spring 6, Hibernate Validator 7+)
requires the jakarta.* namespace. Pulsar was in a half-migrated state: Jetty 12 was wired through its ee8
(Servlet 4 / javax.servlet) compatibility modules, Jersey was pinned at 2.x (javax.ws.rs), and the
jakarta.*-named API artifacts were pinned to Jakarta EE 8 versions that still ship the javax.* packages.
This change completes the namespace migration so Pulsar 5.0 LTS ships on clean Jakarta APIs.

Modifications

Dependencies (gradle/libs.versions.toml)

  • Jersey 2.423.1.10; jakarta.ws.rs-api 2.1.63.1.0; jakarta.annotation-api2.1.1;
    jakarta.xml.bind-api4.0.2; jakarta.validation-api3.0.2; jakarta.activation-api2.1.3
    (impl switched to org.eclipse.angus:angus-activation, the EE10 reference impl).
  • Added jetty-ee10-* aliases alongside the retained jetty-ee8-*; added jakarta.servlet:jakarta.servlet-api:6.0.0
    alongside the retained javax.servlet:javax.servlet-api:3.1.0.
  • jackson-jaxrs-json-providerjackson-jakarta-rs-json-provider; Prometheus simpleclient_servlet
    simpleclient_servlet_jakarta.

Source migration

  • javax.ws.rs.*jakarta.ws.rs.* (169 files); broker/proxy/websocket/functions-worker REST tier on Jersey 3.
  • javax.servlet.*jakarta.servlet.* for Pulsar's own code; Jetty wiring (WebService, WebServer,
    websocket ProxyServer, functions WorkerServer) moved from ee8 to ee10 (removed the .get() core-Handler
    bridges; ee8.nested.Request.getBaseRequest(...)ServletContextRequest-based connector lookup).
  • WebSocket endpoints migrated to Jetty 12's native WebSocket API
    (org.eclipse.jetty.websocket.api.Session.Listener / Callback).

AdditionalServlet plugin SPI (backward compatible)

  • Added AdditionalServletType.JAKARTA_SERVLET. The broker (PulsarService/WebService) and proxy
    (ProxyServiceStarter/WebServer) now route JAKARTA_SERVLET handlers to Jetty's ee10 environment and keep
    routing legacy JAVAX_SERVLET handlers to the retained ee8 environment, so existing javax.servlet plugins
    keep working without recompilation. Both environments coexist on the same Jetty server.

Shading / packaging

  • pulsar.client-shade-conventions and localrun-shaded relocations updated javax.*jakarta.*; renamed the
    checked-in META-INF/services/org.apache.pulsar.shade.javax.ws.rs.* provider files to ...jakarta.ws.rs.*.

Swagger 1.x → Swagger Core 2.x is deferred to a follow-up (it is io.swagger, compile-/doc-only, and decoupled
from the namespace move); Swagger stays at 1.6.2 in this change.

Third-party javax.* that intentionally remains (not Pulsar's own code — kept available where third-party libs
still reference the legacy packages):

  • The tiered-storage offloaders (jclouds 2.6.0 and Hadoop MiniDFSCluster) still reference legacy javax.xml.bind,
    javax.ws.rs, javax.annotation and javax.validation; these are restored as runtime-/test-scoped deps, and the
    file-system offloader test classpath is pinned to the legacy javax web stack (Jetty 9 / Jersey 2.46 / hk2 2.6.1).
  • async-http-client (the pulsar-client-admin HTTP client) pulls com.sun.activation:jakarta.activation:1.2.2
    (the legacy javax.activation impl, for multipart MIME-type detection), so it remains bundled.
  • Athenz needs no change: current master ships Athenz 1.12.42, which already uses jakarta.xml.bind
    (athenz-auth-core pulls org.glassfish.jaxb:jaxb-runtime:4.0.8).

Verifying this change

  • Make sure that the change passes the CI checks.

This change is already covered by existing tests: the broker / proxy / client REST, servlet, websocket, broker
interceptor, transaction, function, and tiered-storage suites exercise the migrated Jetty 12 ee10 / Jersey 3 stack
end-to-end. A full green CI run on the author's fork (lhotari/pulsar) covered 39/39 jobs — Build & License,
all Broker unit groups (1–5), Client API/Impl, and the System/integration suites (including the broker-interceptor
and tiered-storage offloader suites). spotlessCheck + checkstyleMain + checkstyleTest pass locally.

A few Jetty 12 ee10 / Jersey 3 runtime issues surfaced during the migration and are fixed here:

  • Ambiguous-URI 400s for %2F-encoded topic names — ServletHandler.setDecodeAmbiguousURIs(true) on the
    ee10 servlet handlers (broker / proxy / functions-worker).
  • Broker-interceptor "Trailing token" 400sRequestWrapper.getInputStream() returned a fresh stream per
    call, so the ee10 / Jersey entity reader re-read the body's leading { after EOF as a phantom trailing token;
    fixed by returning a single, Servlet-contract-compliant cached stream with lazy, Content-Length-bounded buffering.
  • Tiered-storage offloader classpaths — restored the legacy javax EE APIs that jclouds / Hadoop need (see
    Modifications above).

Does this pull request potentially affect one of the following parts:

If the box was checked, please highlight the changes

  • Dependencies (add or upgrade a dependency) — Jersey 2→3, Jetty ee8→ee10, the jakarta API bumps, the Jackson
    JAX-RS provider, and the activation impl (Angus); see Modifications.
  • The public API — JAX-RS / Bean-Validation annotations on the published pulsar-client-admin REST DTOs move to
    jakarta.*, and the AdditionalServlet SPI gains AdditionalServletType.JAKARTA_SERVLET. Auth / REST plugins
    must recompile against jakarta.*; existing javax.servlet AdditionalServlet plugins remain binary-compatible.
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints (URLs / request bodies / status codes are unchanged)
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

lhotari added 10 commits June 1, 2026 10:27
Implements PIP-472. Completes the javax.* -> jakarta.* namespace migration so
Pulsar 5.0 LTS ships on clean Jakarta APIs.

Dependencies (gradle/libs.versions.toml):
- Jersey 2.42 -> 3.1.10; jakarta.ws.rs-api 2.1.6 -> 3.1.0; jakarta.annotation-api
  -> 2.1.1; jakarta.xml.bind-api -> 4.0.2; jakarta.validation-api -> 3.0.2;
  jakarta.activation-api -> 2.1.3 (impl -> org.eclipse.angus:angus-activation).
- Added jetty-ee10-* alongside the retained jetty-ee8-*; added
  jakarta.servlet:jakarta.servlet-api:6.0.0 alongside the retained javax.servlet.
- jackson-jaxrs-json-provider -> jackson-jakarta-rs-json-provider;
  Prometheus simpleclient_servlet -> simpleclient_servlet_jakarta.

Source:
- javax.ws.rs.* -> jakarta.ws.rs.* (Jersey 3 REST tier).
- javax.servlet.* -> jakarta.servlet.* for Pulsar's own code; broker/proxy/websocket
  Jetty wiring moved ee8 -> ee10 (removed .get() core-Handler bridges; ee8.nested
  Request connector lookup -> ServletContextRequest).
- WebSocket endpoints migrated to Jetty 12's native WebSocket API
  (org.eclipse.jetty.websocket.api.Session.Listener / Callback).

AdditionalServlet plugin SPI (backward compatible):
- Added AdditionalServletType.JAKARTA_SERVLET; broker/proxy route JAKARTA_SERVLET
  handlers to Jetty ee10 and keep routing legacy JAVAX_SERVLET handlers to the
  retained ee8 environment, so existing javax.servlet plugins keep working.

Shading: client-shade-conventions and localrun-shaded relocations javax.* ->
jakarta.*; renamed shaded META-INF/services javax.ws.rs.* files to jakarta.ws.rs.*.

Swagger 1.x -> Swagger Core 2.x is deferred to a follow-up (decoupled, compile-/doc-only).

See pip/pip-472-notes.md for discovered state, decisions and deviations.

Assisted-by: Claude Code (Opus 4.8)
…e jetty-upgrade modules

The global swap to simpleclient_servlet_jakarta broke jetty-upgrade/*-prometheus-metrics
(io.prometheus.client.exporter.MetricsServlet), which legitimately stay on the javax
Prometheus servlet. No Pulsar code uses the jakarta variant.

Assisted-by: Claude Code (Opus 4.8)
- distribution/{server,shell} LICENSE.bin.txt: jersey 2.42->3.1.10, hk2 2.6.1->3.0.6,
  jackson-jaxrs->jackson-jakarta-rs, jakarta API version bumps, Angus activation impl,
  hk2.external jakarta.inject -> jakarta.inject:jakarta.inject-api, jetty ee8->ee10 jars
  (ee8-servlet retained), + jakarta.servlet/CDI/interceptor API jars.
- BrokerAdditionalServletTest / ProxyAdditionalServletTest: declare JAKARTA_SERVLET so
  the jakarta servlets route to the ee10 environment; drop ee8.nested.Request casts.
- CounterBrokerInterceptor: read path/status via the jakarta servlet API instead of
  casting to Jetty's Response/ee8.nested.Response.

Assisted-by: Claude Code (Opus 4.8)
…CENSE

It is still bundled transitively (the javax.activation JAF impl) alongside the new
org.eclipse.angus:angus-activation; both must be listed.

Assisted-by: Claude Code (Opus 4.8)
…ntime dep

- Allow %2F-encoded path separators in admin/REST URLs: Jetty 12 ee10 rejects ambiguous
  URIs at the servlet layer (independent of the connector UriCompliance), which broke admin
  operations on topics whose names contain encoded separators. Enable
  ServletHandler.setDecodeAmbiguousURIs(true) in broker WebService, proxy WebServer and
  functions WorkerServer.
- Provide javax.xml.bind:jaxb-api at runtime for pulsar-client-auth-athenz and
  pulsar-broker-auth-athenz: the Athenz ZTS client shades jackson-module-jaxb-annotations
  which needs the javax.xml.bind package that jakarta.xml.bind-api 2.3.3 used to ship; the
  bump to 4.0.2 (real jakarta.xml.bind) removed it. Pulsar's own code uses jakarta.xml.bind.

See pip/pip-472-notes.md for the full CI-failure triage (incl. the open transaction-unit-test
server-side request-body over-read under Jetty 12 ee10).

Assisted-by: Claude Code (Opus 4.8)
…offloader tests

Motivation:
The javax.* -> jakarta.* migration (PIP-472) removed the legacy javax EE APIs
from the transitive classpath, breaking the tiered-storage offloader unit tests
that exercise pre-jakarta third-party libraries (jclouds 2.6.0 and Hadoop):
- BlobStoreBackedInputStreamTest: NoClassDefFoundError javax.xml.bind.JAXBException
  (jclouds ContextBuilder.build()).
- FileSystemManagedLedgerOffloaderTest: Hadoop MiniDFSCluster's fully-javax NameNode
  web UI failed because the migration force-upgraded its jersey-*:2.46 -> 3.1.10
  (jakarta), so org.glassfish.jersey.servlet.ServletContainer stopped being a
  javax.servlet.Servlet; cascading NoClassDefFoundErrors for javax.ws.rs,
  javax.annotation.Priority and javax.validation.Validator.

Modifications:
- tiered-storage/jcloud: runtimeOnly javax.xml.bind:jaxb-api:2.3.1 (jclouds needs
  the legacy javax.xml.bind at runtime).
- tiered-storage/file-system: pin the legacy javax web stack for test configs only
  (mirrors the existing Jetty-9 force) - Jersey 2.46 + hk2 2.6.1, plus testRuntimeOnly
  legacy javax.ws.rs/annotation/validation APIs; runtimeOnly jaxb-api for Hadoop.
- libs.versions.toml: add javax-ws-rs-api (javax.ws.rs:javax.ws.rs-api:2.1.1) alias.
- pip-472-notes.md: document this fix and the definitive server-side root-cause of
  the remaining transaction multi-broker over-read.

Production offloaders are NARs that bundle their own deps and use HDFS RPC (no web
UI), so they are unaffected; these forces are scoped to test configurations.
All tiered-storage offloader tests (90) pass locally.

Assisted-by: Claude Code (Opus 4.8)
…under Jetty 12 ee10

Motivation:
After the javax->jakarta migration moved the broker web layer to Jetty 12 ee10 /
Jersey 3, every request whose broker enables a BrokerInterceptor failed admin calls
that have a body (e.g. clusters().createCluster(...)) with:
  HTTP 400 Trailing token (of type START_OBJECT) found after value bound as ClusterDataImpl
This broke all TransactionTestBase subclasses (TransactionStablePositionTest,
TransactionBufferCloseTest, AdminApiTransactionMultiBrokerTest, TransactionEndToEndTest),
ExceptionsBrokerInterceptorTest, and the TestBrokerInterceptors integration test. The
Client Api unit group passed because it enables no interceptor - the key clue.

Root cause:
Enabling an interceptor activates PreInterceptFilter, which wraps the request in
RequestWrapper. RequestWrapper.getInputStream() returned a NEW ByteArrayInputStream on
every call. Under Jetty 12 ee10 + Jersey 3 the entity reader fetches getInputStream()
more than once; after consuming the body and reaching EOF, a later call returned a fresh
stream positioned at byte 0, so the next read yielded the body's own leading '{' again -
a START_OBJECT that the stricter Jackson jakarta-rs provider rejects (FAIL_ON_TRAILING_TOKENS).
The extra byte is the body's own first byte, never a real next-request byte, so on-the-wire
framing was never corrupted; the wrapper was simply not Servlet-contract-compliant.

Modifications (RequestWrapper):
- Return a single, stable ServletInputStream from getInputStream() (cache it), as required
  by the Servlet contract; re-fetching after EOF now yields EOF, not the body again.
- Buffer the body lazily, so an interceptor that does not read the body (the common case)
  never causes the body to be buffered and the underlying stream is consumed once, by Jersey.
- Bound the buffered read to Content-Length and report isFinished() correctly.

Verified locally: TransactionStablePositionTest (6), TransactionBufferCloseTest (4),
ExceptionsBrokerInterceptorTest, BrokerInterceptorTest and InterceptFilterOutTest all pass;
checkstyle + spotless clean.

Assisted-by: Claude Code (Opus 4.8)
Update the implementation notes CI iteration log: run 26700351491 (commit
8c70136) is fully green across all 39 jobs, completing Phase A of the
javax->jakarta migration with end-to-end validation on lhotari/pulsar.

Assisted-by: Claude Code (Opus 4.8)
….12.42 uses jakarta.xml.bind)

Motivation:
Earlier in this PR a temporary `runtimeOnly(libs.jaxb.api)` (javax.xml.bind:jaxb-api:2.3.1) was
added to pulsar-client-auth-athenz and pulsar-broker-auth-athenz to satisfy Athenz 1.10.62, whose
shaded jackson-module-jaxb-annotations referenced the legacy javax.xml.bind package (which the
jakarta.xml.bind-api 4.0.2 bump no longer ships).

master has since upgraded Athenz to 1.12.42 (apache#25905, 14e228c), which itself migrated to
jakarta.xml.bind: athenz-auth-core now pulls org.glassfish.jaxb:jaxb-runtime:4.0.8. The legacy
javax.xml.bind classes are therefore no longer needed on the Athenz runtime classpath, so the
workaround is obsolete.

Modifications:
- Remove `runtimeOnly(libs.jaxb.api)` from pulsar-client-auth-athenz and pulsar-broker-auth-athenz
  (both build files revert to their pre-PR state).
- De-Athenz the gradle/libs.versions.toml jaxb-api comment: the alias is retained because it is now
  used only by the tiered-storage offloaders (jclouds/Hadoop), which still reference legacy
  javax.xml.bind.
- Update pip/pip-472-notes.md (issue 2, the CI iteration log) to record the upstream resolution.

Verified: AuthenticationAthenzTest (8) and AuthenticationProviderAthenzTest (4) pass, and
javax.xml.bind:jaxb-api is no longer on the Athenz runtimeClasspath (only the jakarta stack:
jakarta.xml.bind-api 4.0.5 + org.glassfish.jaxb:jaxb-runtime 4.0.8).

Assisted-by: Claude Code (Opus 4.8)
Drop pip/pip-472-notes.md (internal implementation working notes — not part of the
upstream contribution) and the pip/pip-472.md "Implementation status / notes" pointer
that referenced it, so the accepted PIP design document is left unchanged by this
implementation PR.

Assisted-by: Claude Code (Opus 4.8)
@lhotari lhotari added this to the 5.0.0-M1 milestone Jun 1, 2026
@lhotari lhotari merged commit 6bfa77d into apache:master Jun 1, 2026
81 of 83 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants