Skip to main content

Release 10.0.0: REST server

This page is part of the Release 10.0.0 release notes. The overview page lists the major changes, the breaking changes and the API changes for the whole release.

juneau-commons / juneau-rest-server​

@ConfigProperties — prefix-scoped whole-object config binding, with RestContext as the pilot​

Juneau 10.0 adds a new @ConfigProperties(prefix = "...") annotation (org.apache.juneau.commons.inject) — Juneau's analog of Spring Boot's @ConfigurationProperties — plus its underlying, independently-callable engine, ConfigPropertiesBinder. Where @Value resolves one configuration value onto one field, @ConfigProperties resolves an entire prefix onto every field of a POJO in one pass:

@ConfigProperties(prefix = "MyService")
public class MyServiceProperties {
public String host = "localhost";
public int port = 8080;
}
  • Relaxed key matching by default — reuses RelaxedPropertySource.candidates(...), so MyService.host, MY_SERVICE_HOST, and my.service.host are all equivalent.
  • Same source-chain precedence as @Value — thread-local/global test overrides, then any caller-scoped PropertySource beans, then the process-wide Settings chain (registered sources, system properties, system environment).
  • Nested config objects — a field whose type is itself @ConfigProperties-annotated binds recursively under a sub-prefix.
  • Coexists with @Value/@Inject on the same class — fields owned by either are never touched by the whole-object bind.
  • Auto-registered in the BeanStore — the bound instance is retrievable via beanStore.getBean(...) or plain @Inject anywhere downstream.
  • Current limitations: no Optional<T>/Supplier<T> field types (raw-type conversion only); validation is a no-op extension seam (ConfigPropertiesValidator.NO_OP) reserved for a future release.

Pilot: RestContextProperties. As proof, RestContext's 15 same-prefix RestContext.* env-driven-default @Value fields have been collapsed into a single public @ConfigProperties(prefix = "RestContext") bean, RestContextProperties, retrievable via the new RestContext.getRestContextProperties() or from the resource's BeanStore. Every existing RestContext getter keeps its exact signature and @Rest-annotation-blending behavior — only the source of the underlying default moved. RestContext.uriAuthority/uriContext (a null-vs-empty distinction the binder's raw-type conversion can't express in v1) are unaffected and remain plain @Value fields.

See the new @ConfigProperties Annotation Basics topic page, and the RestContext topic page for the pilot.

@Bean factory-method fixes (juneau-rest-server / juneau-commons). Several @Bean(name=...) factory methods of the same type on a @Rest class now each register their own result. Previously they collapsed into one bean, so @Named("cache") could receive the "db" bean, and health indicators were lost. Each @Bean method is invoked exactly once: overridden methods once, and named framework types such as @Bean(name="encoders") EncoderSet are no longer invoked twice or exported to child resources. @Bean methods may now be protected, package-private or private, matching @Configuration classes. The @Bean("x") value form now matches named framework slots. New: BeanStore.invokeBeanMethod(...) and BeanAnnotation.find(MethodInfo).

juneau-rest-common / juneau-rest-server​

BeanQueryRequest request bean​

org.apache.juneau.http.BeanQueryRequest is a stock @Request class (a concrete BeanQuery subclass) that binds the usual list-fetch query parameters as a single method argument and feeds a BeanQuery session directly:

private static final InMemoryBeanQueryContext<Row> ROWS = InMemoryBeanQueryContext.create(Row.class).build();

@RestGet
public List<Row> getList(BeanQueryRequest q) {
try (var s = ROWS.getSession(rows)) {
return s.find(q).rows();
}
}

Each getter binds one single-valued @Query whose wire name equals the property name: search, view (a column projection), sort, position, limit and opts (renamed from opt). The $-language and the key=value opts bag live inside those single strings and are decoded by the session, not on the wire. Each getter carries a @Schema description, so an operation taking a BeanQueryRequest documents all six parameters in Swagger automatically (position also declares minimum: 0).

A malformed query is reported as HTTP 400 with an X-BeanQuery-Error header and, with problem details on, a "code" member in the body; see Queryable rebuilt on the BeanQuery engine below. The @Request bean's constructor and setters are now verified when the operation table is built (see the Breaking Changes entry on request beans).

Apps add extras by subclassing BeanQueryRequest and redeclaring a getter with its own @Query name (e.g. @Query("v") getView()). Saved-view CRUD endpoints must not take BeanQueryRequest (view there is a saved-view id, not a column projection).

See the BeanQuery Package topic page.

@Request getters now honor collectionFormat=multi (getAll, not last-wins) and @Query(def=) / equivalent def= on the other part annotations, including Optional<T> returns.

Content Security Policy (CSP) support​

Juneau can now emit Content-Security-Policy response headers for the HTML it serves, including dynamic per-request nonce support. New in this release:

  • A first-class immutable ContentSecurityPolicy HTTP header bean + fluent builder in juneau-rest-common (org.apache.juneau.http.header), with typed directive setters (defaultSrc, scriptSrc, styleSrc, imgSrc, connectSrc, fontSrc, objectSrc, baseUri, frameAncestors, reportUri/reportTo), a generic directive(name, sources...) escape hatch, source-expression helpers, report-only mode, and a ready-made nonce-based "strict starter" preset.
  • Per-response SecureRandom nonce generation: the same nonce is injected into the emitted Content-Security-Policy header and stamped onto every inline <script>/<style> Juneau emits — the HtmlDoc template, the Swagger UI, the OpenAPI UI, and menu-item widgets.
  • Opt-in by design: turning CSP on is a deliberate, one-line choice; existing apps are unaffected by default.

See the new Content Security Policy topic page for details.

This feature was inspired by an earlier proposal from Gary Gregory (@garydgregory, PR #57); the implementation shipped here is a fresh one built against the current 10.0 APIs.

CSRF protection for apps that are not on loopback (CsrfBoundary)​

LoopbackBoundary protects an app only when every request arrives under one pinned Host over plain HTTP. A new CsrfBoundary in org.apache.juneau.rest.server.filter covers everything else: an app reached under a real host name, over HTTPS, behind a proxy, or under more than one name.

  • Same-origin check with nothing pinned. On an unsafe request, Origin (with Sec-Fetch-Site as a fallback) must match the request's own scheme, host and port. A literal null origin is refused. Default ports are equated, and IPv6 literals are compared in bracketed form exactly as sent.
  • Header token. A browser request must also carry a per-session synchronizer token in a request header (X-Csrf-Token by default). HTML form posts cannot send a header, so they are unsupported by design.
  • Non-browser clients. A request with neither Origin nor Sec-Fetch-Site is admitted without a token only when a browser could not have sent it cross-site without a preflight (for client authors: send JSON, or send Origin plus the token). NonBrowser.REJECT and NonBrowser.REQUIRE_TOKEN tighten that.
  • Proxies. OriginSource.HOST_HEADER (the default) never reads X-Forwarded-*; OriginSource.CONTAINER takes the origin from the container's own forwarded-header support (Jetty ForwardedRequestCustomizer, Tomcat RemoteIpValve, Spring Boot forward-headers-strategy).
  • DNS rebinding. allowedHosts(...) refuses any request, safe ones included, whose host is not listed.
  • Applied by RequestBoundaryFilter, which works with either boundary. Register it at /* for REQUEST dispatches.
  • The microservice admin samples. AdminBoundary in juneau-microservice-examples registers a CsrfBoundary over ConfigResource, LogsResource and ShutdownResource, answering loopback host names only unless others are given, and the Jetty petstore installs it. See the microservice examples entry below.
  • Bus WebSocket handshakes. BusPolicy.boundary(...) now accepts a CsrfBoundary. A missing, null or malformed Origin is refused with HTTP 403 before the upgrade; a well-formed cross-site or mismatched Origin is refused after the upgrade with close code 4403, before any frame is sent or any bus session is touched. The handshake has no server name, so it judges the raw Host header (over HTTP/2, the request URI's authority) plus the request URI scheme, whatever OriginSource is set. Behind a proxy that rewrites Host with no container-side host rewriting, same-origin upgrades are refused; preserve Host at the proxy. BusWebSockets.register refuses any boundary type other than LoopbackBoundary and CsrfBoundary. Verified on Jetty and Tomcat; on Tomcat, RemoteIpValve forwards the scheme to the handshake but not the Host header, so a forwarded host is refused even with the valve's hostHeader set.
  • Opt-in trusted forwarded origin. CsrfBoundary.Builder.trustedForwarding(X_FORWARDED | FORWARDED) and trustedHops(n) let a boundary judge HTTP writes and bus WebSocket handshakes against the host and protocol a proxy you operate forwards, for deployments whose container does no forwarded-host handling. Off by default with no behavior change. Host and protocol are both required, the nth entry from the right is used, the forwarded origin replaces the container's rather than adding to it, and anything missing, malformed or in disagreement with the container is refused (seven new BoundaryReason values, HTTP 403; handshakes close with 4403). X-Forwarded-Port is never read.
  • Proxy allow-list for the forwarded origin. CsrfBoundary.Builder.trustedProxies(cidrs...) believes the forwarded header only from listed peer addresses (IPv4 and IPv6 ranges, validated at build; host bits are masked). Others carrying it are refused (FORWARDED_PEER_NOT_TRUSTED); others without it are judged as if forwarding were off. Off by default. HTTP where the container reports the TCP peer; WebSocket handshakes on Jetty; on Tomcat a forwarded handshake is refused while the list is set.

Behavior changes in the same work:

  • HTTP/2 requests with no Host header are judged on the container's :authority view. CsrfBoundary no longer refuses them with HOST_MISSING, and LoopbackBoundary no longer refuses them with HOST_MISMATCH. Bus WebSocket handshakes over HTTP/2 (RFC 8441 extended CONNECT, which Jetty enables by default with HTTP/2) are judged on their :authority instead of closing with 4403. Under HOST_HEADER, HTTP/2 requests are judged on the container's server name, which container forwarding support may rewrite. HTTP/1.x requests without Host are unchanged.
  • LoopbackBoundary judges the effective method. A GET carrying ?method=DELETE or an X-HTTP-Method-Override header now takes the write checks, even when the resource it reaches would not honor the override (it now does honor it where allowedMethodHeaders is set; see the next bullet).
  • Method overrides: all four headers honored, conflicts refused. X-HTTP-Method-Override, X-HTTP-Method and X-Method-Override are now honored under @Rest(allowedMethodHeaders) (and the global J_ALLOWED_METHOD_HEADERS), exactly as X-Method is; the setting is still empty by default, so applications that have not set it see no change. A request whose enabled override channels disagree (two different methods across ?method= and the headers, or repeated different values on one of them) is answered 400; identical repeats are fine. The method query parameter is now matched case-insensitively (?METHOD=PUT works) and read from the query string only, so a form-body method= field is no longer honored. The MCP bearer gate treats a POST claim on any of these channels as a POST.
  • BREAKING: method overrides on safe requests are restricted. A GET, HEAD, OPTIONS or TRACE request may now be overridden (through ?method= or any of the four override headers) only to a safe method (GET, HEAD, OPTIONS, TRACE) or to a custom method whose operation is declared @RestOp(read=true). An override to any other method, for example GET ?method=DELETE, GET ?method=POST on an RRPC operation, or GET ?method=FOO reaching a method="*" operation, is refused with 405 Method Not Allowed and an Allow header, instead of being dispatched. Overrides on POST (for example POST ?method=PATCH for clients without PATCH) are unchanged. Defaults are unaffected: allowedMethodParams still defaults to HEAD,OPTIONS. Migration: mark read-only custom verbs @RestOp(read=true); change links that used ?method= to trigger a write into a POST (a form or a script), or call the method directly (curl -X DELETE); prefer path-based read links (/view/{file}) over ?method=VIEW, because a CSRF boundary still treats any custom-verb claim as a write. read=true on POST/PUT/PATCH/DELETE/CONNECT, *, RRPC, or together with @Mutating fails at startup. An MCP server mounted as a mixin now answers an unauthenticated GET ?method=POST with 405 rather than 401. Every framework 405 now carries an Allow header, including the existing one for a path that matches under another method.
  • Rejections and token-bearing pages are no-store. A rejected request is answered with Cache-Control: no-store. Code that stamps a token into a response (ViewTable.CSRF_ATTR, RegionTable.of(region, csrfToken), PageSpec.csrf(...)) must call RequestBoundaryFilter.markTokenBearing(response), which sets Cache-Control: private, no-store.
  • No token in a committed response. A token is never embedded into a response that is already committed, including csrf attributes an author put on the page's body element. The page's writes then fail visibly.
  • Breaking (unreleased API): the nested LoopbackBoundary.Reason and LoopbackBoundary.Result types are now the top-level BoundaryReason and BoundaryResult, with no aliases. Code built against a 10.0.0 snapshot needs the new names.
  • Breaking (unreleased API): the rejection header is now X-Request-Boundary (it was X-Loopback-Boundary).

See REST Security and the Views Security Model.

Actuator-style management surface​

Juneau 10.0 adds a coherent, discoverable Actuator-style management surface while keeping Juneau's a-la-carte composability. Every endpoint ships in two independently-mountable flavors — a composable *Mixin and a routed *Resource sharing one worker — and a convenience BasicActuatorGroup assembles them under one configurable path prefix (default /actuator, overridable via the juneau.actuator.path system property).

New endpoints:

  • /info (juneau-microservice, org.apache.juneau.microservice.management) — renders the running application's ManifestFile main attributes (build/version/git metadata). Degrades to {} when no manifest is registered. Build-time git/version stamping goes in the consumer's application-jar build (a git-commit-id + maven-jar-plugin recipe is in the topic page).
  • /loggers (juneau-rest-server, org.apache.juneau.rest.server.management) — read all logger levels and (deny-by-default) set a level at runtime. Drives java.util.logging by default; Logback / Log4j2 backends are an explicit opt-in (see the dedicated entry below).
  • /metrics (juneau-rest-server-metrics-micrometer) — renders a Prometheus scrape from a consumer-provided Micrometer MeterRegistry resolved from the bean store; degrades to 501 when no scrapeable registry is present. Never auto-registers a default registry.
  • /threaddump + /heapdump (juneau-rest-server) — server-agnostic ThreadMXBean thread dump and HotSpot .hprof heap dump, both deny-by-default on security grounds (opt in via DumpsSettings).

Exposure is on-by-default-with-gating: non-sensitive reads are exposed; mutating/sensitive operations (/loggers set-level, the dumps) are deny-by-default behind explicit opt-in settings beans (LoggersSettings, DumpsSettings). No auth provider is auto-wired — the consumer supplies their own guard.

BasicActuatorGroup mounts only /healthz//readyz//livez and the dumps by default; /info and /loggers are off. The convenience group composes HealthMixin + DumpsMixin only — InfoMixin and LoggersMixin are deliberately left out, since an unauthenticated manifest dump (/info) and logger-topology disclosure (/loggers read) are both meaningful information leaks with no guard auto-wired. Re-enable either (or both) with an a-la-carte subclass adding @Rest(mixins={InfoMixin.class, LoggersMixin.class}) (additive to the inherited mixins). Applications that mount InfoMixin/LoggersMixin directly (not through BasicActuatorGroup) are unaffected.

See the new Management Surface topic page for the endpoint catalog, the BasicActuatorGroup one-liner, the exposure policy, and the manifest-stamping recipe.

/loggers non-JUL backends (Logback, Log4j2)​

The /loggers endpoint (above) now drives a pluggable LogBackend instead of being hardwired to java.util.logging. juneau-rest-server still ships only the JUL backend (JulLogBackend, the default) so the core stays dependency-free, and a new opt-in juneau-rest-server-management-logging module adds LogbackLogBackend + Log4j2LogBackend (the backends declared provided, so they never leak transitively).

  • Explicit selection (no classpath auto-detection, per the explicit-over-magic principle): declare the backend on the existing settings bean — LoggersSettings.create().backend(new LogbackLogBackend()).build().
  • Identical response shape across backends: read reports the configured level (empty string = inherited from an ancestor; ROOT key for the root logger); set is process-lifetime-only (no config-file rewrite). Level names are backend-native; an invalid name fails 400.
  • Drives the concrete backend, not the SLF4J facade: LogbackLogBackend for SLF4J→Logback, Log4j2LogBackend for Log4j2; an slf4j-jdk14 binding is JUL, so the default backend already covers it. Custom backends implement LogBackend directly.

This closes the 174b JUL-only /loggers limitation. See the extended Management Surface topic page.

Host-side mixin overrides — @Rest(mixinDefs=@Mixin(...))​

@Rest(mixins=...) lets a mixin class declare its own @Rest settings that apply only to its endpoints — but the composition decision often belongs to the host. New in 10.0.0, the @Rest(mixinDefs=@Mixin(...)) attribute lets a host declare a mixin and override selected @Rest-level settings for that mixin's endpoints, in one place, without editing or subclassing the mixin:

@Rest(mixinDefs=@Mixin(type=AdminResource.class, guards=AdminGuard.class))
public class ApiResource extends BasicRestServlet { ... }
  • Additive / non-breaking. mixinDefs coexists with the existing mixins=Class<?>[]; a @Mixin(type=X.class) with no overrides is exactly equivalent to a bare mixins=X.class entry, and existing usages are untouched.
  • Override slots mirror the composition-shaped @Rest attributes: guards, roleGuard/rolesDeclared, converters, encoders, serializers/parsers, responseProcessors, restOpArgs, partSerializer/partParser, messages, the default request/response headers + attributes, produces/consumes/defaultAccept/defaultContentType/defaultCharset, and maxInput.
  • Precedence: the host override layers on top of the inherited host chain (list-shaped props append, replace-shaped props win) and wins over the mixin class's own declaration. @Mixin carries its own noInherit (unioned with the mixin class's @Rest(noInherit=...)) so the host override is authoritative but overridable by noInherit.
  • Host-chosen re-mount: @Mixin(path=...)/paths={...} re-mount the mixin's endpoints under host-chosen prefix(es), reusing the existing pathToken normalization.
  • Transitive mixins are overridden only when separately named.

Internally this also normalized mixin discovery (one ResolvedMixin carrier for both bare and rich forms) and replaced the RestContext.Args boolean mixinContext flag with a typed ContextKind discriminator (Root/Child/Mixin). See Host-side overrides with @Mixin.

Mixin-declared response-processor fold — @Rest(mergeResponseProcessorsIntoHost=true)​

By default a mixin's responseProcessors apply only to the mixin's own endpoints — a host's own @RestOp methods see only the host's chain. The host-side @Mixin(mergeIntoHost=true) directive can fold all of a mixin's list-shaped attributes into the host, but it must be spelled out at every use-site. New in 10.0.0, a mixin class can declare @Rest(mergeResponseProcessorsIntoHost=true) on itself, so that a plain @Rest(mixins=ThatMixin.class) reference automatically folds only the mixin's responseProcessors into the host's own chain:

@Rest(responseProcessors=FreemarkerViewRenderer.class, mergeResponseProcessorsIntoHost=true)
public class FreemarkerMixin { ... }
  • Opt-in and non-silent. Defaults to false; only a mixin that declares it changes behavior. Mixins that don't keep today's isolated scoping exactly — no change to the default for arbitrary existing mixins.
  • Response-processor-scoped. Folds only responseProcessors — never guards, serializers, or any other list-shaped attribute (the narrower, mixin-declared counterpart to the host-side, fold-everything @Mixin(mergeIntoHost=true)). Same-class de-duplication applies.
  • View mixins opt in. All four view-renderer mixins — FreemarkerMixin, JspMixin, MustacheMixin, ThymeleafMixin — now declare it, so a bare @Rest(mixins=FreemarkerMixin.class) (etc.) renders the host's own View returns through the engine with no mergeIntoHost or explicit responseProcessors= ceremony.

See REST Server — Mixin Sub-Contexts and FreeMarker View Support.

Host-side child seeding — @Rest(childrenDefs=@Child(...))​

Children are deliberately isolated from the host's resolution chain — a child's serializers, parsers, guards, part serializer, etc. resolve against its own RestContext only, never inherited from the parent. New in 10.0.0, the @Rest(childrenDefs=@Child(...)) attribute lets a host seed a curated set of settings onto a routed child's otherwise-isolated context, without editing the child class — the child-resource analog of the @Rest(mixinDefs=@Mixin(...)) attribute above:

@Rest(childrenDefs=@Child(type=AdminResource.class, partSerializer=AdminPartSerializer.class, guards=AdminBearerGuard.class))
public class ApiResource extends BasicRestServlet { ... }
  • Additive / non-breaking. childrenDefs coexists with the existing children=Class<?>[]; a @Child(type=X.class) with no seed members is exactly equivalent to a bare children=X.class entry, and if both name the same class the rich entry wins (the bare entry is upgraded in place).
  • Two seed-semantics buckets — since children are isolated by design, a seed can't "override" an inherited chain the way a @Mixin override does; it can only add to or fill gaps in the child's own config: additive-security (guards, converters, roleGuard, rolesDeclared) — the host's contribution is added alongside the child's own (list-shaped members prepend; the two role-based members AND-stack) and can never be removed or weakened by the child; child-wins scalars (partSerializer, partParser, defaultCharset, maxInput) — the seed is a fallback that applies only when the child leaves the property unset, and the child's own explicit @Rest declaration always wins.
  • noInherit cuts the seed too. A child's own @Rest(noInherit="<property>") suppresses the corresponding host-seeded value, for either bucket, so the child always stays in full control of its own configuration.
  • Survives lazy materialization — a seed applies to a @Rest(lazyChildren="true") child on first request exactly as it would to an eagerly-built one.
  • Not (yet) seedable: defaultAccept/defaultContentType were originally planned but dropped before shipping — they resolve through a shared first-wins mechanism that would make the seed win over the child's own value, the opposite of every other child-wins scalar's contract.

See Host-side seeding with @Child and REST Server — Children vs Mixins.

Child resources inherit their parent's @Bean group beans​

@Rest(children=...) child resources now see the @Bean methods and non-null @Bean fields declared on their parent (the "group"), instead of only the parent's bootstrap bean store. Each RestContext now has an inheritable bean store — its own @Bean results layered over its bootstrap store — and that store is used as the parent/bootstrap store of every child.

@Rest(children={PetResource.class, OrderResource.class})
public class RootResources extends BasicRestServletGroup {

// One store shared by every child resource.
@Bean
public PetStore petStore() {
return new PetStore();
}
}
  • New API: RestContext.getInheritableBeanStore() returns the store a resource exposes to its children.
  • Transitive. Each child's inheritable store sits on top of its parent's, so a grandchild sees the beans of its parent, its grandparent, and so on up to the root (including any Spring context bridged in at the root).
  • Child wins. A child's own @Bean of the same type and name takes precedence over the inherited one.
  • Framework-managed beans stay per-context. Serializers, parsers, the call logger, RestChildren and similar are not exported; each child still builds its own from its own @Rest configuration.
  • Beans flow down only. A child's beans are never visible to its parent or siblings.

Custom observations + one-dependency OTLP export + log correlation​

Juneau 10.0 extends its request-boundary observability to cover custom (non-request) observations, ships a single OTLP export bundle, and adds OpenTelemetry trace-id log correlation — all keeping the explicit-over-magic, off-by-default contract (no classpath auto-configuration).

  • Custom-observation API (org.apache.juneau.rest.server.observation): a new explicit programmatic API — Observations.observe(name, tags) (try-with-resources Observation handle), an Observer that composes the existing SPIs, and non-request default methods on the two SPIs (MetricsRecorder.record(name, tags, elapsed, error) + TracerHook.startSpan(name)). Instruments an arbitrary block of work with a timer + span; reuses the exact request-path metricName/metricTags model. No @Observed annotation — Juneau has no AOP/method-interception substrate, so observation is an explicit block (consistent with the explicit-over-magic philosophy). No-backend path is zero-allocation. The Micrometer + OTel bridges override both default methods (OTel opens an INTERNAL-kind span, nested under any active request span).
  • juneau-observability-otlp-bundle: a curated dependency bundle (under juneau-bundles) that pulls both Juneau observability bridges plus a version-aligned OTLP exporter stack — opentelemetry-exporter-otlp (+ SDK + autoconfigure) for traces and micrometer-registry-otlp for metrics — in one dependency. Metrics + traces for v1 (OTLP logs export deferred). A dependency aggregator only; you still wire the @Bean MetricsRecorder/@Bean TracerHook explicitly.
  • Log correlation: the JUL LogEntryFormatter gains optional {traceId}/{spanId} placeholders, sourced reflectively from the active OTel Span.current() (no hard OTel dependency — they render empty when OTel is absent). MdcAsyncListener additionally folds the active trace/span id into the propagated SLF4J MDC snapshot (keys trace_id/span_id) so correlation survives the async-completion hop, gated by isMdcAsyncPropagation() and kept reflective. SLF4J→Logback/Log4j2 apps wire their own MDC pattern (JUL-only is a documented v1 limitation). This also fixed a latent multi-digit format-group bug in the LogEntryFormatter regex builder.

See the extended Observability — Micrometer + OpenTelemetry topic page.

Resource-level authentication — RestAuthenticator​

Juneau 10.0 adds a RestAuthenticator SPI (org.apache.juneau.rest.server.auth) — an ergonomic, in-resource way to resolve a request's Principal + roles so roleGuard, RoleBasedRestGuard, @Auth Principal, and req.isUserInRole(...) all work with no container-managed security and no servlet-layer filter. It complements the AuthN Filter Framework and reuses the same underlying auth implementations.

  • Shared Authenticator contract — a new Authenticator functional interface (Optional<AuthResult> authenticate(HttpServletRequest)) is now implemented by both AuthFilter and AuthFilterChain, so every existing filter (SAML / OAuth / JWT / API-key / bearer) is reusable verbatim at the resource layer. RestAuthenticator.of(Authenticator) adapts any of them.
  • AuthResult merge modes — AuthResult gains an immutable merge mode: of(...) = ADD (union roles, principal required — unchanged), ofRoles(...) = roles-only ADD (null principal — keeps the inherited identity), replacing(...) = REPLACE (swap identity outright). The existing of(...) factories and behavior are preserved (backward compatible).
  • Registration & DI — resolved per RestContext with precedence registered bean › @Bean method › @Rest(authenticator=...) › RestServlet.createAuthenticator(BeanStore). Both Authenticator-into-RestAuthenticator and RestAuthenticator-into-resource injection work through the bean store.
  • Inheritance & folding — unlike guards, authenticators inherit down the child-resource tree: a single root authenticator covers all descendants, which may augment (add roles), replace (swap identity), or opt out with @Rest(noInherit={"authenticator"}).
@Rest(children = { ChildResource.class })
public class RootResource extends BasicRestServlet {
@Bean
public RestAuthenticator authenticator(AuthFilterChain chain) {
return RestAuthenticator.of(chain); // covers root + all descendants
}
}

See the new REST Authenticator — Resource-Level Authentication topic page.

Queryable rebuilt on the BeanQuery engine + juneau-console-datatables (new module)​

Juneau 10.0 replaces Queryable's hand-rolled search/view/sort/paging code (ObjectSearcher/ObjectSorter/ObjectViewer/ObjectPaginator) with the shared BeanQuery engine, so one search language, one set of limits and one error model cover REST responses, DataTables server-side processing and SQL-backed queries. A new opt-in module also ships a DataTables server-side processing adapter plus browser-side helpers.

  • Queryable on BeanQuery — the search / view / sort / position / limit / opts request parameters (bound by BeanQueryRequest) run against an InMemoryBeanQueryContext built from the response's row type (one context per element class, cached for the life of the converter; Map rows get a per-request context). Example: GET /people?search=age=$gte(18)&sort=name,age:desc&view=name,age&position=20&limit=10. Non-tabular results are returned unchanged.
  • QueryableSettings — a bean-store-resolved per-resource bean (mirroring IntrospectableSettings) that tunes the context: defaultLimit (100), maxLimit (1000), allowRegex (on by default, allowRegex(false) opts out), regexTimeout, countPolicy, and the search-clause / sort-key / search-length / expression-depth caps. Unset settings keep the BeanQuery builder defaults.
  • Counts — ?opts=counts=true adds X-BeanQuery-Total and X-BeanQuery-Matched response headers (each only when that count was computed). The body stays a bare list.
  • Central error mapping — RestContext.convertThrowable maps BeanQuerySyntaxException to HTTP 400 with an X-BeanQuery-Error: <CODE> header (BeanQueryRequest.ERROR_HEADER) and, with problem details on, a "code" member in the body; BeanQueryExecutionException maps to HTTP 500 with the fixed body Query execution failed. (the cause is logged, never sent to the client). The DataTables adapter uses the same contract (next bullet).
  • juneau-console-datatables module (org.apache.juneau:juneau-console-datatables) — a new opt-in add-on (not bundled in juneau-distrib, mirroring the juneau-rest-server-management-logging precedent) with:
    • DataTablesQuery (org.apache.juneau.console.datatables.adapter) — maps the DataTables server-side contract onto BeanQuery: DataTablesQuery.toBeanQuery(req, ctx) builds a BeanQuery from a DataTablesRequest (per-column search, order, paging, and a global search that expands to an $or of a per-SearchType leaf — $contains/$prefix/$eqic/$eq — over every searchable, resolvable column, not just text), and DataTablesQuery.run/runArrays execute it and map the result onto the DataTables envelope. The browser posts the draw state as JSON (not DataTables' default form-encoded request) through a new JuneauDataTables.ajax(url, extra?) helper in juneau-datatables.js, or the data-juneau-datatable-ajax auto-init attribute. A malformed request (unknown column, a maxSearchClauses/maxSortKeys cap exceeded, a bad search expression) raises BeanQuerySyntaxException, which the adapter does not catch: the central mapping answers HTTP 400 with an X-BeanQuery-Error: <CODE> header and the message in the body, exactly like any other Queryable endpoint. JuneauDataTables.ajax installs an xhr.dt listener that shows that server message through DataTables' normal error path (the error.dt event, then DataTable.ext.errMode) instead of a generic "Ajax error"; other failed requests keep the DataTables default. An execution failure is HTTP 500 via the central BeanQueryExecutionException mapping. See DataTables.
    • Views runtime server-mode tables now ride the same wire format. A juneau-console-views table with dataMode: "server" now gets serverSide: true and ajax: JuneauDataTables.ajax(url, {...}) (in juneau-views.js: buildOptions → buildServerAjax) instead of a hand-built GET config, so there is one wire format end to end. Breaking for existing server-mode endpoints: they must now be @RestPost methods taking @Content DataTablesRequest, not GET handlers reading query-string parameters. Column-scoped ribbon options now ride the JSON body as that column's search.value (ribbonColumnSearches), $and-merged with the user's own search on the same column (mergeColumnSearches) — fixing a bug where such filters were silently dropped. param:-scoped ribbon options (ribbonQueryParams) and the nested-table parent-scope param (applyNestedScope) stay URL query parameters, re-derived on every request through beforeSend. Client-mode tables are unchanged. A page must load juneau-datatables.js before juneau-views.js; otherwise server mode fails loudly (a warning) instead of silently falling back to GET. See DataTables.
    • DataTablesResults<T> — the {draw,recordsTotal,recordsFiltered,data} (+ optional error) envelope bean, rendered by the negotiated serializer (no hardcoded JSON).
    • Browser-side helpers — DataTablesColumns (generate columns:[…] from bean-property order + @Schema titles), DataTablesTable (build a DataTables-ready HTML5 Table DOM bean, with MarshallingContext overloads), and DataTablesMixin (serves a first-party juneau-datatables.js auto-init glue script). DataTables' own JS/CSS and jQuery remain caller-provided (not vendored — their license is not ASF category-A).

Themeable admin-console chrome + FreeMarker macro composition (new modules: juneau-console, juneau-console-freemarker)​

Juneau 10.0 adds two opt-in modules that give an admin-console-style resource a themeable CSS "chrome", an enum status/domain pill component, and (for FreeMarker consumers) macro-based composition — without pulling a template engine or the DataTables add-on into juneau-rest-server core. The two modules form an acyclic dependency chain (juneau-console → juneau-console-freemarker); a chrome-only consumer never drags in FreeMarker or DataTables transitively.

  • juneau-console — ConsoleChromeMixin serves a dynamically themed chrome.css (the module's shipped structural stylesheet plus a :root block built from validated --jc-* theme tokens); Theme validates every token value through a normalize-then-accept-known-safe pipeline (reject C0/C1/DEL control chars → strip CSS comments → case-insensitively reject any url( production → match an allowlist grammar for colors/fonts/lengths/gradients) rather than a denylist, so a token value can never smuggle a url()-based injection. Color/font/length tokens sink only into non-URL-capable CSS properties; the sole URL-capable sink (background-image, for --jc-page-bg/--jc-avatar-bg) is unreachable via url() anyway since the grammar structurally excludes that production. The console logo is a static chrome.css constant, not a configurable token. The reusable enum pill/badge component (<span class="tag <domain> <value>">) is rendered by the <@tag domain=... value=.../> FreeMarker macro in the console base.ftlh (see Admin Console Chrome).
  • Console theming is FTL-only: custom palettes are authored with <@theme>/<@token> (value= for leaves, alias= for live var() references). ThemePack, ThemeSettings, ThemePackSettings, ConsoleChromeMixin.Builder.pack(...)/pageBackgroundImage(...) and the /juneau-console/assets/logo/page-bg endpoints were removed; ConsoleChromeMixin.create().theme("<stock-name>") selects a stock palette for the served chrome.css.
  • Page Tabs and Page Subtabs — a Page Tab is 38.5px tall: a 3px accent border, 8px/10px padding, and 13px text at line-height 1.5. A Page Subtab is 30px tall. The selected Page Tab and the selected Page Subtab are bold. The box is themeable through --jc-nav-tab-padding-block, --jc-nav-tab-padding-inline, --jc-nav-tab-line-height, --jc-page-nav-child-padding-block and --jc-page-nav-child-padding-inline.
  • <@console theme="<stock-name>"> still selects a built-in stock theme (an unknown name is rejected fail-closed); a nested <@theme name=> inside the directive overrides it.
  • The juneau-rest-server-theme-packs module was removed.
  • juneau-console-freemarker — ConsoleFreemarkerMixin extends FreemarkerMixin (register it typed as FreemarkerMixin, mirroring the load-bearing-return-type rule FreemarkerMixin beans already follow) augments the resolved Configuration's TemplateLoader with a second, classpath-root-rooted ClassTemplateLoader, without shadowing the consumer's own basePath-relative templates. This resolves the reserved org/apache/juneau/console/base.ftlh template (pulled in via a loader-root-absolute <#include "/org/apache/juneau/console/base.ftlh">) and its <@tag domain=... value=.../> macro — the trusted-HTML adapter for the pill component above. A consumer-supplied Configuration bean (Spring Boot autoconfig, etc.) is honored untouched, not augmented.
  • DataTablesTable additive @Html(render=...) honoring (in the existing juneau-console-datatables module) — DataTablesTable.of(MarshallingContext, String, Collection<?>, Class<?>) now resolves and applies each row property's @Html(render=...) the same way the ordinary HtmlSerializer/HtmlDocSerializer path already does, so a column whose bean property declares a custom @Html(render=...) (for example an enum rendered as a status pill) renders through it instead of as a raw enum name. This is not a purely-additive change: a bean already using @Html(render=...) for the existing serializer path now also renders through that same render when passed through this DataTablesTable overload, where it previously rendered the raw value. Map-based rows are unaffected (no BeanPropertyMeta to resolve a render from).
  • juneau-console-freemarker — shareable-URL state and more datatables catalog keys. The views toolkit pack now includes juneau-urlstate.js (after juneau-pagestate.js), so the View Settings Copy link button and shareable-URL state work on <@page toolkit="views"> pages without loading the script by hand. <@card type="datatables"> catalogs now pass copyLink, rowActions, rowClassRules and pausePollingWhileEditing through onto the view, and selection and bulk through onto the slot; previously these keys were silently dropped on FTL-built pages.
  • juneau-console-views / TableSpec — urlState key and the day(YYYY-MM-DD) directive. A table view can now opt in to built-in shareable-URL directives with TableSpec.urlState(String...) (or urlState: {day: {}} in a catalog; the page schema validates it). day stores a calendar date in ?state= and passes it to the server as a day query parameter (rename it with TableSpec.urlStateDayParam(String param), emitted as urlState.day.param) so a resource can return that day's [start, end) window; malformed dates are ignored. Views without the key are unchanged.
  • juneau-console-freemarker — open card-type registry. Unknown well-formed card types now pass through the server, and the JS shell fails them with E-JS-4. <@card> gains a src= attribute. A bare-URL datatables body is removed (it is now E-25), and searchType validation moves to render time (E-32). The bulk wire contract is "2" (aggregate mode, BulkResult). <@card type="datatables"> is the one table surface.
  • QuickStat gains meter and segmented kinds. QuickStat.meter(id, label, value, max) paints a filled track with role="meter", and QuickStat.segmented(id, label).segment(...) paints a proportional bar with a text legend. Both require a non-blank label. The new statusTone(StatusTone) sets an item tone from the shared status palette; tone(QuickStat.Tone) is unchanged.
  • Inline datatables catalogs refuse a malformed quickStats strip (E-37). This includes an off-palette tone on any item, plain tiles included. Such tones used to be dropped silently by the client.

See the new Admin-Console Chrome topic page.

Console page contract and JS shell​

<@console> pages now carry a versioned JSON page contract (#juneau-page, schema juneau-page.schema.json) and load one shell script, /juneau-console/juneau-console.js, which draws the header, nav (any depth, aria-current from activeNav or a segment-aware URL fallback), cards and footer. The server keeps rendering <head>.

  • New: PageContractAssert (published test helper), ConsoleTemplateValidator (static template lint), ConsoleFreemarkerMixin.Builder.devMode(boolean) (schema validation at render time), jcHasToolkit(name), <@footer text="…"/>, and window.JuneauConsole (contract, card, registerCard, mount, chrome, and the juneau:console-mounted event).
  • Loud failures: template mistakes fail the render with E-1 to E-18; contract problems in the browser show a .jc-console-error banner (E-JS-1 to E-JS-12, then E-JS-14).
  • Fix: a @Bean returning ConsoleFreemarkerMixin (rather than FreemarkerMixin) is now found; previously it silently fell back to the default mixin without the console directives.
  • BREAKING (10.0 snapshots only): the page contract's top-level version key is renamed contractVersion, matching every other Juneau envelope contract (ModalDef, FormDef, BarSlot, ActionResult, RegionDef, the card/slot envelopes). The value is unchanged ("1"). There is no fallback: juneau-page.schema.json now requires contractVersion and forbids version, juneau-console.js reads only contractVersion (JuneauConsole.SUPPORTED_VERSIONS is now SUPPORTED_CONTRACT_VERSIONS), PageContractAssert.hasVersion(String) is renamed hasContractVersion(String), and assertPage(...) throws if a contract still carries version. Pages are generated by <@console>, so upgrading Juneau is the only step; code that hand-builds a page contract, or calls hasVersion, must be updated. Browsers that cached the previous juneau-console.js (served with a one-day max-age) show an E-JS-2 banner until the cache expires or the page is hard-reloaded.
  • BREAKING: the FTL globals pageBody, pageTab, pageInit, pageCss, pageToolkit, pageToolkitCss, pageToolkitJs and pageThemeCss are removed (use jcHasToolkit("views")). The legacy chrome templates that did not use <@console> are removed; a <@page> whose chrome renders no <@console> is an error. The <@card> types js, json and calendar are removed (use type="html" with a body or a registered card type), as are the data-juneau-card-sidecar scripts (no directive emits them any more; the orphaned juneau-page-cards.js asset is deleted). juneau-chrome.js is absorbed into juneau-console.js. Server HTML no longer contains nav, header, footer or card wrappers; tests that matched that HTML must move to PageContractAssert.

Card asset dependencies​

Console cards now declare the scripts and stylesheets they need, and <@page> loads each one once, in dependency order. See Card Asset Dependencies.

  • New: ToolkitPack (packs with dependsOn and a VENDOR / RUNTIME kind), the built-in packs jquery, datatables, datatables-glue and datatables-buttons, <@card requires=...>, and ConsoleFreemarkerMixin.Builder methods registerToolkitPack(ToolkitPack), cardRequires(type, packs...) and providedPacks(names...). build() fails on an unknown pack, a cycle, or a VENDOR pack depending on a RUNTIME pack.
  • New: <@card class="..."> adds one or more CSS classes to the card's wrapper element, so a page can keep its own scope class on the card. Each name must be a plain CSS class token or the render fails. See Admin Console Chrome.
  • New: WebJarsMixin and WebJarResolver in juneau-rest-server serve /webjars/* from the classpath. juneau-console-datatables now depends on the jQuery and DataTables WebJars (DataTables 2.3.8, Buttons 3.2.6, jQuery 3.7.1).
  • Changed: every <@card type="datatables"> loads jQuery, DataTables and juneau-datatables.js, in client and server mode. Previously the glue loaded only for server-mode cards and the page had to load jQuery and DataTables itself. A host with datatables cards composes WebJarsMixin as well as DataTablesMixin.
  • BREAKING (10.0 snapshots only): juneau-datatables.js left the views pack. ToolkitPackRegistry.Resolved is split into vendorCss, vendorJs, runtimeCss and runtimeJs, and the resolve(names, req, boolean) overload is removed. A page that loads jQuery or DataTables through <@scripts phase="after-toolkit"> must drop those tags (they would load twice, after the glue). A copy loaded through init= or css= is skipped only when its file name matches the pack's; an app with its own copies should name the packs in providedPacks(...) or mount WebJarsMixin.
  • Removed (10.0 snapshots only): APIs left unused once every consumer moved to card-declared packs. DataTablesMixin.JQUERY_CDN_URL, DATATABLES_JS_CDN_URL and DATATABLES_CSS_CDN_URL are gone; jQuery and DataTables now come from the WebJars. PageCapture.hasServerModeTable() is gone, because every datatables card now requires the datatables-glue pack. On ToolkitPackRegistry, the 2-arg resolve(names, req) and the register(name, cssPaths, jsPaths[, resolver]) overloads are gone; use resolve(names, req, pageUrls, template) and register(ToolkitPack). ConsoleFreemarkerMixin.Builder.registerToolkitPack(name, cssPaths, jsPaths) and providedPacks(...) stay.

JUL-level-driven REST debug/logging (RestDebugFormatter)​

Juneau 10.0 replaces the multi-signal REST debug/logging system (@Rest(debug)/@Debug/DebugEnablement/ CallLogger) with a single, JUL-logger-level-driven mechanism. This is a hard break with no deprecation shim (unreleased 10.0 surface) — see the Breaking Changes section below and the V10 Migration Guide for the full removed-symbol mapping.

  • One signal. A resource's own java.util.logging.Logger level is the sole control for request/response debug capture — no annotations, no per-request headers, no separate config keys. Verbosity is cumulative: INFO = basic, FINE = + headers, FINEST = + bodies (only when the body-dump gate is set — see the secure-by-default entry below), one LogRecord per request (stable emitted level: INFO).
  • Per-operation granularity falls out of ordinary JUL parent-inheritance via a hierarchical child logger (resourceClass.getName() + "." + methodName) — elevate one method to FINEST without touching the rest.
  • Off by default. Under stock JUL (nothing configured for the resource) no debug record is built or logged, so a running service no longer writes one line per request. The status line appears only when a level of INFO or finer is set explicitly on the resource's logger (or an enclosing logger such as its package), and FINE/FINEST add headers/bodies as before. The record is not constructed at all when it would not be emitted. Apps that relied on the per-request line appearing by default must now set the level, e.g. Logger.getLogger("com.acme.MyResource").setLevel(Level.INFO).
  • Spring Boot propagation path. For real Logback backends, logging.level.* now propagates into JUL by default (juneau.rest.logging.propagate-levels=true), with an additive default posture (juneau.rest.logging.propagate-levels.reset-jul=false) and silent no-op fallback for non-Logback backends.
  • New RestDebugFormatter SPI (org.apache.juneau.rest.server.logging) — a per-tier formatter interface (formatBasic/formatHeaders/formatBody) with a shipped BasicRestDebugFormatter default. It's the only public extension point; the two-phase capture pipeline underneath it is internal. Resolution precedence: resource-implements-the-interface > BeanStore-registered bean > BasicRestDebugFormatter.
  • Secure by default (hardened). BasicRestDebugFormatter no longer dumps bodies just because a logger was raised to FINEST. See the dedicated secure-by-default entry below for the full contract.
  • Bounded body buffering. Request/response body-caching wrappers are installed only when the resolved logger is loggable at FINEST, and buffer at most the configured cap rather than the entire stream — an improvement over the prior unbounded debug body caching.
  • Completion-path emission for async/reactive responses. True asynchronous responses (CompletableFuture/CompletionStage) and reactive SSE/NDJSON streams now emit their single debug record on the response-completion path — after the async body and any completion-time headers have been written — instead of during the synchronous request finish. This fixes incomplete capture where an async record could be emitted before its late headers/body existed. A resolved logger/formatter/detail-tier snapshot is captured on the request thread at the async hand-off and carried across the completion hop; finish-time attributes such as Exec time are recorded on the completion path. Synchronous responses and the MockRestClient/non-async-container fallback are unchanged (still emitted during synchronous finish), and @RestEndCall still runs on the request thread. Exactly one INFO record is emitted per request on every path.
  • Logger level controls verbosity only. REST-driven marshalling debug behavior and EchoMixin reachability are decoupled from the JUL level (see the dedicated entry below). RestRequest.isDebug() still exists, now derived and read-only (true when the resolved logger is FINE-or-finer), but the framework no longer keys any behavior off it.

See the new Logging / Debugging topic page for the full model.

Decoupled EchoMixin reachability and marshalling debug from log level​

Two side effects that previously rode the JUL logger level are now controlled by their own explicit, non-logging switches — so raising a logger to FINE/FINEST changes verbosity only. This is an unreleased-10.0 hard break (no compatibility shim, no new echo annotation).

  • EchoMixin is disabled by default. /echo/* now returns 404 Not Found regardless of logger level. Enable it explicitly: EchoMixin.create().enabled().build(). Enablement is tri-state — an explicit enabled(true)/ enabled(false) wins; when unset it resolves the ${juneau.echo.enabled:false} SVL fallback (system property / relaxed environment variable). Because that fallback is JVM-global, prefer per-host builder enablement and pair it with the host's inherited @Rest(guards=...) chain. This closes an accidental attack-surface expansion where raising a logger to FINE silently un-hid a request-reflecting endpoint.
  • Marshalling debug behavior has a dedicated cascading setting. Serializer/parser debug behavior (richer/ stack-prefixed serialization exceptions and retained/quoted parse input in error messages) is now driven by debugMarshalling, a String on all eight REST annotation types (@Rest, @RestOp, and the six HTTP-method annotations) defaulting OFF at every level. Blank "" means inherit (op inherits resource, which resolves ${juneau.debugMarshalling:false}); query at runtime via RestRequest.isDebugMarshalling(). A serializer/parser configured with its own marshall-layer Context.debug remains independent — the REST layer never clobbers it.

See Logging / Debugging and the Ops / Introspection Mixin Pack topic pages.

Secure-by-default REST debug hardening (RestDebugFormatter)​

BasicRestDebugFormatter is hardened so that raising a resource/operation logger to FINE/FINEST can never, by itself, leak plaintext credentials or hand a remote client a log-forging channel. This is an unreleased-10.0 hard break in the debug formatter's output — there is no compatibility shim. Two reusable primitives land in juneau-rest-common (a sanitize(...) log-forging escaper and the RestDebugBodyScrubber SPI); the formatter changes land in juneau-rest-server.

  • No body dumps by default. Request/response bodies are never dumped unless an operator explicitly sets the JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES environment variable (environment-only, no system-property fallback, read once/cached, trim-then-parse truthy kill-switch semantics). With the gate unset, FINEST emits a suppression placeholder + byte count instead of body content. Even opted in, bodies render only at FINEST and only for renderable text content (via the new isBodyRenderable(String) predicate); non-renderable/binary content and non-identity Content-Encoding yield a distinct placeholder.
  • RestDebugBodyScrubber SPI (org.apache.juneau.http) — an optional, gate-subordinate, fail-closed transform (String scrub(String contentType, String body), registered via bodyScrubber(...)) that chooses scrubbed-vs-raw body text once dumping is permitted; a throw or null result falls closed to a placeholder, never the raw body.
  • All header values masked against a widened set. Every value of every credential-bearing header (not just the first) is masked with [REDACTED], using a widened formatter-local set (the shared RedactedHeaders.DEFAULT plus X-Auth-Token, X-Authorization, WWW-Authenticate, Referer, Location) matched with separator-normalized folding (so X-Auth-Token/X_Auth_Token/XAuthToken all match). RedactedHeaders.DEFAULT is unchanged. New additive addRedactedHeaders(...) knob alongside the replacing redactedHeaders(...).
  • Log-forging sanitization. The request method/URI (including the 404/unrouted path), header names/values, and any dumped body text are escaped (CR/LF, NEL, line/paragraph/bidi separators, C0/C1 controls → \r/\n/\uXXXX; TAB preserved), so a client cannot inject a newline to forge a second log line or a fake status banner.
  • Bounded output. New maxHeaders (100), maxHeaderScan (1000), maxFieldLength (1024), and maxUriLength (2048) knobs bound header and URI output; the existing bodyCap() remains the single body cap.
  • Finish-path containment. A formatter or scrubber that throws during request completion can no longer escape and fail a completed request — only a fixed diagnostic token is logged (never the secret, message, or stack).

See the Logging / Debugging topic page for the full model and the operator opt-in danger callout.

Always-on request correlation (X-Request-Id / requestId)​

Juneau 10.0 adds always-on, end-to-end request correlation on the REST server. A per-request requestId is resolved during session build — before any @RestStartCall hook — so it covers every code path that produces a debug record, including 404s and early-error paths, and gives the debug formatter's statusLine a synchronous RestRequest.getRequestId() to read.

  • Mint-or-honor. An inbound X-Request-Id request header is honored when present; otherwise a fresh time-ordered Uuid7 is minted. The resolved id is echoed on the X-Request-Id response header. Exactly one requestId LogContext scope is opened per request and closed after the debug record is emitted (emit-before-close), so every record on the request thread — the cumulative debug record and any handler-emitted records — carries the structured requestId field, and the rendered message is prefixed [requestId=<id>] . The id also rides the async-completion snapshot, so true-async completion records correlate identically.
  • RequestIdSettings bean (public write-surface, resolved by RestContext via a getBean → @Bean-factory → default ladder): validator(Predicate<String>) defaults to sanitize-and-accept (strip log-forging sequences, accept the bounded remainder — not reject-and-remint), plus attributeKey(String) and idSupplier(Supplier<String>).
  • RequestIdFilter reconciled to a thin façade. Its apply() is now idempotent with respect to the already-resolved session id (honor, never double-mint); its per-instance builder knobs are documented no-ops — request-id behavior is configured on RequestIdSettings.
  • Auth non-influence. X-Request-Id has zero influence on authentication/authorization outcomes; configuring an auth guard to read it is an anti-pattern (regression-tested).

See the Logging / Debugging topic page.

juneau-console-widgets / juneau-console-views​

Shared endpoint-safety predicate (juneau-commons)​

A new org.apache.juneau.commons.http.SafePathTemplate centralizes the same-origin/non-templated URL check and the MIN_POLL_INTERVAL_MS = 5000 poll floor. The views toolkit's region and polling definitions call it, and the client-side runtime re-implements the same rule as defense-in-depth.

AsyncJobRegistry de-duplicates jobs by IdempotencyKey​

AsyncJobRegistry.tryCreate(IdempotencyKey) and create(IdempotencyKey) register a job under the key's value; submitting again with a key that is already registered returns the existing job instead of creating a second one, so a double-click, an impatient re-submit or a browser retry collapses to one job.

  • Atomic. The lookup-or-create is atomic — concurrent submits with one key yield exactly one job.
  • Finished jobs replay. A key whose job has finished returns that terminal job and its result (the same replay-on-duplicate contract as IdempotencyKey) rather than triggering a second run. This holds until the job is swept after the 60-second retention window; after that the key is forgotten and the same key starts a fresh job.
  • Duplicates are free. A repeat request creates nothing, so it does not count against MAX_CONCURRENT_JOBS.

Async job streams resume from Last-Event-ID (BREAKING)​

Async job streams (/juneau-jobs/{jobId}/stream) now tag each progress event with an SSE id and resume from Last-Event-ID (or ?lastEventId=). A fresh attach no longer replays earlier progress. The job-log card asks for the full history itself.

  • Live-only default (BREAKING). A fresh attach with no resume value no longer replays buffered progress. This affects every EventSource(streamUrl) client, including third-party callers of the HTTP API. Add ?lastEventId=0 to get the whole history.
  • New lastEventId query parameter on GET /juneau-jobs/{jobId}/stream. A non-empty Last-Event-ID header beats it, so a browser's own reconnect resumes where it left off.
  • 400 for a bad or past-the-end resume value. The status checks run in the order 404, then 400, then 429.
  • An ended job still sends its result on every attach, fresh attaches included.
  • Job-log card: connects with ?lastEventId=0 and no longer de-duplicates lines by count.

See Async Jobs.

Datatable View Settings (juneau-console-views)​

The View Settings dialog is reworked so that saved choices actually take effect on load.

  • Applied on load. Stored View Settings (columns, search membership, sort, page size, wrap, density, auto-refresh) are restored when the page loads, not only when the dialog opens. This fixes a hidden column reappearing after a reload, in both client and server data modes.
  • Ordered Sort tab. Sort is a priority list of column and direction entries, and an untouched Apply keeps the view's defaultOrder instead of persisting an ascending sort over every column.
  • Auto-refresh. The Options tab gains an auto-refresh interval (Off, 30 s, 1 m, 5 m, 15 m), stored as autoRefreshMs.
  • Per-view tabs. columnConfig: {tabs: [...]} restricts the dialog to a subset of columns, search, sort and options.
  • Accessibility. Dialog tabs carry aria-selected/aria-controls with arrow-key navigation, every row control's label names its column, and a polite live region announces search results and a dismissed, unapplied search.
  • Copy link button beside the gear, shown for the primary (or only) view table.
  • Column search popover shows a per-type help line (the text of SearchType.bareHelp()).
  • BREAKING: the columnSearchToggle ribbon type is removed. The ribbon ignores an unknown type silently, so an adopter just loses the button; per-column search is the search icon in each header cell.
  • BREAKING: the saved View Settings schemaVersion is now 2, and NS.config.readViewSettings returns {settings, reset}. A saved blob at version 1, or with the old string-array sort, is discarded with a one-time notice; saved-view blobs at version 1 are rejected too. A stored blob is ignored for tables without columnConfig.
  • BREAKING: NS.config.defaultSortMembership is removed.
  • BREAKING: the Options blob gains autoRefreshMs (old blobs read as Off), and isPollSuspended now also returns true while the View Settings dialog is open, which pauses any existing pausePollingWhileEditing view.

See the new Datatable View Settings topic page.

Terminal-view region (juneau-console-views, juneau-terminal-report)​

New terminal-view region: a multi-step run summary driven by a small, versioned event stream, with <@card type="terminal-view">, TerminalViewMixin, and readers that turn test-report files into events.

  • New module: juneau-terminal-report (package org.apache.juneau.terminal.report) holds TerminalEvent, the grammar checks (TerminalViewChecks) and the report readers. It does not depend on the REST server, so a producer can use it alone; juneau-console-views depends on it.
  • Events: TerminalEvent factories for step, end, suite, test, replace, note and done (contractVersion "1"). The client reduces them to a failures-first summary, a step list, and per-suite strips of test blocks with tooltips; counts stay exact past the retention caps.
  • Sources: TerminalViewLog (in memory) and FileTerminalViewSource (a growing JSON Lines file, with 410 Gone after truncation). Implement TerminalViewSource for anything else.
  • Endpoint: TerminalViewMixin serves /juneau-terminal-view/{runId}/events; TerminalViewEndpoints gives the same logic for custom paths.
  • Report readers: ReportReaders.forKind(...) reads Surefire, JUnit XML, pytest, Jest JSON and Playwright JSON reports without throwing on bad input, and ReportResult.toEvents(step) feeds the region.
  • Re-run steps: steps named <id>.2, <id>.3 (titled (attempt N)) collapse under the latest attempt, behind an "N earlier attempts" toggle that starts collapsed.
  • Client: juneau-terminal-view.js joins the views toolkit pack and publishes JuneauViews.terminalView. poll: false regions take events through append, so any transport (such as SSE) works.
  • Errors: the region reports E-RV-1 to E-RV-9; the console shell adds E-JS-14. The console shell's unknown-card-type message now lists html|datatables|terminal-view.

See the new Terminal View Region topic page.

Terminal region (juneau-terminal-region)​

New terminal region: a command's raw output rendered by xterm.js, colours, redraws and all, with <@card type="terminal">, TerminalMixin, and a launcher that runs commands under a PTY.

  • New module: juneau-terminal-region (opt-in). It depends on juneau-console-views and the xterm.js WebJar org.webjars.npm:xterm__xterm 6.0.0.
  • Endpoints: TerminalMixin serves /juneau-terminal/{id}/bytes (raw bytes by offset, with Term-* headers) and /raw; TerminalEndpoints gives the same logic for custom paths.
  • Sources: FileTerminalSource (a growing log with a .size sidecar) and MemoryTerminalSource (a ring buffer).
  • Launcher: TerminalProcess runs a command under juneau_terminal.py --pty and serves its log and events.
  • juneau_terminal.py: new --pty, --size COLSxROWS and --events PATH options, plus --input stdin and --prompts stdio to pass input or the job's prompt channel through. Non-PTY runs are unchanged.
  • Prompts: TerminalProcess.start(..., TerminalPrompts) relays the job's prompt lines and sends answers back with answer(jsonLine); closing it cancels the job.
  • Terminal view: step, suite and test events gain an optional rawOffset; rawHref takes {offset} and onBlockClick gets offset. rawLine, {line} and info.line are gone. The contract version stays "1".
  • Console UI: the terminal card type and the xterm and terminal toolkit packs. terminal is now a reserved card type name. It replaces the console-output card.
  • xterm options: the client takes an allowlisted terminal object (convertEol, scrollback, fontFamily) in mount and on the terminal card; other keys are ignored. Server-built cards set it through the new TerminalDef.terminal(Map) builder method or the card body, and the server rejects unknown keys and bad values.

See the new Terminal Region topic page.

juneau-terminal (new module)​

New build tooling: juneau_terminal.py, a stdlib-only Python wrapper that runs Maven, pytest, Playwright, Gradle, npm and Bazel commands, parses their output into steps and test counts, and draws a board console view. Juneau's own test.py, push.py and release.py run on it. It is attached to org.apache.juneau:juneau-terminal as a py (the engine), a zip (bootstrap.py, the juneau_terminal_scripts toolkit, the pytest plugin and the Playwright reporter) and a py with classifier pytest. It is build tooling, so juneau-bom does not manage it.

  • Console view: --console full|board|none (or JUNEAU_TERMINAL_CONSOLE), one row per step with a progress bar, counts and a summary line, and JUNEAU_TERMINAL_DETAIL (summary, actionable, modules, all) for how much a finished step keeps. --full-log and --board-log write the raw output and the rows.
  • Run history: per-step timings and test counts under ~/.cache/juneau-terminal/history, shared by branches and worktrees of one repository, give percents, estimates and an Overall line (JUNEAU_TERMINAL_HISTORY, JUNEAU_TERMINAL_HISTORY_DIR, JUNEAU_TERMINAL_HISTORY_KEY).
  • Run-protocol markers: TERMINAL_MARKERS=1 prints ##terminal <json> events for a runner UI, and TERMINAL_ARTIFACTS keeps copies of the test reports.
  • Maven rows: one block per module. A single-project build (a one-module tree, or -pl without --also-make) draws one module and counts 1/1 on success. A build that stops before test (goals up to test-compile, or -DskipTests) still draws its module bar, counting modules as they compile, with no test totals.
  • Pytest plugin: -p juneau_terminal_pytest is added to pytest runs (opt out with JUNEAU_TERMINAL_PYTEST_PLUGIN=0), so the bar has one block per test file and shows the running file. Without the plugin, the bar keeps one block per test.
  • Playwright reporter: added through PW_TEST_REPORTER beside the config's reporters (opt out with JUNEAU_TERMINAL_PLAYWRIGHT_REPORTER=0). It gives one block per test file, counts a retried test once, writes <step>-playwright.json, and off a terminal sets PLAYWRIGHT_HTML_OPEN=never.
  • PTY mode: --pty, --size, --events, --input stdin and --prompts stdio; this is what TerminalProcess runs (see the Terminal region section above).
  • Cancelling: each tool runs in its own process group. Ctrl-C or SIGTERM sends SIGTERM to the group, then SIGKILL after a grace period, so forked JVMs and browsers don't outlive the run; the run ends as cancelled (exit 130).
  • Script toolkit: juneau_terminal_scripts gives push and release scripts git helpers, a session with exit codes, tool runners, resumable steps, locks, notifications and prompts, loaded through the vendored bootstrap.py.
  • Juneau build rows: test.py draws Compile, Java tests, Python tests and Browser tests (always shown, marked skipped with the reason when not run); push.py adds Prechecks, Tags, BOM, Timing, Install, Commit and Push.
  • Wrapping: every line of a wrapped bar holds the same number of cells as the first.
  • Expected tests: a running row shows the expected test total from previous runs, e.g. 41,200 / ~83,000 tests.
  • Steadier estimates: the estimated total time changes only when a new estimate differs by more than 15 seconds or 10%, whichever is larger, and is rounded to 5, 15 or 30 seconds by size. The overall ~X left figure behaves the same way.

See the new juneau-terminal topic page.

Console chart and job-log cards​

Two new console card types. Both load their script on demand through a toolkit pack, so pages that don't use them load nothing new.

  • chart card: line, bar and donut charts drawn as inline SVG by juneau-charts.js, from a src= endpoint or inline data, with optional refreshMs, click-through, a data-table view and an open kind registry (JuneauCharts.register). Built from Java with the new ChartDef builder.
  • job-log card: a form that starts an AsyncJobsMixin job, streams its progress lines, shows the outcome, offers Cancel, and reconnects after a reload. A dropped stream resumes after the last line shown, using the server's Last-Event-ID support (see "Async job streams resume from Last-Event-ID" above). Built from Java with CardSpec.of("job-log", id).
  • Reserved: job-log is now a reserved card type.

See the new Chart Cards and Job-Log Card topic pages.

Row jobs and the row-aware job-log card (juneau-console-views, juneau-console)​

A job can now be started for one table row, from the row or from its detail panel, with a key the server binds to that row.

  • JuneauViews.jobs: run (submit, stream from the start, settle, cancel, with onStart / onProgress / onResult / onEnd hooks and an optional paint: "none"), fetchKey, active, pending and cancel. It allows one job per row, takes the row id only from the row's stamp, and substitutes {id} only. "jobs" joins NS.HOOK_NAMESPACES.
  • helpers.jobButton: a button that runs a row job through JuneauViews.jobs.
  • JobKeyLedger and RowJobSubmit: server-side keys bound to an action, a row id and the caller, issued by a key endpoint, verified on submit, and spent once their job exists. A key for one row is refused for another with 409 write-guard:idempotency-binding-mismatch; a spent key replays its job or answers write-guard:idempotency-key-spent, so a replay never starts a second job.
  • Job-log region: the job-log card now runs inside a row-detail region (populate: 'job-log', built from Java with JobLogRegion). It gains submit.envelope / submit.action, hidden fields, source: 'row.<field>' presets and output, which writes a result value into the card or panel. A datatables card with a job-log region loads the joblog pack by itself, through the new CardTypeHandler.requiredPacks(JsonMap). window.JuneauJobLog.mount mounts one programmatically. JobLogChecks holds the validation both paths share. New errors E-JS-109 to E-JS-113. A panel re-attached after a collapse can cancel its job.
  • RegionDef.config: populator-owned configuration, emitted only when set and delivered to the populator as ctx.declared.config, including from a datatables card's detail.region.
  • A busy row refuses ordinary row actions: a row action activated on a row that already has a pending or running job (a JuneauViews.jobs run or another row action's job) is refused with row-job-running and sends nothing, instead of starting a second concurrent job for the row. It runs normally once the job ends.
  • Marker re-applied after redraws: a row's data-juneau-job marker, row-action jobs included, is put back on the new <tr> after every table draw, and cleared from whichever <tr> is drawn when the job ends.
  • Poll held while a panel runs a job: while an expanded detail panel hosts a running row job, the table's timer reload is held and the staleness pill shows Paused, whether or not the view sets pausePollingWhileEditing. User reloads, sorts, filters and page changes still draw.
  • Fix: in-panel elements resolve their parent row. hookContextOf, and with it NS.rowActions.contextOf, now resolves an element inside a detail panel to the panel's parent row and its data, instead of a child row with empty data. A row-detail region's visibleWhen therefore tests the parent row's data, as documented. An element in a nested table's own row still resolves that row.
  • Fix: console-card detail regions project lazy. A datatables card's detail.region now carries the same lazy value as the builder path (false unless the region is a tab body), so the two produce the same sidecar.
  • Petstore: the Jobs page gains a species table whose detail panel restocks that species, and a "Groom again" button on each groom run.

See the new Row Jobs topic page and Job-Log Card.

Console views cleanup and ribbon parity corpus (BREAKING)​

Three Java copies of logic that the views JS owns are removed, and the JS is the single implementation:

  • CalendarLayout removed. Calendar lane and chip layout is computed by juneau-calendar.js only. CalendarDef and the calendar card are unchanged.
  • RegionDef.serializeParams(...) / resolvedDataUrl() removed. Region data URLs are built client-side by juneau-regions.js.
  • Saved views removed. SavedViewsMixin, SavedViewStore, InMemorySavedViewStore and SavedViewQuotaException are gone, along with the server persistence provider in juneau-config.js (createServerProvider, data-juneau-saved-views). Column configuration persists to localStorage only.
  • Built-in operator help text moved to JS. Built-in SearchOperators no longer carry help on the wire. The column-search popover reads built-in help from JuneauViews.search.SearchOperators.help(name). Custom operators still supply their own help.

Ribbon behavior:

  • Client-mode tables now apply column-scoped ribbon options. Previously only server-mode tables did, so client-mode adopters needed their own bridge script.
  • Ribbon values are BeanQuery $-expressions ($eq(DROPPED), $in(Waiting,"Partially reviewed")), never regular expressions. A regex-looking value is passed to the search engine as written and matches nothing useful.
  • New default setting. Use default: true on an option, or default: '<memberId>' on an optionGroup, to choose the initial selection when nothing is persisted. A stored choice always wins. Nothing auto-selects without default.
  • opt is no longer comma-joined across ribbon contributions. The last contribution wins.
  • Parity corpus. ribbon-corpus.json (views test resources) is the contract between the JS encoder and the Java DataTables request decoder. RibbonCorpus_Test (Java) and RibbonCorpus_Parity_Test (JS) both bind to it. Any change to ribbon encoding must update the corpus in the same change.

Migration: replace regex ribbon values with the equivalent $eq/$in. Replace any page script that auto-applied a ribbon's first option with default.

Console views API gap fixes (juneau-console-views, juneau-console-freemarker)​

  • RibbonItem.id(String): a ribbon item, including a refresh() item, can now carry an id. The id is emitted on the item's ribbon entry and is rejected on divider and export items.
  • Ribbon optionGroup title: the group title is now rendered as role="group" with aria-label, so assistive technology announces it. No visible caption is drawn.
  • Render.meta(String, Object): the value may now be a Number or Boolean, which stays a JSON number or boolean (for example meta("places", 2)). It was String-only.
  • Column.href(...) implies render("linked"): a column with an href and no render now emits the linked renderer, in both Java (Column.toCatalogMap()) and FreeMarker (DatatablesCardType). An explicit render always wins. Migration: Column.toCatalogMap() used to throw IllegalArgumentException for an href without render("linked"). That throw is gone, so a test that expected it must change, and render("linked") may be dropped.
  • RowDetail region is optional: RowDetail.toMap() no longer requires a region. A detail with none is a header-only panel whose title and icon are filled from the endpoint's fields, and its entry carries no region key.
  • openConfirmOnlyDialog passes the row to a confirm renderer: a per-row action's confirmRenderer now receives rows (the one row), labelField and rowIdField, the same way the bulk path passes them, so it can name the row it guards. A ribbon-hosted dialog has no row, so rows is empty. Migration: a custom confirmRenderer that assumed rows was always empty or absent for a single-row action must handle a one-element rows.
  • Same-origin path checks (stricter): a card src must be a relative or root-relative same-origin path, so an absolute https://this-host/... src is now rejected at page build. Same-origin path checks now reject a backslash anywhere (including in the query, for example /search?q=C:\x), a leading or trailing space or control character, any C0 control character, and DEL. Badge src and href must be same-origin paths. Migration: change an absolute card src to a root-relative path.
  • PageSpec.card(...) runs the card type's handler (behaviour change): a builder-API card of an adopter (non-reserved) type is now built the way <@card> builds it. Its body map is the authored body, the type's registered CardTypeHandler validates it and returns the fragment the card carries, and an invalid body fails the render with the <@card> message. Markup passed to card(spec, markup) is the card's body, as the content of <@card> is: JSON5 markup is parsed by the handler and is not emitted as a <template>, its subscribes / publishes keys are wired onto the card (refused when the CardSpec sets them too), and markup is emitted as the card's template only when the handler captures it. As in <@card>, the body may not set a base key (id, type, title, src, template, ref), a card takes at most one of a body map, non-blank markup, template or src, and sanitize is refused before the handler runs. An unregistered type still passes its body through as-is. Previously such a card was seeded as written, with no validation, and its markup was always emitted as a template. Migration: a builder card that carried the handler's output (for example by calling toFragment itself) must carry the authored body instead.

juneau-rest-common​

HTTP Annotations Moved from juneau-marshall​

The HTTP parameter annotations (@Header, @Query, @Path, @Content, @FormData, @Request, @Response, @StatusCode, @PathRemainder, @HasQuery, @HasFormData, @Contact, @License, @Tag) along with the constant classes (CollectionFormatType, FormatType, ParameterType) and the httppart.bean package (RequestBeanMeta, RequestBeanPropertyMeta, ResponseBeanMeta, ResponseBeanPropertyMeta, MethodInfoUtils) have been moved from juneau-marshall to juneau-rest-common.

Package names are unchanged (org.apache.juneau.http.annotation, org.apache.juneau.httppart.bean), so import statements do not need updating. Maven resolves the classes from the new module location automatically. Existing compiled code will need recompilation.

Removed Features​

  • serializer() and parser() attributes removed from @Query, @Header, @FormData, @Path, @PathRemainder, @Request, @Response. Use the new @HttpPartMarshalling annotation (in juneau-marshall) to specify custom HttpPartSerializer/HttpPartParser classes.
  • @Repeatable removed from all HTTP annotations. Dynamic annotation stacking via on/onClass is no longer supported.
  • on() and onClass() attributes removed from all HTTP annotations.
  • @ContextApply removed from all HTTP annotations.
  • 14 XAnnotation companion classes deleted (ContentAnnotation, FormDataAnnotation, HasFormDataAnnotation, HasQueryAnnotation, HeaderAnnotation, PathAnnotation, PathRemainderAnnotation, QueryAnnotation, RequestAnnotation, StatusCodeAnnotation). ContactAnnotation, LicenseAnnotation, TagAnnotation, and ResponseAnnotation remain for Swagger generation utilities.
New @HttpPartMarshalling Annotation​

A new @HttpPartMarshalling annotation in juneau-marshall (org.apache.juneau.httppart) replaces the serializer() and parser() attributes that were removed from HTTP annotations. This decouples the HTTP annotations from HttpPartSerializer/HttpPartParser types.

@HttpPartMarshalling(serializer=UonSerializer.class, parser=UonParser.class)
@Query("name")
String name

RestSharedConstants​

  • RestSharedConstants — Home for static literals shared across REST modules (server, client, mock) so wire names and similar strings stay aligned. Serializer/parser session option wire names are HEADER_JuneauSerializerOptions, HEADER_JuneauParserOptions, QUERY_juneauSerializerOptions, and QUERY_juneauParserOptions. Add more public static final fields when other cross-module constants are needed. (This supersedes the earlier RestSessionOptionWire type.)

juneau-rest-server​

REST class family architecture — naming triad, RestMixin, op-mixins, flavor fill-in​

A coordinated, breaking restructuring of the @Rest resource family establishes one consistent architecture across servlets, child resources, and mixins. All changes below are hard breaks with no deprecation bridges (acceptable for a new major version); they supersede the pre-rename class names used elsewhere in these notes.

Naming triad — *Servlet / *Resource / *Mixin. Every reusable capability now follows a self-describing <Capability><Flavor> naming scheme, and the vestigial Basic prefix is dropped from capability classes (it is retained only on the convenience-bundle bases that have a barer sibling, e.g. BasicRestServlet):

  • RestObject → RestResource (the non-HttpServlet child-resource base), cascading to BasicRestObject → BasicRestResource and BasicRestObjectGroup → BasicRestResourceGroup.
  • The mixin pack drops Basic and standardizes on the Mixin suffix: BasicStaticFilesResource → StaticFilesMixin, BasicSwaggerResource → SwaggerMixin, BasicSwaggerUiResource → SwaggerUiMixin, BasicOpenApiResource → OpenApiMixin, BasicRedocResource → RedocMixin, BasicRouteIndexResource → RouteIndexMixin, BasicEchoResource → EchoMixin, BasicAdminResource → AdminMixin, BasicVersionResource → VersionMixin, BasicWellKnownResource → WellKnownMixin, BasicSeoResource → SeoMixin, BasicFaviconResource → FaviconMixin, and the four view mixins BasicJspResource/BasicMustacheResource/BasicThymeleafResource/BasicFreemarkerResource → JspMixin/MustacheMixin/ThymeleafMixin/FreemarkerMixin.
  • The servlet companions drop Basic too: BasicStaticFilesServlet → StaticFilesServlet, BasicAdminServlet → AdminServlet, BasicVersionServlet → VersionServlet, and the abstract BasicViewServlet → ViewServlet (with Basic*Servlet view companions → <Engine>Servlet).

New RestMixin base (opt-in). A third base class org.apache.juneau.rest.servlet.RestMixin joins RestServlet and RestResource to complete the triad. It is opt-in — a plain @Rest POJO remains fully valid as a mixin or child via annotation-only configuration; extending RestMixin is a convenience and does not change composition semantics. Beyond the fluent builder surface it shares with RestServlet / RestResource (see the builder-API entry below), RestMixin adds getHostContext(), which returns the host (mixed-into) resource's RestContext — backed by the mixin sub-context's parent linkage, so it is usable at config time and does not depend on an in-flight request — letting a mixin op introspect the host (e.g. enumerate the host's child resources for a navigation page) rather than its own childless sub-context. It returns null when a RestMixin subclass is instantiated standalone rather than composed via @Rest(mixins=...).

Op-interfaces deleted; replaced by single-responsibility op-mixins. BasicRestOperations and BasicGroupOperations are removed. Their endpoints are now true @Rest(mixins=...) mixins — ErrorMixin (/error), HtdocMixin (/htdoc), StatsMixin (/stats), and FaviconMixin (/favicon.ico) — added to the @Rest(mixins=...) lists of BasicRestServlet / BasicRestResource and the Spring BasicSpringRestServlet. The host-wide nav-links / JSON-schema configuration that lived on BasicRestOperations is re-homed as class-level @HtmlDocConfig / @JsonSchemaConfig on BasicRestServlet; the per-method @HtmlDocConfig page decoration moves onto StatsMixin.getStats (OQ-8). getChildren stays a concrete method on the group bases (BasicRestServletGroup / BasicRestResourceGroup / BasicSpringRestServletGroup) so the host's main navigation page retains its @HtmlDocConfig chrome (a mixin op runs in its own sub-context and would lose the host's page decoration).

Favicon reconciliation. The legacy getFavIcon op (which served REST/favicon → images/juneau.png through static files) is dropped in favor of a FaviconProvider-backed flavor set: the flavor-neutral FaviconProvider worker bean holds the icon bytes + Cache-Control value and the serve logic, and the independent FaviconMixin (mixin) / FaviconResource (child) / FaviconServlet (servlet) flavors each hold a FaviconProvider and delegate to it. The default serves a classpath ICO (/juneau-favicon.ico) with a 30-day Cache-Control (max-age=2592000, public). This is a behavior change for both the default favicon bytes and the configuration key.

Per-capability flavor fill-in (flavor-neutral worker beans). Capabilities where standalone or routed-child deployment is a real use case gain their missing flavors as independent classes that each delegate to a flavor-neutral worker bean — never to another flavor — so the forms cannot drift. The new worker beans are FaviconProvider / VersionProvider (in rest.convention), AdminProvider (in rest.ops), HealthAggregator (in rest.health), and the four view-engine raw-template dispatchers JspDispatcher / MustacheDispatcher / ThymeleafDispatcher / FreemarkerDispatcher; the static-files flavors resolve the existing StaticFiles worker bean from the request context (RestContext.getStaticFiles()) instead of holding a mixin instance. New child-resource flavors: StaticFilesResource, VersionResource, AdminResource, HealthResource, plus a ViewResource abstract base and per-engine JspResource / MustacheResource / ThymeleafResource / FreemarkerResource; new favicon flavors FaviconServlet and FaviconResource. HealthServlet / HealthMixin / HealthResource all delegate to a shared HealthAggregator (OQ-5 split of the former HealthServlet). All other capabilities (Swagger, SwaggerUi, OpenApi, Redoc, RouteIndex, Error, Stats, Htdoc, WellKnown, Seo) remain mixin-only.

RestRequest.getStaticFiles() removed. The request-level forwarder is deleted; obtain the active StaticFiles bean via RestContext.getStaticFiles() (e.g. req.getContext().getStaticFiles()) or bean-store injection. FileVar, Widget, and the static-files flavors are repointed accordingly. RestContext.getStaticFiles() is unchanged.

Route-index endpoint now content-negotiated. RouteIndexMixin (/options) returns a RouteDescriptions POJO instead of writing JSON directly, so the listing is now content-negotiated through the host's configured serializers: Accept: text/html renders a browsable table with clickable route links, while application/json / text/xml clients receive the same entries in their requested format (mirroring NavigationMixin.getChildren(...)). Because it now relies on the host's serializers, the host must have serializers configured (e.g. via BasicUniversalConfig or a Basic* servlet base) — a bare @Rest host with no serializers can no longer content-negotiate the listing, whereas the former JSON-only op did not require them. The historical /routes alias remains reachable via -Djuneau.routeindex.path=routes.

Fluent programmatic builder API — RestBuilder / AbstractRestBuilder self-type CRTP​

A public, fluent builder surface lets a @Rest resource, child resource, or mixin be configured programmatically — the primary use case being instantiating a configured resource bean in a Spring @Bean method — instead of (or layered over) the @Rest annotation. Builder-supplied values take precedence over the class's own @Rest annotation members.

RestBuilder<SELF> config surface + AbstractRestBuilder<R, SELF> base (full self-type CRTP). The public interface org.apache.juneau.rest.RestBuilder<SELF extends RestBuilder<SELF>> declares one fluent setter per @Rest member, plus the programmatic-only mdcAsyncPropagation(boolean) knob and a set(String, Object) forward-compat escape hatch. The shared abstract base AbstractRestBuilder<R, SELF extends AbstractRestBuilder<R, SELF>> implements every setter once — recording into a backing RestAnnotation.Builder override bag and returning self() — and leaves the SELF self-type open so a builder subclass's bespoke setters chain with true covariant returns (Option B / CRTP, aligned with the project-wide SELF / self() convention).

Per-flavor builders with a concrete DefaultBuilder<R> leaf. Each base class exposes its own self-typed flavor builder — RestServlet.Builder<R, SELF>, RestResource.Builder<R, SELF>, and RestMixin.Builder<R, SELF> — together with a concrete DefaultBuilder<R> leaf. The static factory RestServlet.builder(MyRest.class) (and the RestResource / RestMixin equivalents) returns the DefaultBuilder<R>, so the common, non-subclassed path never has to spell two type parameters:

@Bean
public MyRest myRest() {
return RestServlet.builder(MyRest.class)
.path("/foo")
.allowedHeaderParams("foo")
.build();
}

A resource may also add a one-line static Builder<MyRest> builder() { return RestServlet.builder(MyRest.class); } sugar so callers can write MyRest.builder().path("/foo")....build().

Constructor trio + builder-beats-annotation precedence. Every family base provides the constructor trio: a no-arg constructor, a Foo(RestBuilder) builder-injection constructor, and a Foo.Builder. build() stashes the builder onto the produced instance (a package-private restBuilder field read non-reflectively via getRestBuilder()). At RestContext construction the stashed builder's override bag is turned into a synthetic @Rest annotation prepended as the most-derived entry of the resource's annotation chain, so builder-set values win over the class's own @Rest members (rung 1 of the existing override-resolution chain). Collection-valued members replace the annotation value rather than merging.

Mirror-and-forward builders for worker-backed capabilities (OQ-11). For a multi-flavor capability whose logic lives in a flavor-neutral worker bean, the flavor builder mirrors the worker bean's builder methods on its own surface and forwards into the worker's builder, while inheriting the full RestBuilder surface from RestMixin.Builder. For example FaviconMixin.create() mirrors bytes / classpath / cacheControl into FaviconProvider.Builder and inherits path / roleGuard / etc.; VersionMixin mirrors VersionProvider the same way. This is how the three flavors share capability config without a shared flavor-builder base class (Java single inheritance forbids one — each flavor builder already extends its REST builder base):

@Bean
FaviconMixin favicon() {
return FaviconMixin.create()
.classpath("/myapp/icon.ico") // mirrored → FaviconProvider.Builder
.path("/branding") // inherited RestBuilder surface
.build();
}

Bean-instantiation preference (internal). So the new generic builder(Class) factories don't hijack framework default instantiation, BeanInstantiator now prefers a builder that builds the exact requested type (or a usable direct constructor) over one that only promises a supertype. A flavor's own create() / nested Builder therefore wins over the inherited generic builder(Class) (whose build() erases to a base class), and a plain resource/mixin carrying only the inherited builder falls through to its no-arg constructor.

Opt-in lazy @Rest(children=...) materialization​

Parent resources annotated with @Rest(lazyChildren=true) now defer the construction of each child RestContext until the first HTTP request routed to that child. Default behavior (eager at startup) is unchanged for all existing code.

  • @Rest(lazyChildren="true") on the parent resource opts all declared @Rest(children=...) into deferred (first-invocation) construction. The value is a String member with full SVL variable support (e.g. @Rest(lazyChildren="$E{LAZY_CHILDREN,false}")).
  • RestContext.Builder.lazyChildInit(boolean) is the host-wide programmatic knob with the highest resolution priority (overrides both annotation and ${RestContext.lazyChildren:false} env default).
  • Shell-context routing: At parent startup the route table is fully populated — each lazy child contributes its path prefix (resolved from @Rest(path=...) without instantiating a full context) to the parent's RestChildren matcher list. URL matching works immediately; the full RestContext is built only on the first matching request.
  • Concurrency safety: Exactly one materialization per child even under concurrent first-requests. RestChildren.LazyChildEntry.materialize() uses a double-checked lock on a per-entry Object lock with a volatile RestContext materialized field. Two simultaneous first-requests serialize on the lock; the second thread reads the already-built context without re-constructing. A materialization that throws leaves the materialized field null — the next request retries.
  • Observability: Materialization is logged at INFO level: Lazy REST child materializing: com.example.ChildResource at path '/child' / materialized.
  • Lifecycle: RestContext.destroy() (via RestChildren.destroy()) only calls the child's shutdown hooks if the child was actually materialized. Never-invoked children are silently skipped.
  • Interaction with @Rest(observability="true"): Startup-fail is also deferred — the observability-backend wiring check fires when the lazy child is first materialized, not at parent startup.
@Rest(path="/api", children={AdminResource.class, MonitorResource.class}, lazyChildren="true")
public class ApiGroupResource extends BasicRestServletGroup {
// AdminResource and MonitorResource are built only on first request
}

Outgoing-response W3C trace-context headers​

When a non-no-op TracerHook is active on a request, the server now writes the W3C traceparent — and, when present, the tracestate — header back onto the HTTP response, so a client calling the server can read the resulting trace id off the response and correlate against it. The emitted traceparent reflects the server-started span's context (a freshly-minted span id parented to the inbound trace), not the inbound header verbatim. tracestate is written only when the active trace state is non-empty — an empty trace state never produces an empty header.

  • org.apache.juneau.rest.server.tracing.TraceContextResponseProcessor (new) — a ResponseProcessor prepended to the default ResponseProcessorList. It reads the rendered traceparent / tracestate strings that a TracerHook bridge stashed as request attributes (juneau.traceparent / juneau.tracestate) and writes them as response headers. Its first action is a single request-attribute read that short-circuits with zero allocations when no tracer ran, preserving the off-by-default observability contract on the no-tracer path. If the response is already committed (e.g. a streaming / SSE handler that flushed headers early) it logs a FINE message and skips the write — it never throws. The processor carries no OpenTelemetry dependency, so core juneau-rest-server stays OTel-agnostic; it works with any TracerHook bridge that populates the two attributes.
  • RestContext.responseTraceparent (new env-driven default, ${RestContext.responseTraceparent:true}) — controls whether TraceContextResponseProcessor is registered. Defaults to true (on-when-tracer); set it to false to keep the processor out of the chain entirely even when a tracer is active. Exposed via RestContext.isResponseTraceparent(). A per-resource @Rest(observability="false") opt-out is now available vian earlier change — see below.

The companion OTel bridge change lives in juneau-rest-server-tracing-otel: OtelTracerHook.startSpan(...) now renders the W3C header values from the server-started span context at span-start time (the only point where it is reliably active) and stashes them as the request attributes the processor reads. See REST Server — Observability for the full topic.

Per-resource / per-method observability opt-in attributes​

@Rest, @RestOp, and all HTTP verb annotations (@RestGet, @RestPost, @RestPut, @RestDelete, @RestPatch, @RestOptions) now carry three new String members for observability control:

  • observability (default "") — tri-state opt-in attribute with SVL variable support:

    • "" (empty, default) — inherit from parent / preserve today's behavior (observability fires when a backend @Bean is wired, is silent otherwise).
    • "true" — strict mode: the RestContext / RestOpContext constructor throws InternalServerError at startup if no non-NoOp MetricsRecorder or TracerHook @Bean is wired on the resource. Use this to make a missing-backend wiring mistake a hard deployment error rather than a silent no-op.
    • "false" — explicit opt-out: the observability block in RestOpInvoker is short-circuited for that resource or op, even when a backend is wired. Use this on health-probe and admin endpoints that should not contribute to metrics cardinality.
    • Verb-level setting overrides the resource-level setting symmetrically (can both enable and disable).
    • SVL variables (e.g. @Rest(observability="$S{my.env.obs,}")) are resolved at RestContext build time, so the startup-fail check fires correctly against the environment's resolved value.
  • metricName (default "") — per-op timer / metric name override. When non-empty, passed to MetricsRecorder.record(...) as the metricName parameter; MicrometerMetricsRecorder uses it as the Micrometer Timer name instead of the default http.server.requests.

  • metricTags (default "") — per-op additional metric tags in comma-separated key=value format (e.g. "team=payments,region=us-east"). Passed to MetricsRecorder.record(...) as the metricTags parameter; MicrometerMetricsRecorder adds these as extra Timer tags alongside the standard method / uri / status / exception tags.

Example:

@Rest(path="/api", observability="true")  // startup-fail if no @Bean MetricsRecorder / TracerHook
public class ApiResource extends RestServlet {

@RestGet("/orders/{id}")
@RestGet(path="/orders/{id}", metricName="orders.lookup", metricTags="team=payments")
public Order getOrder(@Path String id) { ... }

@RestGet(path="/health", observability="false") // health probe: never metered
public HealthStatus health() { return HealthStatus.OK; }
}

MetricsRecorder.record(...) signature change (migration required if you implement MetricsRecorder): The method now takes two additional trailing parameters — String metricName and String metricTags — after the existing Throwable error parameter. Update any custom MetricsRecorder implementations to add these two parameters (they may be safely ignored if not needed). The NoOpMetricsRecorder and MicrometerMetricsRecorder bundled with Juneau have been updated.

Interaction with TraceContextResponseProcessor: When observability="false" is effective for an op, RestOpInvoker skips the TracerHook.startSpan(...) call. Since OtelTracerHook stashes the juneau.traceparent / juneau.tracestate request attributes inside startSpan(...), those attributes are never set, and TraceContextResponseProcessor naturally emits no trace-context response headers for that op — no additional wiring is needed.

View-returning @RestOp methods now reach their renderer automatically​

Response processors implementing the new ViewRenderer marker interface are automatically positioned before catch-all processors (CatchAllResponseProcessor) in the ResponseProcessorList. The four built-in renderers (JspViewRenderer, ThymeleafViewRenderer, MustacheViewRenderer, FreemarkerViewRenderer) implement ViewRenderer. Third-party renderers gain the same ordering guarantee by implementing the interface.

Previously, SerializedPojoProcessor (the catch-all) ran before mixin-registered view renderers, causing @RestOp methods returning JspView, ThymeleafView, MustacheView, or FreemarkerView to emit Juneau-bean HTML instead of invoking the template engine.

Before (workaround required):

// User had to list JspViewRenderer explicitly ahead of SerializedPojoProcessor.
@Rest(responseProcessors={JspViewRenderer.class, SerializedPojoProcessor.class, ...})

After (zero config, automatic):

@Rest(path="/app", mixins=JspMixin.class)
public class AppResource extends RestServlet {
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return JspView.of("hello.jsp").attr("name", name);
}
}
// GET /app/hello/Bob → rendered hello.jsp (not a bean-HTML dump of JspView)

This is the foundational fix that unblocks the typed-handler View-return path for all four view modules — the per-engine typed-handler integration test matrices landed alongside this change (see the juneau-rest-server-view-* entries below).

Per-RestContext @Value resolution against @Rest(config=...) Configs​

  • RestContext's constructor now installs a PropertySource named "rest.config" on the resource's BeanStore — not on the process-wide Settings.get() singleton. The source wraps the resource's @Rest(config=...) Config(s) and walks them in child-to-parent order so the child's keys win on collision and the parent's keys fill the gaps.
  • @Value("${section/key}") injection sites on the resource bean (and on request-scoped beans whose BeanStore parent-walks to the resource's) consult the scoped PropertySource between the Settings local/global override stores and the global Settings sources chain. Resolution order for these sites is now:
    1. Settings.setLocal(...) / Settings.setGlobal(...) overrides (still win — test override semantics are preserved).
    2. The resource's @Rest(config=...) Config(s).
    3. The global Settings sources list (system properties, env vars, classpath ConfigPropertySourceProvider, microservice Config, Spring Environment, …).
    4. The @Value("${key:default}") literal default branch.
  • The BeanStore-scoped registration means MockRestClient's static RestContext cache (and any other long-lived cache) cannot leak ConfigPropertySources into the global Settings singleton — the 6.7× full-suite regression seen during an earlier prototype is structurally impossible. A new Settings_NoLeakedSources_Test (200× MockRestClient.build cycle) asserts the source count stays flat across the suite.
  • @Value Supplier<String> field types compose with the scoped chain: the BeanStore reference is captured (by-reference, not snapshotted) by the supplier so every .get() re-evaluates against the current state of the resource's PropertySources. Hot-reload of the underlying Config (or Settings.set(...) of an override key) takes effect on the next .get() without rebuilding the resource bean.
  • Cross-link: @Value Annotation Basics — Per-resource @Rest(config=...) integration.

Test-time Bean Injection​

A new JUnit 5 extension and @TestBean annotation enable Spring-style test-time bean substitution for MockRestClient, Microservice, SerializerSet, ParserSet, and EncoderSet — without CGLIB, AOT bytecode generation, or any Mockito dependency. See REST Server — Test-time Bean Injection for the full reference.

  • New in juneau-bct (org.apache.juneau.bct.junit): JuneauBeanStoreExtension, @TestBean(name, type, scope, mode), Scope, Mode, and TestBeanStore (a fluent BasicBeanStore overlay-builder).
  • New BeanStoreOverridable<B> marker interface in juneau-commons plus overridingBeanStore(BeanStore) setters on MockRestClient.Builder, Microservice.Builder, SerializerSet.Builder, ParserSet.Builder, and EncoderSet.Builder. The overlay is installed in the overridingParent slot of the builder's bean store, putting overrides at tier 1 of the resolution chain.
  • New Args.overridingParent slot on RestContext.Args for threading the overlay into the bean store the RestContext constructs internally.
  • Two wiring modes:
    • Mode INJECT (default) — construction-time wiring with overrides. The overlay is layered into the SUT's bean store at construction time. Universal — every bean type is eligible for replacement.
    • Mode OVERLAY (opt-in via @TestBean(mode = Mode.OVERLAY)) — push/pop overlays on a live SUT. The extension is attach(...)-ed to a long-lived SUT's bean store; per-test overlays are push'd at @BeforeEach / @BeforeAll and pop'd at @AfterEach / @AfterAll. Backed by new WritableBeanStore.pushOverlay(BeanStore) / popOverlay(Snapshot) primitives.
  • Per-test and per-class scopes via @TestBean(scope = METHOD) (default) and @TestBean(scope = CLASS) (must be on static members). Method-scope overlays chain on top of class-scope overlays.
  • Named-bean qualifier via @TestBean(name = "...") matching the framework's existing @Bean(name = "...") parameter resolution.

AuthN Guards — Bearer / API-Key / @Auth Principal​

juneau-rest-server now ships two opt-in AuthN guards plus an @Auth Principal argument resolver. The core jar stays JWT-free — a separate juneau-rest-server-auth-jwt sub-module (see below) carries the nimbus-jose-jwt integration. See REST Server — AuthN Guards for the full reference.

  • org.apache.juneau.rest.auth.BearerTokenGuard — RFC 6750 bearer-token guard with a fluent builder: realm(String) (default: "api"), validator(TokenValidator) (required). Stashes the resolved Principal on RequestAttributes under RestServerConstants.PRINCIPAL_ATTR. Throws 401 with WWW-Authenticate: Bearer realm="<realm>" on missing / malformed / rejected tokens — preserves richer challenges supplied by the validator.
  • org.apache.juneau.rest.auth.ApiKeyGuard — opaque API-key guard with a fluent builder: store(ApiKeyStore) (required), fromHeader(String) (default: X-API-Key), fromQuery(String), fromCookie(String), realm(String) (default: "api"). Falls back to manual Cookie header parsing when the underlying container (e.g. MockRestRequest) doesn't populate getCookies().
  • org.apache.juneau.rest.auth.TokenValidator SPI — single-method functional interface (Principal validate(String token) throws AuthenticationException) for plugging in opaque-token, JWT, or custom validators.
  • org.apache.juneau.rest.auth.ApiKeyStore SPI — single-method functional interface (Optional<Principal> lookup(String key)) for plugging in in-memory, database, or Vault-backed key stores.
  • @org.apache.juneau.rest.auth.Auth annotation — marks an @RestOp-method parameter for principal injection. The new AuthArg RestOpArg resolver also supports type-driven resolution: a bare Principal (or ClaimsPrincipal) parameter is injected with no annotation needed.
  • org.apache.juneau.rest.auth.ClaimsPrincipal — Principal subclass with typed claim access (getClaim(String, Class<T>), getClaims()). Used by JWT validators; lives in core so the API surface is reachable without the JWT module.
  • org.apache.juneau.rest.auth.AuthenticationException — Unauthorized (401) subclass with fluent wwwAuthenticate(String) setter (RFC 7235 §4.1).
  • RestServerConstants additions: PRINCIPAL_ATTR (request-attribute key for the stashed principal), API_KEY_HEADER (default header name "X-API-Key").
  • AuthArg registered in DefaultConfig.restOpArgs so type-driven Principal injection works out of the box.
  • Composition with AdminMixin. Swapping the mixin's default DenyAllGuard for a BearerTokenGuard is a zero-mixin-source-change migration — a single @Bean RestGuardList adminGuards(...) on the host wins via the existing guard-override seam (see BasicAdminResource_AuthIntegration_Test).

Auth Filter Framework — Servlet-Layer AuthN​

juneau-rest-server now ships a pluggable servlet-filter authentication framework in the new org.apache.juneau.rest.auth package. Filters run at the container layer — before RestServlet routing — and expose the resolved identity via standard HttpServletRequest overrides so downstream RoleBasedRestGuard and @Auth Principal injection work with zero changes. See AuthN Filter Framework for the full reference.

  • org.apache.juneau.rest.auth.AuthFilter — abstract base class implementing jakarta.servlet.Filter. Concrete subclasses implement Optional<AuthResult> authenticate(HttpServletRequest req) throws AuthenticationException. Three states: Optional.empty() = no credentials for this filter (pass through), throw AuthenticationException = invalid credentials (abort with 401), Optional.of(AuthResult) = success.
  • org.apache.juneau.rest.auth.AuthResult — immutable value type holding the resolved Principal and an unmodifiable Set<String> of roles. Factory methods: AuthResult.of(Principal, String...) and AuthResult.of(Principal, Set<String>).
  • org.apache.juneau.rest.auth.AuthenticatedRequestWrapper (public) — HttpServletRequestWrapper that overrides getUserPrincipal(), isUserInRole(String), getRemoteUser(), and getAttribute(PRINCIPAL_ATTR) to reflect the filter-resolved identity. Roles are the union of all successful filters in chain mode.
  • org.apache.juneau.rest.auth.AuthFilterChain — jakarta.servlet.Filter orchestrator. Each entry carries an AuthFilter and an optional UrlPathMatcher pattern. doFilter(...) semantics: select matching filters, iterate in declaration order, first-success principal wins, all-success roles aggregate, all-failure exceptions aggregate into a single 401 with combined WWW-Authenticate. If no filter matches the path, or all matching filters return Optional.empty(), the request passes through unchanged.
    • Builder: AuthFilterChain.create(BeanStore).append(AuthFilter).append(AuthFilter, String pattern).build().
  • org.apache.juneau.rest.auth.BearerTokenAuthFilter — concrete AuthFilter that extracts Authorization: Bearer <token>, delegates to a TokenValidator, and extracts roles from a ClaimsPrincipal claim (default: "roles"). Builder: BearerTokenAuthFilter.create().validator(TokenValidator).realm(String).rolesClaim(String).build().
  • org.apache.juneau.rest.auth.ApiKeyAuthFilter — concrete AuthFilter that reads an API key from a header / query param / cookie (configurable; default: X-API-Key header), delegates to an ApiKeyStore, and extracts roles from a ClaimsPrincipal claim. Builder: ApiKeyAuthFilter.create().store(ApiKeyStore).fromHeader(String).fromQuery(String).fromCookie(String).realm(String).rolesClaim(String).build().
  • @Bean AuthFilterChain auto-mount — JettyServerComponent.onStart(...) scans the BeanStore for an AuthFilterChain bean and registers it at /* before any servlet is mounted. No explicit addFilter(...) call required for the common case.
  • Spring Boot note — Spring Security's SecurityFilterChain is the recommended Spring path. BearerTokenAuthFilter / ApiKeyAuthFilter can be registered via FilterRegistrationBean if desired; the @Bean auto-mount is Jetty-only.

Rate-Limit Guard + Request-Id Filter​

juneau-rest-server now ships two opt-in operational primitives — both purely additive, both wired through existing @Bean / @RestStartCall extension points. See REST Server — Rate-Limiting and Request-Id Propagation for the full reference.

  • org.apache.juneau.rest.server.guard.RateLimitGuard — token-bucket RestGuard with a fluent builder: permitsPerSecond(int) / permitsPerMinute(int) / permitsPerHour(int), burst(int), keyBy(Function<RestRequest,String>) (default: client IP from req.getRemoteAddr()), xForwardedForAware(boolean) (default: false — only enable behind a trusted proxy that re-writes the header), exemptPaths(String...) (default: /healthz, /readyz, /livez to match HealthMixin), whenLimitExceeded(BiConsumer<RestRequest,RateLimitInfo>) (logging / metrics hook), storage(Storage) (default: in-memory with a 100k key cap and LRU eviction).

  • Rejection shape. 429 Too Many Requests with Retry-After in seconds (Math.max(1, secondsUntilNextToken)).

  • Advisory headers. Every passing request gets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (documented as advisory — no IETF standard yet).

  • RateLimitGuard.Storage SPI. Pluggable backend for distributed deployments; v1 ships only the in-memory default. The token-bucket math uses System.nanoTime() deltas so it is immune to wall-clock drift.

  • org.apache.juneau.rest.server.filter.RequestIdFilter — X-Request-Id mint / honor / echo filter: idSupplier(Supplier<String>) (default: UUID.randomUUID().toString()), validator(Predicate<String>) (default: ^[A-Za-z0-9-_]{1,128}$), attributeKey(String) (default: RestServerConstants.REQUEST_ID).

  • Mint / honor / reject contract. Absent → mint; present + valid → honor; present + invalid → discard + mint fresh. The minted/honored id is stashed on the request attributes and echoed on the response.

  • Re-entrancy safe. A second invocation on the same request short-circuits on the existing attribute and re-echoes the same id.

  • New constant. RestServerConstants.REQUEST_ID ("requestId") — the canonical request-attribute key used by RequestIdFilter.

Observability — Micrometer + OpenTelemetry hooks​

Metrics and tracing are opt-in and disabled by default. Same non-negotiable contract as Bean Validation: a fresh RestContext built with no MetricsRecorder / TracerHook bean registered never records a sample, never opens a span, and never adds a per-request cost — even when io.micrometer:micrometer-core and io.opentelemetry:opentelemetry-api are on the classpath. Opt in by registering a @Bean MetricsRecorder / @Bean TracerHook on the resource.

New SPIs in juneau-rest-server​

  • org.apache.juneau.rest.metrics.MetricsRecorder — interface with a single method record(String opName, String httpMethod, String uriTemplate, int statusCode, Duration elapsed, Throwable error, String metricName, String metricTags). Called once per @RestOp invocation (happy path and exception path). The uriTemplate is the @RestOp path template (/users/{id}) — not the raw concrete URI — so tag cardinality stays bounded. The metricName and metricTags parameters (added in an earlier change) carry the per-op overrides from @RestOp(metricName=...) / @RestOp(metricTags=...); both are "" when not set. If you implement MetricsRecorder directly, add these two trailing parameters to your record(...) override.
  • org.apache.juneau.rest.metrics.NoOpMetricsRecorder — process-wide singleton INSTANCE; resolved when no bean is registered. Drops events on the floor with zero allocations.
  • org.apache.juneau.rest.server.tracing.TracerHook — @FunctionalInterface with Scope startSpan(RestRequest request). The framework opens the scope before parameter binding and always closes it in a finally block; the resolved HTTP status and any thrown throwable are delivered via Scope.setStatusCode(int) / Scope.setError(Throwable) before Scope.close().
  • org.apache.juneau.rest.server.tracing.Scope — AutoCloseable with setStatusCode(int), setError(Throwable), and close(). The framework guarantees close() runs exactly once per call (including parameter-resolution failures and 5xx paths).
  • org.apache.juneau.rest.server.tracing.NoOpTracerHook — process-wide singleton INSTANCE (and NoOpTracerHook.NoOpScope.INSTANCE); resolved when no bean is registered.
Where the hooks fire​

The hooks wrap the @RestOp handler invocation in RestOpInvoker.invokeOp(...) — they cover parameter binding, the user method body, and the framework's standard exception-mapping path. Pre/post-call lifecycle methods (@RestPreCall / @RestPostCall) are intentionally outside the observability boundary so the metric / span describes the user-facing operation only. Resolution goes through the RestContext bean store: the first MetricsRecorder / TracerHook bean wins; absent any registration, the NoOp singletons are returned and the path short-circuits.

Wiring​
@Rest
public class MyResource extends BasicRestServlet {

@Bean
public MetricsRecorder metrics(MeterRegistry r) {
return new MicrometerMetricsRecorder(r); // see juneau-rest-server-metrics-micrometer below
}

@Bean
public TracerHook tracer(OpenTelemetry otel) {
return new OtelTracerHook(otel); // see juneau-rest-server-tracing-otel below
}

@RestGet("/users/{id}") public User get(@Path String id) { ... }
}

See REST Server — Observability (Micrometer + OpenTelemetry) for the full topic.

Async returns + virtual-thread dispatch​

@RestOp handler methods may now return CompletableFuture<T> (or any CompletionStage<T>); the framework unwraps the value through the standard response-processor chain when the future resolves. A separate, opt-in @Rest(virtualThreads=true) flag dispatches handler invocation onto Java 21+ virtual threads with graceful degradation on Java 17. The two features compose: a CompletableFuture return on a virtualThreads=true resource gives you the high-throughput "no-thread-blocked" pattern without changing the rest of the request pipeline.

CompletableFuture<T> / CompletionStage<T> return support​

  • New org.apache.juneau.rest.server.processor.AsyncResponseProcessor slotted ahead of SerializedPojoProcessor in the default ResponseProcessorList. Detects CompletionStage<T> return values, calls req.startAsync() to obtain an AsyncContext, and registers a whenComplete((value, error) -> ...) callback that re-feeds the unwrapped value (or throwable) through the rest of the response-processor chain when the future resolves.
  • Failed-future propagation: the throwable routes through the existing exception pipeline (ThrowableProcessor / ProblemDetailsProcessor); RFC 7807 / 9457 problem-details rendering works unchanged for failed futures when @Rest(problemDetails="true") is set.
  • Bare Future<T> is rejected with HTTP 500 and a clear message: "Bare java.util.concurrent.Future is not supported as a return type from @RestOp methods. Return CompletableFuture or CompletionStage instead, or block on the result yourself before returning." (Resolved OQA #4 — silently blocking on Future.get() would be a footgun.)
  • Synchronous-fallback path for unit-test mock environments: when the underlying servlet container does not support async dispatch (e.g. MockRestRequest), the processor falls back to a bounded CompletableFuture.get(timeout) and re-feeds the unwrapped value through the chain on the calling thread. Production servlet containers (Jetty, Tomcat, Undertow) take the true async path.
  • AsyncContext.complete() runs exactly once behind an AtomicBoolean state machine, even when both the timeout listener and the future-resolution callback fire concurrently.
Async timeout​
  • New @Rest(asyncTimeoutMillis) and @RestOp(asyncTimeoutMillis) attributes. Default is 30 seconds (resolved OQA #2 — matches typical proxy timeouts). On timeout, the response is 504 Gateway Timeout and the AsyncContext is aborted.
  • 0 disables the timeout for that scope. Negative values are treated as "use the default 30s". SVL is supported (asyncTimeoutMillis = "${TIMEOUT_MS:30000}").
Virtual-thread per-request dispatch (Java 21+, opt-in, off by default)​

Same non-negotiable off-by-default contract as Bean Validation and Observability. A fresh RestContext built with no virtualThreads attribute set never instantiates a virtual-thread executor, even on Java 21+.

  • New @Rest(virtualThreads) and @RestOp(virtualThreads) attributes — "true" opts in for every op on the resource (or just the annotated op); "false" at op level opts out of a resource-level enablement; "" (default) inherits.
  • When enabled (and the runtime is Java 21+), RestOpInvoker dispatches the entire handler call — parameter binding, the user method body, and the standard exception-mapping path — onto a virtual thread submitted to a per-resource Executors.newVirtualThreadPerTaskExecutor(). The platform "carrier" thread that received the request is parked on CompletableFuture.get() until the virtual thread finishes.
  • Java 17 graceful degradation: on Java 17 / 18 / 19 / 20, the flag is detected at first use, a one-shot WARNING is logged via Logger.getLogger("org.apache.juneau.rest.RestContext.async"), and the framework falls back to caller-thread dispatch. No runtime error.
  • Reflective executor creation: Executors.class.getMethod("newVirtualThreadPerTaskExecutor").invoke(null) is gated behind Runtime.version().feature() >= 21, so the framework compiles cleanly under the project's Java 17 source level (no <source>21</source> bump needed).
Composition​

The two features are independent but compose naturally — a CompletableFuture return on a virtualThreads=true resource gives both structured-concurrency benefits AND a non-blocking response path. Virtual-thread dispatch sits ABOVE the observability hooks: when virtualThreads=true, the entire invoke(...) body — including TracerHook.startSpan(...) and MetricsRecorder.record(...) — runs on the virtual thread, so OTel's Context.makeCurrent() works as expected. Async returns defer observability completion to future-resolution time: when the handler returns a CompletionStage, the framework attaches a whenComplete callback that closes the OTel Scope and records the Micrometer Timer sample at the moment the future resolves — not the synchronous "future returned" moment.

Migration / backward compatibility​

Synchronous handlers (no CompletableFuture, no virtualThreads=true) have zero behavioral change. Async opt-in is purely by changing the return type; virtual-thread opt-in is purely by setting the attribute.

Thread-local caveats​

RequestAttributes, VarResolverSession, Locale are request-scoped, not ThreadLocal-backed — they survive the async hop. MDC and security contexts that rely on ThreadLocal (SLF4J MDC, classic SecurityContextHolder-style patterns) do not survive the async hop. If you need MDC across an async boundary, copy the relevant keys into RequestAttributes before returning the future, or enable the new SLF4J MDC bridge described below.

See REST Server — Async Returns + Virtual-Thread Dispatch for the full reference.

SLF4J MDC async propagation bridge​

When a @RestOp handler returns a CompletableFuture, the whenComplete callback typically runs on a different thread (a virtual thread, pool thread, or fork-join worker). Log statements emitted inside that callback previously had no SLF4J MDC context — any requestId, userId, or trace-correlation keys set by an upstream filter were invisible to the logger running on the completion thread.

New behavior (default-on when SLF4J is on the classpath): AsyncResponseProcessor now snapshots the request thread's MDC map immediately before registering the whenComplete callback, then wraps the callback so the completion thread sees the same diagnostic context.

// No code change required — MDC propagates automatically:
@RestGet("/orders/{id}")
public CompletableFuture<Order> getOrder(@Path String id) {
// MDC.get("requestId") is non-null here (set by upstream filter).
return orderService.fetchAsync(id);
// ... and is also non-null inside the whenComplete callback, even on a virtual thread.
}

Key properties:

  • Zero-cost when unused. If the request thread's MDC is empty at dispatch time (the common case for services that don't set MDC), the snapshot is null and the callback is registered unchanged — no extra allocations.
  • Clean-up on the completion thread. After the callback returns (or throws), the completion thread's MDC is restored to exactly the state it was in before the callback. The listener removes only the keys it installed — any MDC state the completion thread already had is preserved. No thread-pool contamination.
  • Exceptional-completion path. If the future completes with an error, the finally block still runs — MDC is cleared even when the callback throws.
  • SLF4J-only. The bridge covers SLF4J MDC (org.slf4j.MDC). Log4j2 ThreadContext direct users (without the SLF4J facade) are not covered in v1. Users on the SLF4J ↔ Log4j2 bridge get the correct behavior automatically.
  • Opt out. Set the system / environment variable RestContext.mdcAsyncPropagation=false to disable globally, or call RestContext.Builder.mdcAsyncPropagation(false) to disable on a specific resource.

New API surface:

  • org.apache.juneau.rest.server.processor.MdcAsyncListener (new) — static utility: snapshot() captures the current thread's MDC; wrap(BiConsumer, Map) decorates a completion callback with snapshot-restore-clear logic. No SLF4J compile-time dependency; detected reflectively at class-load time.
  • RestContext.isMdcAsyncPropagation() (new) — returns true when MDC propagation is enabled. Defaults to true; overridden by the RestContext.mdcAsyncPropagation env-driven property.
  • RestContext.Builder.mdcAsyncPropagation(boolean) (new) — per-resource programmatic override.

Per-resource async completion executor​

Production deployments need to isolate CompletableFuture completion callbacks on a dedicated thread pool — the future's natural completion thread is often an I/O-driver or virtual thread that should not be blocked by response-handler work. A new asyncCompletionExecutor attribute lets you route callbacks to any named Executor bean registered with the resource.

@Rest(path="/api", asyncCompletionExecutor="myCompletionPool")
public class ApiResource extends RestServlet {

@Bean(name="myCompletionPool")
public Executor completionPool() {
return Executors.newFixedThreadPool(8, r -> new Thread(r, "response-handler-"));
}

@RestGet("/orders/{id}")
public CompletableFuture<Order> getOrder(@Path String id) {
return orderService.fetchAsync(id);
// whenComplete callback runs on myCompletionPool, not the I/O thread
}
}

Key details:

  • @Rest(asyncCompletionExecutor="beanName") — resource-level default; resolves a java.util.concurrent.Executor bean by name from BeanStore. Empty string (default) = no override; natural completion thread is used.
  • @RestOp(asyncCompletionExecutor="beanName") and all verb annotations (@RestGet, @RestPost, …) — per-operation override; wins over the resource-level setting.
  • RestContext.Builder.asyncCompletionExecutor(String beanName) — programmatic knob for the same configuration.
  • Startup-fail — if the configured bean name does not resolve to an Executor in BeanStore, RestContext construction throws immediately (misconfiguration is never silently ignored).
  • MDC propagation compatibility — the MdcAsyncListener wraps the callback before executor routing; the MDC snapshot is taken on the dispatching thread and restored on whichever thread the executor selects.
  • Virtual-thread executor — Executors.newVirtualThreadPerTaskExecutor() (Java 21+) is a valid value; each callback runs on a fresh virtual thread.
  • Default behavior unchanged — when the annotation is absent, whenComplete(callback) is used (natural thread, identical to 9.4.x behavior).

New API:

  • @Rest(asyncCompletionExecutor=...) / @RestOp(asyncCompletionExecutor=...) / all verb annotations — new String annotation member.
  • RestContext.Builder.asyncCompletionExecutor(String) — programmatic setter.
  • RestContext.getAsyncCompletionExecutor() — resource-level resolved executor (or null).
  • RestOpContext.getAsyncCompletionExecutor() — op-level resolved executor with resource-level fallback.
  • RestServerConstants.PROPERTY_asyncCompletionExecutor — annotation property key constant.

Reactive-Streams return types — Flow.Publisher core + Reactor / RxJava bridge​

@RestOp handlers may now return reactive-streams values. The feature is fully opt-in by adding a module to the classpath — a bare juneau-rest-server has zero reactive behavior and DefaultConfig wires no reactive processor. The JDK-native java.util.concurrent.Flow.Publisher<T> is supported by the new dependency-free, opt-in juneau-rest-server-reactive module; Project Reactor (Mono / Flux), RxJava 3 (Single / Maybe / Completable / Flowable / Observable), and the generic org.reactivestreams.Publisher<T> are supported by the further opt-in, provided-scope juneau-rest-server-reactive-reactor module (which depends on juneau-rest-server-reactive).

@Rest(path="/feed")
public class FeedResource extends BasicRestServlet {

// Single-value reactive type — collapses onto the existing async (CompletableFuture) path.
@RestGet("/{id}")
public Mono<Article> get(@Path String id) {
return articleService.findById(id); // Reactor Mono → CompletionStage
}

// Bounded stream, default shape — collected into a JSON array.
@RestGet("/recent")
public Flux<Article> recent() {
return articleService.recent(20); // buffered to a List, serialized as JSON
}

// Unbounded stream as Server-Sent Events — opt in via the response content type.
@RestGet("/live")
public Flux<SseEvent> live(RestResponse res) {
res.setContentType("text/event-stream"); // → SSE frames, one per element
return articleService.liveEvents();
}

// Same stream as newline-delimited JSON.
@RestGet("/ndjson")
public Flux<Article> ndjson(RestResponse res) {
res.setContentType("application/x-ndjson"); // → one JSON object per line
return articleService.recent(1000);
}
}

Unified architecture (the two promoted plan items ship as one coherent feature):

  • Opt-in by classpath presence. ReactiveResponseProcessor is not wired into DefaultConfig. It ships in juneau-rest-server-reactive with a META-INF/services/org.apache.juneau.rest.server.processor.ResponseProcessor provider file; RestContext discovers module-contributed response processors via ServiceLoader and front-loads them ahead of AsyncResponseProcessor. On a bare juneau-rest-server the discovery finds nothing and the processor chain is byte-for-byte the pre-feature default.
  • ReactiveResponseProcessor (org.apache.juneau.rest.reactive, in the opt-in juneau-rest-server-reactive module) is the single shared spine. Once on the classpath it is front-loaded ahead of AsyncResponseProcessor and natively understands Flow.Publisher. Synchronous returns (String, beans, byte[], …) are untouched.
  • Single-value reactive types (Mono, Single, Maybe, Completable) are adapted to a CompletionStage and collapse onto the existing AsyncResponseProcessor path — inheriting its timeout, @Rest(asyncCompletionExecutor), and SLF4J MDC bridge behavior for free.
  • Multi-value streams are rendered in one of three shapes, selected by the negotiated response media type (handler Content-Type, then request Accept):
    • Buffer (default) — all elements collected into a List and serialized through the normal chain (e.g. a JSON array). Wrapped in a CompletableFuture so a slow producer never blocks the request thread.
    • SSE (text/event-stream) — each element emitted as a Server-Sent-Events frame; SseEvent elements are written verbatim, any other element type is JSON-encoded into the data: field.
  • Behavior change: reactive NDJSON responses now echo the negotiated application/jsonl / application/json5l type (aliases canonicalised) instead of always application/x-ndjson; an explicit Content-Type still wins.
  • Backpressure — streaming subscribers request one element at a time (request(1) on subscribe and again after each frame is flushed). Because the servlet output stream blocks until the socket drains, the producer is paced by the client and the server-side buffer stays bounded.
  • Third-party libraries are provided-scoped — reactive-streams, reactor-core, and rxjava are all declared provided in juneau-rest-server-reactive-reactor. A dependency:tree on juneau-rest-server never surfaces them. Adapters are discovered lazily via ServiceLoader and skipped at runtime if their backing library is absent, so a consumer adds juneau-rest-server-reactive-reactor plus only the reactive library (and version) they actually use.
  • Extensibility — the ReactiveStreamsAdapter SPI + Adaptation value type let third parties bridge any other reactive library to the core Flow.Publisher / CompletionStage plumbing without modifying juneau-rest-server.

New API (all in the opt-in juneau-rest-server-reactive module):

  • org.apache.juneau.rest.reactive.ReactiveResponseProcessor — the response processor, auto-registered via ServiceLoader when the juneau-rest-server-reactive module is on the classpath (no DefaultConfig wiring).
  • org.apache.juneau.rest.reactive.ReactiveStreamsAdapter — SPI for bridging third-party reactive types (boolean canAdapt(Object) + Adaptation adapt(Object)).
  • org.apache.juneau.rest.reactive.Adaptation — single-value (CompletionStage) or streaming (Flow.Publisher) adaptation result.

RestContext now discovers module-contributed ResponseProcessors via ServiceLoader (inert on a bare classpath) — this is the seam that auto-activates the reactive processor.

See Reactive-Streams Returns for the full topic.

New module: juneau-rest-server-reactive​

A new dependency-free, opt-in module that holds the JDK-native Flow.Publisher spine (ReactiveResponseProcessor), the ReactiveStreamsAdapter SPI, the Adaptation value type, and the META-INF/services/...ResponseProcessor auto-registration file. Adding it alone enables Flow.Publisher<T> returns with no third-party dependency. A bare juneau-rest-server without it has zero reactive behavior.

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-reactive</artifactId>
<version>10.0.0</version>
</dependency>

New module: juneau-rest-server-reactive-reactor​

A further opt-in bridge module (depends on juneau-rest-server-reactive) providing ReactiveStreamsAdapter implementations for the popular reactive libraries. Server-side only — there is no client-side counterpart. All three backing libraries are provided-scope.

AdapterHandlesAdapts to
ReactorReactiveAdapterMono<T> / Flux<T>Mono → CompletionStage (via toFuture()); Flux → Flow.Publisher (via FlowAdapters)
RxJavaReactiveAdapterSingle / Maybe / Completable / Flowable / Observablesingle types → CompletionStage; Flowable → Flow.Publisher; Observable → Flowable(BackpressureStrategy.BUFFER) → Flow.Publisher
ReactiveStreamsPublisherAdaptergeneric org.reactivestreams.Publisher<T>Flow.Publisher (via FlowAdapters); registered last so Mono/Flux are matched as single/stream first

Maven coordinate:

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-reactive-reactor</artifactId>
<version>10.0.0</version>
</dependency>
<!-- plus ONLY the reactive library you use, e.g.: -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.11</version>
</dependency>

Health Probe SPI + Resource​

juneau-rest-server now includes a built-in probe SPI and aggregation resource under org.apache.juneau.rest.server.health:

  • HealthIndicator functional SPI (Health check()).
  • Health value type with static builders: Health.up(name), Health.down(name, throwable), Health.unknown(name), plus structured detail entries.
  • HealthProbe / HealthStatus enums for probe categorization and aggregate state.
  • HealthMixin exposing /healthz, /readyz, and /livez with aggregate body format: {status, components:{name:{status,details}}}.
  • HTTP status mapping follows Kubernetes conventions: 503 if any component is DOWN, else 200.
  • HealthProbeSettings introduces per-indicator timeout control (default 1s); exceptions/timeouts are converted to DOWN with details.error.

@Rest(mixins=...) Composition + @Rest(paths=...) Multi-Mount​

See REST Server — Mixins and Multi-Mount Paths for the full reference.

The health-probe follow-up adds two new @Rest composition/mounting primitives:

  • mixins (Class<?>[]) — graft @RestOp-group methods from mixin classes into a resource's operation tree. Local methods on the importing resource win on path collisions.
  • paths (String[]) — optional multi-mount path specs for top-level servlet registration. Jetty mounts a single servlet instance at each declared path.

HealthMixin now declares @Rest(paths={"/healthz","/readyz","/livez"}) instead of @Rest(path="/"), eliminating startup collisions with apps whose root resource already owns /*. Users can now choose either:

  • Recommended: @Rest(mixins=HealthMixin.class) on the root resource (single servlet).
  • Fallback: standalone HealthProbeConfiguration auto-mount (separate servlet, explicit paths).

Mixin Sub-Context Inheritance​

@Rest(mixins=…) mixin classes are now each elevated into a dedicated RestContext sub-context rather than being treated as bare method libraries grafted into the host's context. Each mixin gets its own bean store, hook chain, and contribution lists; the host's RestContext becomes the mixin's parentContext, and the mixin inherits configuration via the same parent-walk used for the rest of the @Rest annotation chain — with @Rest(noInherit={…}) available as the per-property opt-out (semantics aligned with the rest of the framework).

  • Inheritance walk. RestContext.getRestAnnotationsForProperty(name) prepends the host's @Rest chain onto the mixin's own so the reduce-last / append-all walks all the contribution lists transparently. Verified by a per-list regression matrix covering serializers, parsers, encoders, converters, response processors, REST op args, guards (default inherit + noInherit opt-out), callLogger, debugEnablement, debugDefault, partSerializer, and partParser. None of these lists needed bespoke wiring — the walk drives them all.
  • Hook dual-firing. @RestStartCall / @RestEndCall / @RestPreCall / @RestPostCall / @RestDestroy now fire host-first-then-mixin for mixin-endpoint requests and host-only for host-endpoint requests. Hook execution order is the standard host→mixin chain regardless of which mixin class declared the operation that's being dispatched, so per-request observability (logging, metrics, request-id injection) stays uniform across host and mixin endpoints.
  • Messages chaining. Messages doesn't ride the @Rest walk (it has its own parent-resource-bundle chain semantics), so the new Messages.chain(child, parent) static factory composes two pre-built bundles into a single parent-chained bundle without mutating either input. The messages memoizer uses it to inherit the host's bundle on mixin sub-contexts (mixin-keys-first, host-keys-as-fallback). @Rest(noInherit={"messages"}) cuts the chain off for callers that want isolation.
  • DefaultConfig synthesis is host-only. The framework's synthesized DefaultConfig entry on a bare @Rest resource is now skipped for mixin sub-contexts, because the host's chain already supplies it. Without this, a mixin without an explicit per-property declaration would silently pick up the framework's DefaultConfig default after the host's explicit value in the combined parent-walk — i.e. mixins would shadow host overrides for any property they didn't declare themselves. The skip is invisible to user code; it just makes "no declaration on the mixin" actually inherit from the host.
  • New canonical RestContext.Args constructor. The legacy 8-arg back-compat constructor was removed; the canonical 9-arg form now takes an explicit mixinContext flag (host=false, mixin sub-context=true). No external callers exist; all internal call sites in juneau-rest-server, juneau-rest-mock, and juneau-commons were updated.

Runtime-Overridable @Rest(paths=...) Resolution Chain​

The paths array on @Rest is now a default in a three-rung runtime-override resolution chain, walked once during RestContext construction and exposed via RestContext.getPaths(). Apps can substitute the top-level mount paths at deploy time without recompiling the resource.

Precedence (highest first):

  1. Programmatic — RestContext.Args.paths() / RestContext.Builder.paths(String...). null means "inherit from lower rungs"; new String[0] is the explicit-clear sentinel.
  2. getPaths() virtual getter — added to both RestServlet and RestResource (default null). Returning null inherits the lower rung; new String[0] is the explicit-clear sentinel; non-empty wins.
  3. @Rest(paths={"…"}) annotation default — new behavior: each element runs through SVL substitution and the resolved value is then split on , (trim each piece, drop empties). A single template element can therefore expand to zero, one, or many mount paths.

The annotation-default rung now supports the same SVL surface as any other @Rest string member:

@Rest(paths={"$C{health.paths}"})                      // single key, multi-value
@Rest(paths={"/api", "$C{extra.paths}"}) // mix literal + resolved
@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"}) // env var w/ comma-separated default

SVL $C{key} consults whatever Juneau Config is registered on the bean store (test overlay, @Bean factory, or the framework's runtime Config); $E{NAME,default} and $S{prop,default} use the bootstrap variable catalog without needing any beans. An SVL failure or missing key with no default substitutes empty, which the comma-split then drops — same semantics as every other SVL-bearing @Rest member.

The same chain runs whether the app is wired via Juneau auto-discovery (JettyServerComponent) or Spring Boot's manual registration. A new public mount-time helper RestContext.resolveTopLevelPaths(Class, Object, BeanStore) lets Spring Boot users compute the resolved paths inside their @Bean ServletRegistrationBean<MyServlet>(...) factories — the programmatic Builder rung is N/A for that flow (no RestContext exists at registration time; substitute via the getter or by registering a Config bean that the annotation's $C{...} elements can resolve through).

Spring Boot application.yaml integration is planned under a separate work item. Once that lands, $C{key} in @Rest(paths=...) elements will transparently consult Spring's Environment under Spring Boot via a Juneau-Config-to-Environment bridge — no API change here, just an additional resolution source registered with the Config bean.

See REST Server — Mixins and Multi-Mount Paths § Runtime-overridable paths for the full reference, including the precedence table, worked examples for each rung, the Spring Boot integration walkthrough, and the null-vs-empty-array semantic table.

This change is purely additive — resources that don't override getPaths(), aren't constructed with a programmatic Args.paths(), and don't include SVL markers in their @Rest(paths=...) literals continue to behave exactly as they did under an earlier change (the annotation default wins on its own, element-for-element). Pre-10.0.0 users who already adopted @Rest(paths=...) see no behavior change unless they introduce an SVL marker or an embedded comma in an element.

SVL ${...} resolution in @RestOp(path) / @RestGet(path) / @RestOp(value)​

SVL variables (${var:default}, $S{key,default}, $E{NAME,default}, $C{key}, etc.) are now resolved in op-level path declarations — @RestOp(path), @RestGet(path), @RestPost(path), @RestPut(path), @RestDelete(path), and the @RestOp(value="METHOD path") verb-on-value form — closing the asymmetry with class-level @Rest(path) / @Rest(paths), which were already SVL-resolved.

This enables configurable mount paths for mixin-style resources without subclassing:

@Rest(responseProcessors=JspViewRenderer.class)
public static class JspMixin {
// Mounts at "/jsp/*" by default; deployers can override at runtime via system property,
// env var (after Settings normalization), or a registered Config bean.
@RestGet(path="/${jsp.path:jsp}/*")
public View hello() { return new JspView("hello.jsp"); }
}

The shorthand ${name:default} uses Juneau's dollar-brace shortcut (registered as PropertyVar, which reads from Settings.get()); the longer SVL forms — $S{key,default}, $E{NAME,default}, $C{key} — also work and are preferred when the source is explicit. SVL resolution happens at RestContext construction time, so deployer overrides must be in place before the resource context is built.

The auto-detected fallback path (derived from the Java method name when no path() / value() is declared) is intentionally not SVL-resolved — it is framework-derived, not user input. Path strings whose SVL resolution yields an empty string are dropped (matches how class-level @Rest(paths) drops empty pieces in the post-comma-split pipeline). Unresolved variables with no default pass through to UrlPathMatcher.of(...) as the literal ${...} placeholder, which then fails predictably at routing time without crashing at startup.

This change is purely additive — annotation strings that contain no SVL markers are passed through unchanged.

Bundled mixin op-paths now SVL-configurable​

Building on an earlier change (SVL resolution in op-level path declarations), every Juneau-bundled mixin resource whose mount path is a Juneau organizational/conventional default — not fixed by an external specification — now declares its op-level path as /${juneau.<role>.path:<default>} so deployers can relocate the mount via system property, environment variable, or Config key without subclassing.

The variable map:

MixinSVL variableDefault mount
SwaggerMixin${juneau.swagger.path:api}/api/*
SwaggerUiMixin${juneau.swaggerui.path:swagger}/swagger/*
OpenApiMixin${juneau.openapi.path:openapi}/openapi/*, /openapi.json, /openapi.yaml
RedocMixin${juneau.redoc.path:redoc}/redoc/*
StaticFilesMixin${juneau.staticfiles.path:static}/static/*
VersionMixin${juneau.version.path:version}/version
EchoMixin${juneau.echo.path:echo}/echo/*
AdminMixin${juneau.admin.path:admin}/admin/threads, /admin/heap, /admin/cache/flush, /admin/ratelimit
RouteIndexMixin${juneau.routeindex.path:options}/options
JspMixin${juneau.jsp.path:jsp}/jsp/*

Example — relocate the Swagger UI mount to /docs/swagger-ui:

java -Djuneau.swaggerui.path=docs/swagger-ui -jar my-app.jar

Three mixins remain hardcoded because their mount paths are specification-fixed and a runtime override would break spec-compliant clients:

  • FaviconMixin — /favicon.ico (browser convention + WHATWG HTML rel="icon" default).
  • SeoMixin — /robots.txt (RFC 9309 Robots Exclusion Protocol) and /sitemap.xml (sitemaps.org protocol). The two endpoints sit on separate handler methods, not a multi-path array, so they remain individually pinned.
  • WellKnownMixin — /.well-known/security.txt (RFC 8615 well-known URIs + RFC 9116 security.txt).

Migration note — multi-path collapse: four mixins that previously declared multiple paths in a single op annotation's path={...} array — historical "convenience alias" defaults from before the SVL mechanism existed — have been collapsed to a single SVL-configurable path. The secondary aliases are now reached by overriding the SVL variable instead of being mounted by default:

MixinPrimary defaultRemoved secondary alias(es)Migration
StaticFilesMixin/static/*/htdocs/*-Djuneau.staticfiles.path=htdocs
VersionMixin/version/info, /about-Djuneau.version.path=info (or about)
EchoMixin/echo/*/debug/echo/*-Djuneau.echo.path=debug/echo
RouteIndexMixin/options/routes-Djuneau.routeindex.path=routes

Deployers who relied on a secondary default must either set the SVL override at startup or compose a second mixin instance with the override. The default BasicStaticFiles classpath search root still walks both the static/ and htdocs/ directories at the JAR-resource layer — only the URL-side mount alias has been removed.

SVL resolution happens once at RestContext construction time (see the earlier section above for the full resolution chain). Each configurable mixin ships with a *_SvlPathOverride_Test in juneau-utest that exercises the override end-to-end via MockRestClient.

Mixin path overrides now accept liberal input forms​

Building on an earlier change (SVL-configurable mixin paths) and an earlier change (pathToken() in the SVL scripting catalog), all 10 SVL-configurable mixin op-paths now wrap their override token in #{pathToken(...)} so users can supply any of the following forms and the framework always produces a clean mount:

Input formExampleEffective mount
Bare tokenjsp/jsp/*
Leading slash/jsp/jsp/*
Trailing slashjsp//jsp/*
Both slashes/jsp//jsp/*
Wildcard suffix/jsp/*/jsp/*
Multi-segment/api/v1/jsp/*/api/v1/jsp/*

Before this change, setting juneau.jsp.path=/jsp resulted in a double-slash path //jsp/* that silently failed to match any request. After this change, all of the above input forms resolve identically to the canonical mount.

The pathToken() function (added in an earlier Phase D string catalog) strips leading/trailing slashes and a trailing /* wildcard suffix while preserving multi-segment paths. It is registered on VarResolver.DEFAULT via defaultFunctions() and is available to any SVL template. See the SVL scripting section above for the full function catalog.

Each mixin's *_SvlPathOverride_Test has been extended with 5–6 new equivalence test cases (bare token, leading slash, trailing slash, both slashes, wildcard suffix, and multi-segment) covering all input forms per class.

OpenAPI 3.1 Emission + API-docs Mixin Pack​

juneau-rest-server now ships first-class OpenAPI 3.1 document generation alongside the existing Swagger v2 path, with the public surface composed via four small @Rest(mixins=...) mixins instead of a apiFormat string knob:

OpenAPI 3.1 emission core:

  • New org.apache.juneau.rest.server.openapi package with OpenApiProvider SPI, BasicOpenApiProvider, and BasicOpenApiProviderSession. The session generates OpenAPI 3.1 via a JSON-level transformation of the existing Swagger 2.0 emission, so every existing Swagger-aware annotation surface (@Schema, @Content, @StatusCode, etc.) round-trips to the OpenAPI document with no source changes.
  • New RestRequest.getOpenApi() accessor and RestContext.getOpenApiProvider() / RestContext.getOpenApi(Locale) parallel the Swagger family.
  • New RedocUI swap (in juneau-bean-openapi-v3) renders OpenAPI 3.1 documents as a two-column Redoc-style HTML view for text/html requests.
  • New SVL variable $OS{path} mirrors $SS{path} for OpenAPI document lookups.
  • New @Rest(openApiProvider=X.class) attribute lets resources select a custom provider.
  • components.schemas reuse. BasicOpenApiProviderSession runs an explicit dedup pass after the Swagger-2.0-to-OpenAPI-3.1 transform: any inline schema that appears two or more times under operation parameter, request-body, or response-content slots is lifted into components.schemas and each occurrence is rewritten to a {"$ref":"#/components/schemas/<name>"} pointer. Names are derived from the schema's title (when present and unique) and fall back to synthesized Schema<N> keys with collision avoidance against existing entries. Bean-typed slots already flow through definitions → components.schemas via the existing transform, so this primarily covers ad-hoc inline schemas that didn't fall under useBeanDefs="true".
  • YAML round-trip coverage. OpenApiYamlRoundTrip_Test exercises YamlSerializer.DEFAULT_READABLE → YamlParser.DEFAULT over (a) a hand-built OpenApi bean, (b) the live document produced by a BasicRestServlet-based resource (mounted via the api-docs mixin pack), and (c) the /openapi endpoint served with Accept: application/yaml. Each path asserts structural equality across openapi, info, servers, paths, and components.schemas.

API-docs mixin pack:

The api-docs surface is now four sibling @Rest(mixins=...) mixins in the new org.apache.juneau.rest.server.docs package — composition replaces the apiFormat string knob:

MixinDefault pathsOwns
SwaggerMixin/apiSwagger v2 spec + SwaggerUI swap. Standard content negotiation.
SwaggerUiMixin/swaggerHTML-first mount; @Rest(defaultAccept="text/html") so bare browser requests render the Swagger-UI view. Transitively pulls in SwaggerMixin.
OpenApiMixin/openapi, /openapi.json, /openapi.yamlOpenAPI 3.1 spec + RedocUI swap on /openapi. The .json and .yaml paths are format-pinned — they ignore the request Accept header and always emit the file extension's wire format (implemented via RestResponse.getDirectWriter(...), not content negotiation, so the pin survives Accept: text/html). Mixin declares @Rest(serializers={YamlSerializer.class}) so YAML is available for the /openapi content-negotiated path; an earlier sub-RestContext model confines the contribution to mixin endpoints only.
RedocMixin/redocHTML-first mount; @Rest(defaultAccept="text/html"). Transitively pulls in OpenApiMixin.

Pick the mixins you want and the URL surface follows. Three common compositions:

// OpenAPI 3.1 spec only — /openapi, /openapi.json, /openapi.yaml.
@Rest(path="/api", mixins=OpenApiMixin.class)
public class ApiResource extends RestServlet { ... }

// OpenAPI 3.1 + Redoc UI — pulls OpenApiMixin in transitively.
@Rest(path="/api", mixins=RedocMixin.class)
public class ApiResource extends RestServlet { ... }

// Everything (post-10.0.0 default for BasicRestServlet / BasicRestResource).
@Rest(path="/api", mixins={SwaggerUiMixin.class, RedocMixin.class})
public class ApiResource extends RestServlet { ... }
// Serves: /api, /swagger, /openapi, /openapi.json, /openapi.yaml, /redoc

Transitive resolution dedupes by class identity (LinkedHashSet in the framework's collectRestMixins(...) walk), so listing both UI mixins doesn't double-instantiate the spec mixins. Parent-chain aggregation also works — a child class's @Rest(mixins=...) unions with the parent's.

BasicRestServlet and BasicRestResource migrated to declare @Rest(mixins={SwaggerUiMixin.class, RedocMixin.class}) so subclasses get all six api-docs URLs by default. Apps that want to opt out (e.g. to mount only OpenAPI) can use @Rest(noInherit={"mixins"}, mixins=...) to cut off the inherited docs mixins.

Breaking changes:

RemovedReplacement
@Rest(apiFormat="swagger" | "openapi" | "both")Pick mixins. Both /api and /openapi mount when both spec mixins are present.
juneau.rest.apiFormat system propertyRemoved. No replacement — apps that toggled this at runtime should pick mixins at compile time.
RestContext.getApiFormat()Removed.
RestServerConstants.API_FORMAT_SWAGGER / _OPENAPI / _BOTH / SYSPROP_apiFormat / PROPERTY_apiFormatRemoved.
BasicRestOperations.getSwagger(...) / getOpenApi(...)Removed from the interface. BasicRestServlet / BasicRestResource mount these via SwaggerMixin / OpenApiMixin instead.
BasicGroupOperations.getChildrenSwagger(...) / getChildrenOpenApi(...) (the ?Swagger / ?OpenApi query mirrors on group resources)Removed. Hit /api, /swagger, /openapi, or /redoc directly — they are always mounted on BasicRestServletGroup / BasicRestResourceGroup.
BasicGroupOperations.HasSwaggerQueryParam / HasOpenApiQueryParam matcher inner classesRemoved (no consumers left).

Migration sketch:

// 9.4.x:
@Rest(apiFormat="openapi")
public class MyApi extends BasicRestServlet { ... }

// 10.0.0:
@Rest(noInherit={"mixins"}, mixins=RedocMixin.class) // opt out of Swagger
public class MyApi extends BasicRestServlet { ... }
// or extend RestServlet directly and pick what you want:
@Rest(mixins=RedocMixin.class)
public class MyApi extends RestServlet implements BasicUniversalConfig { ... }

Apps that subclass BasicRestServlet / BasicRestResource without setting apiFormat or relying on the query mirrors get the new behavior (six api-docs URLs instead of one) automatically.

Static-Files Mixin​

juneau-rest-server now ships a StaticFilesMixin mixin in the new org.apache.juneau.rest.server.staticfile package that wraps the existing BasicStaticFiles plumbing in a servlet-level mixin. Any @Rest-annotated resource can opt into static-file serving via @Rest(mixins = StaticFilesMixin.class) without subclassing BasicRestServlet just to inherit getHtdoc(...). Default mounts are /static/* and /htdocs/* (both routes share the same @RestGet handler) and the resolved StaticFiles bean is read from the importer's bean store at request time, so @Bean StaticFiles factory methods, headers, and classpath search roots flow through unchanged. See Static-Files Mixin for the full reference, including multi-mount semantics, HEAD support, Cache-Control behavior, the OpenAPI-hidden mechanism, and the path-override constraint documented in the topic page's Path-override constraint section.

Convention-Endpoints Mixin Pack​

juneau-rest-server now ships a four-class convention-endpoints mixin pack in the new org.apache.juneau.rest.server.convention package covering the de-facto-standard URLs every public-facing service eventually wires up by hand. Each mixin is a single-purpose @Rest-annotated resource designed to be grafted into any servlet via @Rest(mixins=...) — configure via a @Bean MixinClass factory method on the host, leave the rest unmounted.

  • FaviconMixin — serves /favicon.ico. Default body is the Juneau-branded juneau-favicon.ico shipped on the framework classpath; Cache-Control: max-age=2592000, public (30 days). Builder methods: bytes(byte[]), classpath(String), cacheControl(String).
  • SeoMixin — serves /robots.txt and /sitemap.xml. Default robots body is the RFC 9309 deny-all User-agent: *\nDisallow: /\n; default sitemap is an empty <urlset>. Builder methods: robotsAllow(...) / robotsDisallow(...) / robotsTxt(String) for rule-based or fully-formed bodies; sitemapEntry(String url[, lastmod, changefreq, priority]) for per-URL entries.
  • VersionMixin — serves /version, /info, and /about (synonyms in v1) as a format-pinned JSON map sourced from META-INF/MANIFEST.MF, git.properties, and the running JVM version. Default builder chain is fromManifest().fromGitProperties().fromJavaVersion(); programmatic overrides via entry(String, String) / entries(Map) suppress the defaults so the importer opts into each fromXxx() call explicitly. Spring Boot fat-jar tip: pass Builder.fromManifest(importerClassLoader) to read the app's manifest instead of the framework's.
  • WellKnownMixin — reserves /.well-known/security.txt. Returns 404 Not Found by default (RFC 9116 makes the absence of security.txt itself meaningful); set a body via Builder.securityTxt(String). The class reserves room for future /.well-known/* entries (OIDC discovery, change-password, etc.) without committing v1 to a placeholder.

All four endpoints carry @OpSwagger(ignore = true) so they're excluded from any Swagger / OpenAPI spec generated alongside them — convention URLs aren't API-meaningful and would just clutter the contract. See Convention-Endpoints Mixin Pack for the full reference, including per-mixin builder semantics, default-vs-override behavior, and real-container parity tests under both Jetty microservice and Spring Boot.

Framework alignment: RestContext.buildMixinContext(...) was retuned to first consult the host's BeanStore for a pre-registered mixin instance (beanStore.getBean(MixinClass)) before falling back to beanStore.instantiate(MixinClass). This makes @Bean MixinClass factory methods on the host first-class for every @Rest(mixins=...) mixin (not just the convention-endpoints pack), so a host can configure a mixin's builder once and let the framework use that exact instance instead of constructing a fresh one. The previous behavior — always instantiating via the mixin's own Builder — meant @Bean factories were silently bypassed for any mixin that exposed a create() static method. Existing mixins without @Bean factory methods on their hosts continue to instantiate via their builders unchanged.

  • @OpSwagger(ignore = true) (new annotation member). New boolean member on org.apache.juneau.rest.server.OpSwagger that excludes an operation from the published Swagger / OpenAPI specification. Honored by BasicSwaggerProviderSession (and therefore by the OpenAPI 3.1 emission path, which transforms from the Swagger 2.0 output). Introduced to hide the static-files mixin's greedy /* handlers, but reusable on any @RestOp-annotated method that should not surface in the published API contract (favicon handlers, internal probes, blob handlers). The legacy BasicRestOperations.getHtdoc(...) method now also carries @OpSwagger(ignore = true) so BasicRestServlet-hosted apps get a clean spec for free, with no need to subclass-and-suppress.
  • HEAD body-suppression in HttpResourceProcessor. The shared response processor for any HttpResource return value now suppresses the response body when the request method is HEAD, per RFC 7231 §4.3.2: headers (Content-Type, Content-Length, Cache-Control, plus any resource-supplied headers) are emitted identically to the equivalent GET, but the body is not written. This benefits any @RestOp-annotated handler that returns an HttpResource, not just the static-files mixin.

Ops / Introspection Mixin Pack​

juneau-rest-server now ships a three-class ops / introspection mixin pack in the new org.apache.juneau.rest.server.ops package covering the operational surface every long-running service eventually grows. Each mixin is a single-purpose @Rest-annotated resource designed to be grafted into any servlet via @Rest(mixins=...) — configure via a @Bean MixinClass factory method on the host, leave the rest unmounted.

  • EchoMixin — serves /echo/* and /debug/echo/* request-echo / round-trip introspection. Default 404 Not Found until the host's @Rest(debug=...) chain unlocks it (@Rest(debug="conditional") paired with a Debug: true request header is the recommended posture). When debug is on, returns a JSON body reflecting the inbound method, path, query string, headers (with Authorization / Cookie / Set-Cookie / Proxy-Authorization / X-API-Key redacted to [REDACTED] by default), query params, attributes, and bounded body capture (1 MB cap, configurable via Builder.bodyLimit(long)). Builder methods: bodyLimit(long), redactedHeaders(String...) (replace), redactHeader(String) (additive). The handler dispatches on @RestOp(method="*") so every HTTP verb hits the same code path.
  • AdminMixin — serves /admin/threads (JSON thread dump), /admin/heap (Runtime + MemoryMXBean stats), POST /admin/cache/flush (run registered hooks), and /admin/ratelimit (registered RateLimitGuard beans — both static configuration and live per-key bucket snapshot; see the RateLimitGuard.Storage.snapshot() SPI extension below). Default-deny via the new org.apache.juneau.rest.server.guard.DenyAllGuard until the host registers a @Bean RestGuardList factory; the framework's bean-store override seam replaces the entire annotation-derived guard list (including the deny-all) with the user-supplied chain. Builder methods: cacheFlush(String, Runnable), cacheFlushAll(Map), threadNamePrefixExclude(String...). Returns 404 Not Found when no RateLimitGuard bean is registered.
  • RouteIndexMixin — serves /options and /routes (synonyms) returning a JSON list of every @RestOp-annotated method on the host (and on every mixin on the host), ordered by path. Each entry has path, methods (single-element list of the HTTP method), summary, description, and deprecated. The route-index endpoint omits itself and every @OpSwagger(ignore=true) operation, mirroring the audience separation between the OpenAPI surface and the operator surface.

All endpoints carry @OpSwagger(ignore = true) so they're excluded from any Swagger / OpenAPI spec generated alongside them — ops endpoints are not API-meaningful and shipping them in the spec leaks operator surface to consumers. See Ops / Introspection Mixin Pack for the full reference, including per-mixin builder semantics, default-vs-override behavior, the deny-all override seam for AdminMixin, sensitive-header-redaction guarantees, and real-container parity tests under both Jetty microservice and Spring Boot.

  • DenyAllGuard (new class). Companion RestGuard in the existing org.apache.juneau.rest.server.guard package that rejects every request with 403 Forbidden. Used as the secure-by-default placeholder on operations or resources that require an authentication / authorization chain the importer must explicitly opt into. Reusable on any @Rest(guards=...) or @RestOp(guards=...) site that wants the same default-deny posture with a @Bean RestGuardList override seam.

RateLimitGuard.Storage.snapshot() SPI + AdminMixin enrichment​

org.apache.juneau.rest.server.guard.RateLimitGuard.Storage gained an optional read-side SPI for operator-facing tooling, and AdminMixin's /admin/ratelimit endpoint now uses it to emit live per-bucket state alongside the existing static configuration. Purely additive — the new method is a default returning Map.of() so external storage backends (Redis-backed, DynamoDB-backed, etc.) keep compiling unchanged.

  • New default Map<String, RateLimitGuard.BucketState> snapshot() on RateLimitGuard.Storage. Returns a point-in-time view of every per-key bucket the storage backend currently tracks. The default empty-map implementation is the backwards-compatible opt-out for storages that can't cheaply enumerate buckets. In-memory implementations SHOULD override.
  • New RateLimitGuard.BucketState record. Token-bucket-vocabulary per-bucket snapshot: String key, double tokens (current fractional fill level), int remaining (floor(tokens) — mirrors the X-RateLimit-Remaining advisory header), boolean throttled (convenience flag for tokens < 1.0), Instant lastRequest (wall-clock timestamp of the bucket's last activity).
  • Bundled Storage.inMemory() overrides snapshot(). Walks the internal ConcurrentHashMap<String,Bucket> and emits one BucketState per entry. Each per-bucket read is consistent (synchronized on the bucket); the snapshot as a whole is not a global point-in-time consistent view (consistent with the ConcurrentHashMap weakly-consistent iteration semantics).
  • Bucket gained a lastWallMillis field alongside the existing lastNanos so BucketState.lastRequest is wall-clock accurate (and not skewed by JVM suspension / container pause). Costs 8 bytes per bucket (~800 KB worst-case at the default 100k-key cap).
  • New public accessors on RateLimitGuard. getCapacity(), getPermitsPerSecond(), isXForwardedForAware(), getExemptPaths(), getStorage(), and the convenience snapshot() that delegates to getStorage().snapshot(). Expose existing builder state for operator-facing dashboards.
  • /admin/ratelimit response shape — buckets renamed to snapshot, config enriched. The 10.0.0 ops-pack mixin emitted { "guards": { "<bean-name>": { "config": {"class": "..."}, "buckets": [] } } } with the empty buckets array as an explicit placeholder. Now the response is { "guards": { "<bean-name>": { "config": {"class": "...", "limit": N, "permitsPerSecond": X, "xForwardedForAware": bool, "exemptPaths": [...] }, "snapshot": [ {"key": "...", "tokens": N.N, "remaining": N, "throttled": bool, "lastRequest": "..."} ] } } } — snapshot entries are sorted ascending by key for stable operator-tooling output. Storage backends that don't override Storage.snapshot() (e.g. Redis-backed impls) emit "snapshot": [] for their bean entry. The buckets field name was a placeholder that only ever emitted []; the rename to snapshot matches the SPI method name and is unlikely to affect any real consumer.

Server-side SSE Helpers​

juneau-rest-server now includes an SSE helper layer for streaming endpoints:

  • RestResponse.sse() returns SseResponseSupport for fluent event/comment writes (sendEvent(...), comment(...), flush(), sendFrom(...)).
  • SseBroadcaster / SseSubscription provide in-memory fan-out for per-connection subscriptions with bounded subscriber queues.
  • SseHeartbeat provides optional scheduled : ping comments (when a ScheduledExecutorService bean is available).
  • New RestOpArg resolvers allow direct SseBroadcaster and SseSubscription method-parameter injection in @RestOp handlers.

Dynamic Child Resources​

Parent resources that extend BasicRestServletGroup or BasicRestResourceGroup can now mount and unmount children at runtime, in addition to declaring them statically via @Rest(children=…).

@Rest(path="/root")
public class MyServer extends BasicRestServletGroup {}

var server = new MyServer();
// ... bootstrap into Jetty / Spring Boot / MockRestClient ...

server.addChild(AlphaChild.class); // by class, path from @Rest(path)
server.addChild(new BetaChild()); // by instance, path from @Rest(path)
server.addChild("/gamma", new AlphaChild()); // explicit path override
server.removeChild("/alpha");
server.removeChild(AlphaChild.class);
var keys = server.getChildResources().asMap().keySet();

The underlying state lives on RestChildren, which now holds children in a volatile copy-on-write snapshot. Route matching reads the snapshot without locks (preserving hot-path performance), and mutations are serialized through an internal write lock. @RestInit / @RestPostInit / @RestPostInitChildFirst run before addChild returns; @RestDestroy and Servlet.destroy() run on removeChild (recursively for grandchildren). Adding to an occupied path without replace=true throws IllegalStateException.

See Child Resources for the full API and usage notes.

@RestInject Renamed to @Bean (moved to juneau-commons)​

@RestInject has been renamed to @Bean and moved from org.apache.juneau.rest.annotation to org.apache.juneau.commons.inject. Update imports accordingly:

// Before
import org.apache.juneau.rest.annotation.RestInject;
@RestInject
public EncoderSet myEncoders(BeanStore bs) { ... }

// After
import org.apache.juneau.commons.inject.Bean;
@Bean
public EncoderSet myEncoders(BeanStore bs) { ... }

The semantics are identical; only the package and annotation name changed.

SpringBeanStore Renamed to SpringBeanStore​

SpringBeanStore has been renamed to SpringBeanStore for consistency with the BasicBeanStore → BasicBeanStore rename completed in this release. The old name SpringBeanStore is removed; update any import or type reference to org.apache.juneau.rest.springboot.SpringBeanStore.

RestServerConstants​

  • RestServerConstants — Central place for juneau-rest-server static literals, starting with Settings keys. Serializer/parser session-option settings (juneau.rest.sessionOptions.*) use fields such as SETTING_sessionOptions_rejectWhenAllowlistEmpty and SETTING_sessionOptions_failOnInvalidAllowlistEntry. HTTP wire names for those options are fields on RestSharedConstants in juneau-rest-common. This supersedes the RestSessionOptionsSettings type.

RestOpContext: RestOp-group annotations​

  • RestOpContext.getRestOpAnnotations() / findRestOpAnnotations() — Returns every annotation in the @RestOp group (including @RestGet, @RestPost, etc.) on the operation method and matching superclass/interface methods, in child-to-parent order; results are memoized until reset(). Typed AnnotationProvider.find(RestOp.class, MethodInfo) matches only a literal @RestOp, not grouped specializations such as @RestGet; use AnnotationInfo.isInGroup(RestOp.class) (as these methods do) when you need the full group.
  • RestOpContext.getNoInheritKeys() / findNoInherit() — Aggregates non-empty noInherit tokens from every RestOp-group annotation on the override chain (after SVL resolution and comma expansion), not only the nearest declaration. isInherited(String) on RestOpContext uses this combined set when deciding whether to merge allowedParserOptions / allowedSerializerOptions keys from the enclosing RestContext.
  • RestOpContext.getAllowedParserOptionsKeys() / findAllowedParserOptionsKeys() — Annotation-only parser allowlist keys: merges from RestContext when isInherited(PROPERTY_allowedParserOptions), then aggregates allowedParserOptions from every RestOp-group annotation on the method override chain (parallel to RestContext, but RestContext uses only the nearest @Rest).
  • RestOpContext.getAllowedSerializerOptionsKeys() / findAllowedSerializerOptionsKeys() — Same for serializer keys (allowedSerializerOptions, PROPERTY_allowedSerializerOptions).

Request Attributes and Session Properties Separation​

Request attributes and parser/serializer session properties are now completely separate concepts:

  • defaultRequestAttributes from @Rest and @RestOp annotations are stored in RequestAttributes and accessed via RestRequest.getAttributes()
  • Session properties control parser/serializer behavior and must be set explicitly via the new programmatic API on RestRequest
  • Previously, request attributes were automatically merged into session properties, which conflated two distinct concerns

New API​

RestRequest now provides explicit methods for setting session properties:

// Set individual session properties
req.setSerializerSessionProperty("maxDepth", 10);
req.setParserSessionProperty("trimStrings", true);

// Set multiple session properties at once
req.setSerializerSessionProperties(Map.of("maxDepth", 10, "addBeanTypes", true));
req.setParserSessionProperties(Map.of("trimStrings", true, "strict", true));

// Bridge request attributes to session properties if needed
req.setSerializerSessionProperties(req.getAttributes().asMap());

All methods support method chaining and are typically called from @RestPreCall, @RestPostCall, or within REST methods before parsing/serialization occurs.

Migration​

Code that relied on defaultRequestAttributes being automatically available in serializer/parser sessions must now explicitly pass them:

// Before (9.2.0 and earlier) - automatic merge
@Rest(defaultRequestAttributes={"key: value"})
public class MyResource {
@RestGet
public String get() {
// Serializer automatically received "key=value" in session properties
return null;
}
}

// After (10.0.0+) - explicit bridging
@Rest(defaultRequestAttributes={"key: value"})
public class MyResource {
@RestGet
public String get(RestRequest req) {
// Explicitly pass attributes to serializer if needed
req.setSerializerSessionProperties(req.getAttributes().asMap());
return null;
}
}

This separation provides clearer semantics: request attributes are metadata about the request, while session properties are configuration for parsers and serializers.

Session option allowlists and noInherit​

Serializer and parser session-option allowlists are built from annotation contributions, merged with validation in RestSessionOptionAllowlistMerge, and exposed lazily via Memoizer on RestContext and RestOpContext. Effective keys are returned as unmodifiable SortedSet<String> (natural order). RestContext.reset() clears cached allowlists and cascades reset() to operation contexts.

noInherit is an attribute on @Rest, @RestOp, and HTTP operation annotations (@RestGet, @RestPost, @RestPut, @RestPatch, @RestDelete, @RestOptions). It lists which inherited allowlist attributes should not be merged from less-derived declarations. Supported names include allowedParserOptions and allowedSerializerOptions (case-insensitive; SVL and comma-separated tokens are expanded like allowlist values). The standalone @NoInherit annotation, StringTokenSet, RestAllowedSerializerOptions, RestAllowedParserOptions, RestAllowedSessionOptionsMerge, and NoInheritUtils have been removed.

noInherit Inheritance Semantics​

noInherit on method-level @RestOp annotations controls class-level context inheritance only, not method-level annotation inheritance:

  • Class-level inheritance: When noInherit="allowedSerializerOptions" is set on a @RestOp annotation, the operation will not inherit allowedSerializerOptions from the enclosing @Rest class annotation
  • Method-level inheritance: allowedSerializerOptions from parent method @RestOp annotations (on overridden methods in parent classes) are always inherited, regardless of noInherit settings
@Rest(allowedSerializerOptions="classOpt")
public class Parent {
@RestGet(allowedSerializerOptions="parentMethodOpt")
public void get() {}
}

public class Child extends Parent {
// noInherit blocks "classOpt" from @Rest, but NOT "parentMethodOpt" from parent method
@RestGet(allowedSerializerOptions="childMethodOpt", noInherit="allowedSerializerOptions")
@Override
public void get() {}
// Effective allowlist: ["childMethodOpt", "parentMethodOpt"]
// Does NOT include "classOpt" due to noInherit
}

This distinction allows fine-grained control: you can block class-level options while still inheriting from overridden methods, which is typically the desired behavior for method-level configuration.

Programmatic RestContext.Builder / RestOpContext.Builder no longer expose public appendAllowed* or allowedParserOptions / allowedSerializerOptions list setters for these allowlists; framework code applies contributions through package-visible applyFramework*AllowlistContribution methods during annotation processing.

Swagger: serializer/parser session option parameters​

Generated Swagger (OpenAPI 2) for REST operations now includes optional documentation parameters for Juneau serializer/parser session options on the wire: headers X-Juneau-Serializer-Options and X-Juneau-Parser-Options (JSON5 object strings) and query parameters juneauSerializerOptions / juneauParserOptions (UON-encoded maps). Canonical names are the HEADER_* / QUERY_* fields on RestSharedConstants in juneau-rest-common. Existing explicit parameters with the same in + name are left unchanged.

RestContext.Builder and RestOpContext.Builder removed​

RestContext.Builder and RestOpContext.Builder are no longer part of the public API. All resource-level and operation-level configuration that was previously expressed through fluent builder calls now flows exclusively through @Rest(...) / @RestOp(...) annotation attributes or @Bean-annotated methods and fields.

Key changes:

  • RestContext.Builder — entirely removed from public surface; the framework no longer publishes a builder instance to @RestInit hooks or the resource constructor. Replace each builder.xxx(...) call with the equivalent @Rest(xxx=...) attribute or a @Bean(name="xxx")-annotated bean-supplier method.
  • RestOpContext.Builder — same removal. Replace builder.guards(...), builder.converters(...), etc. with @RestOp(guards=...), @RestOp(converters=...), etc.
  • RestContextInit record replaces the old RestContext.create(resourceClass, parent, cfg).init(supplier).path(p).children(c).build() factory chain. The six bootstrap fields (resource class, parent context, servlet config, resource supplier, path, children) plus an optional Consumer<BasicBeanStore> hook are bundled in one immutable record. The common top-level case becomes new RestContext(new RestContextInit(MyResource.class, () -> new MyResource())).
  • @RestInit(RestContext.Builder b) / @RestInit(RestOpContext.Builder b) injection hooks — removed. The supported @RestInit parameter shapes are now ServletConfig, ServletContext, the resource instance, @Bean-supplied beans, and zero-arg.
  • Annotation memoizers — every setting previously set by the builder is now computed lazily by a findXxx() method on RestContext / RestOpContext that walks the @Rest / @RestOp annotation chain, system properties, and the @Bean bean store. Results are cached in a Memoizer<T> and invalidated by RestContext.reset().

See the V10.0 Migration Guide for a per-setting replacement table.

Bean precedence: Spring > @Bean (inject) > default (breaking)​

The precedence order used by RestContext to resolve framework-managed beans (EncoderSet, SerializerSet, ParserSet, ThrownStore, Config, VarResolver, HttpPartSerializer, HttpPartParser, Messages, MethodExecStore, JsonSchemaGenerator, StaticFiles, SwaggerProvider, RestOperations, RestChildren, named HeaderList / NamedAttributeMap slots, etc.) has been flipped:

Tier9.4 and earlier10.0+
1 (highest)@Bean (inject) method on the resourceSpring @Bean (or any bean reachable through the bootstrap / overriding-parent bean store)
2Spring @Bean (via SpringBeanStore)@Bean (inject) method on the resource
3 (lowest)Memoizer-backed framework defaultMemoizer-backed framework default

@Bean (inject) is now documented as a programmable default, analogous to Spring's @ConditionalOnMissingBean: if a Spring bean of the same type (and name, where named) is available, it wins; otherwise the @Bean method runs and its result is cached. Resolution short-circuits on the first hit, so non-Spring deployments collapse to the familiar @Bean > default chain.

Mechanically, the change is implemented through three new affordances on BasicBeanStore:

  • An overriding parent bean store (constructor / Builder.overridingParent(...)) that is consulted before local entries during getBean(...) / getBeanSupplier(...). The Spring boot integration installs the application context bridge here.
  • addDefaultSupplier(Class, Supplier) (and a named variant) that registers a fallback supplier consulted after local entries and the regular parent. RestContext registers every memoizer-backed framework bean as a default supplier, so they only fire when nothing higher up in the chain has bound the type.
  • RestContext.getBootstrapBeanStore() continues to return a BasicBeanStore, but the resolution order on that store now reflects the new precedence model.

Side effects of the new model:

  • The legacy DELAYED_INJECTION / DELAYED_INJECTION_NAMES skip lists in RestContext are gone. The @Bean (inject) method walk now runs for every type with no hand-maintained filter; default-supplier presence is what auto-derives "skip this for now" behavior.
  • Inside each framework-bean memoizer body, redundant bs.getBean(X).ifPresent(impl::override) lookups have been removed. The bean store is the precedence engine — defaults no longer probe the store on the way out.
  • All RestContext.getX() accessor methods (getConfig(), getVarResolver(), etc.) now route through beanStore.getBean(X), so internal callers see Spring overrides without needing to hit the bean store directly.

Migration​

If you previously relied on @Bean (inject) overriding a Spring @Bean, you have a few options in 10.0+:

  • Preferred — let Spring win. Remove the @Bean (inject) method and lean on the Spring bean. The @Bean (inject) method was effectively a "default", which Spring already provides via @ConditionalOnMissingBean semantics on the bean factory.
  • Skip the Spring @Bean. Don't declare the type as a Spring @Bean and the @Bean (inject) method will continue to win over the framework default.
  • Use Spring-native overrides. Mark the relevant Spring bean with @Primary, @MockBean, or @ConditionalOnProperty so Spring itself picks the right candidate. The bean store will then surface that candidate to RestContext.
  • Programmatic registrations that flow through args.beanStoreConfigurer() (the Consumer<BasicBeanStore> hook on RestContextInit) land as regular entries — the same tier as @Bean (inject). They no longer beat Spring; if you need that, register the bean as a Spring @Bean (or contribute it through the overriding-parent layer in a custom BasicBeanStore subclass).

SpringBeanStore keeps backward-compatible behavior at the API level: as a BasicBeanStore subclass it picks up the new precedence model automatically when used through RestContext. Custom BasicBeanStore subclasses that want Spring-equivalent precedence can wire themselves in via Builder.overridingParent(...); the existing parent(...) slot continues to behave as a regular fallback (consulted after local entries).

RestResponse.setSerializer(Serializer)​

  • RestResponse.setSerializer(Serializer) — Forces the serializer used for the response body (via setContent(Object) or equivalent), bypassing Accept header negotiation. Pass null to clear the override. Content-Type is still set by the response processor from the serializer when appropriate; you can call setContentType(String) first if needed.

Download / Content-Disposition: attachment​

  • RestResponse.downloadAs(String filename) — Sets Content-Disposition: attachment with a quoted filename parameter so browsers typically download the response body (e.g. res.downloadAs("example.pdf").setContent(bytes)).
  • ContentDisposition.attachment(String) and HttpHeaders.contentDispositionAttachment(String) (juneau-rest-common) — Build a safe header value (escapes \ and " in the filename; rejects null/blank names and CR/LF). RFC 5987 filename* is not set; use a custom header value if you need full Unicode filenames.

RFC 7807 / 9457 Problem-Details server-side wiring​

Juneau REST resources can now emit application/problem+json (RFC 7807 / 9457) responses end-to-end. The wiring complements the juneau-bean-rfc7807 module shipped earlier in this release (the typed Problem bean) by adding a response processor, an opt-in annotation flag, a ProblemException throw seam, and a ProblemMapper SPI for custom exception-to-Problem translation.

Opt-in flag — @Rest(problemDetails) / per-op fanout​

  • @Rest(problemDetails) — String attribute, tri-state ("true" / "false" / "" inherits). When "true", uncaught BasicHttpExceptions on the resource are rendered as application/problem+json regardless of the client Accept header (RFC 7807 §3 — the spec encourages this on errors).
  • @RestGet(problemDetails) / @RestPost(problemDetails) / @RestPut(problemDetails) / @RestPatch(problemDetails) / @RestDelete(problemDetails) / @RestOptions(problemDetails) / @RestOp(problemDetails) — same tri-state attribute on every method-level annotation. Per-op "true" opts an op in on a non-opted-in resource; per-op "false" opts an op out of an opted-in resource; "" inherits the resource-level value.
@Rest(path="/orders", problemDetails="true")
public class OrderResource {

@RestGet("/{id}")
public Order get(@Path long id) {
throw new NotFound("Order {0} not found", id); // → 404 application/problem+json
}

@RestPost
public Problem create(Order in) {
if (in.balance < in.amount)
return Problem.fromStatus(403, "Insufficient credit", "Balance " + in.balance + " < amount " + in.amount)
.setType(URI.create("https://example.com/probs/out-of-credit"))
.set("balance", in.balance);
return null;
}
}
ProblemDetailsProcessor (response processor)​
  • org.apache.juneau.rest.server.processor.ProblemDetailsProcessor — slotted into the default responseProcessors chain after ThrowableProcessor. Handles four content shapes:
    • A returned Problem bean — serialized directly. Honors client Accept strictly.
    • A returned or thrown ProblemException — unwrapped via ProblemException.getProblem(). Honors Accept on non-opted-in ops (back-compat); flips to error-path semantics (ignores Accept) on opted-in ops.
    • A thrown BasicHttpException — gated by isProblemDetails() (per-op resolution); on error path ignores Accept and adapts via the registered ProblemMapper (if any matches) or ProblemAdapters.fromException(...) otherwise.
    • Any other thrown Throwable — only when an explicit ProblemMapper matches its class hierarchy. Mappers that return null are skipped and the chain continues; if no mapper matches, the processor returns NEXT.
  • Status precedence — when Problem.getStatus() is non-null, the processor calls RestResponse.setStatus(int) with that value. Otherwise it leaves the existing status alone (RestSession.run() normalizes 0 → 200 so per-op default codes still flow through).
  • type field policy — serialized as-is. A null type is omitted from the JSON output rather than synthesized as "type":"about:blank" (preserves the absent-vs-explicit distinction).
ProblemException throw seam​
  • org.apache.juneau.bean.rfc7807.ProblemException — RuntimeException carrying a Problem payload. Lets handlers throw a custom problem without manually building a BasicHttpException:
throw new ProblemException(
Problem.fromStatus(403, "Insufficient credit", "Balance 30 < cost 50")
.setType(URI.create("https://example.com/probs/out-of-credit"))
.set("balance", 30)
.set("accounts", List.of("/account/12345", "/account/67890")));

Lives in juneau-bean-rfc7807 (the bean module stays free of any juneau-rest-server dep). The processor unwraps it via instanceof ProblemException.

ProblemAdapters static helper​
  • org.apache.juneau.bean.rfc7807.adapter.ProblemAdapters#fromException(BasicHttpException) — lives in juneau-rest-common. Adapts a BasicHttpException (status code, reason phrase, getMessage()) into a Problem bean. Used by the processor's default error-path fallback when no ProblemMapper matches. Available for direct caller use as well.
ProblemMapper SPI — declarative exception translation​
  • org.apache.juneau.bean.rfc7807.ProblemMapper<T extends Throwable> — pluggable interface lets users map arbitrary throwables into custom Problem shapes without writing a processor. Two methods: Class<T> getExceptionType() (used for hierarchy-aware dispatch) and Problem map(T exception). Returning null defers to the next-most-specific mapper, and ultimately to the built-in ProblemAdapters.fromException(...) fallback for BasicHttpException subclasses.
  • org.apache.juneau.bean.rfc7807.ProblemMapperList — aggregator bean for registering more than one mapper on the same resource. The Juneau @Bean walk pairs each @Bean factory with its declared return type, so multiple @Bean public ProblemMapper foo() methods on one class collapse onto the single ProblemMapper.class slot; wrap them in a ProblemMapperList to keep them all reachable. The single-mapper case (@Bean public ProblemMapper<MyException> mapper()) keeps working unchanged via BeanStore.getBean(ProblemMapper.class).
public class InsufficientCreditMapper implements ProblemMapper<InsufficientCreditException> {
public Class<InsufficientCreditException> getExceptionType() { return InsufficientCreditException.class; }
public Problem map(InsufficientCreditException e) {
return Problem.fromStatus(403, "Insufficient credit", e.getMessage())
.setType(URI.create("https://example.com/probs/out-of-credit"))
.set("balance", e.getBalance())
.set("cost", e.getCost());
}
}

@Rest(problemDetails="true")
public class AccountResource {
// Single-mapper case.
@Bean public ProblemMapper<InsufficientCreditException> creditMapper() { return new InsufficientCreditMapper(); }

// Multi-mapper case.
@Bean public ProblemMapperList problemMappers() {
return ProblemMapperList.of(new InsufficientCreditMapper(), new OrderNotFoundMapper());
}
}
ProblemLocalizationStrategy — future-work seam​
  • org.apache.juneau.bean.rfc7807.ProblemLocalizationStrategy — functional-interface SPI seam for locale-aware translation of Problem.title / Problem.detail. The default in-tree implementation is ProblemLocalizationStrategy.IDENTITY (pass-through); the processor consults the bean store for a registered strategy on every emission and falls back to IDENTITY when none is present, so the seam is invisible at zero cost on the hot path until a deliberate strategy is contributed.
  • A reference Messages-driven (resource-bundle) implementation is intentionally out of scope for this release. The IDENTITY default keeps the call-site contract stable so a future implementation can be dropped in without changing the processor.
Accept negotiation policy​
  • Error path (thrown BasicHttpException / thrown ProblemException / mapped Throwable on opted-in ops): emits application/problem+json regardless of the client Accept — the spec encourages this on errors.
  • Success path (returned Problem / returned ProblemException): honors the client Accept strictly. The processor only emits application/problem+json if Accept matches it (or */*); otherwise it passes through to the next processor in the chain unchanged.

See REST Server — RFC 7807 Problem Details for the full topic, including worked examples for the resource-level opt-in, the per-op fanout, the ProblemException throw path, and ProblemMapper registration.

Jakarta Bean Validation 3.x integration​

Validation is opt-in and disabled by default. This is a non-negotiable contract: a fresh RestContext built with no validation-related markers anywhere on the resource never instantiates a Validator, never invokes a constraint check, and never adds a per-request cost — even when jakarta.validation-api and a concrete provider (e.g. Hibernate Validator) are on the classpath. Spring devs migrating in should be aware that, unlike Spring MVC where @Valid is automatic, Juneau requires an explicit marker on the parameter being validated.

Opt-in marker​

  • @jakarta.validation.Valid on a @Content / @FormData / @Request-bound parameter — turns on validation for that one parameter on that one handler. Per-parameter granularity: marking one parameter never opts in a sibling parameter, an unrelated handler, or the resource as a whole. Spring's @org.springframework.validation.annotation.Validated and the legacy @javax.validation.Valid are also recognized via fully-qualified-name (FQN) detection, so no compile-time dependency on those packages is required.
@RestPost("/orders")
public Order create(@Content @Valid OrderRequest in) {
// If any of OrderRequest's @NotBlank / @Size / @Min / etc. constraints fail,
// the handler is never called. Response is 400 with the violations.
return orderService.create(in);
}
Failure response shape​
  • Without @Rest(problemDetails="true") (or when problem-details is off for the op): 400 Bad Request with application/json body of the form { "status": 400, "errors": [ { "path": "...", "message": "...", "constraint": "..." }, ... ] }. The shape mirrors Spring's MethodArgumentNotValidException payload at the top level for migration friendliness.
  • With @Rest(problemDetails="true") (the RFC 7807 / 9457 path documented above): 400 application/problem+json with the standard problem fields (status, title, detail) plus an errors[] extension carrying the same ValidationViolation list.

The invalidValue field of each violation is omitted by default for privacy reasons. Handlers that need to surface the offending value (e.g. an internal debugging endpoint) can include it explicitly by constructing the violation with the invalidValue setter; the renderer honors a non-null value verbatim.

Dependency stance — optional, provided scope​

juneau-rest-server's pom.xml declares jakarta.validation-api:3.0.2 in provided scope. Consumers who never write @Valid pay no transitive cost — no Jakarta Validation, no Hibernate Validator, no Jakarta EL pulled in automatically. Consumers who do opt in supply the runtime provider on their own classpath:

<!-- in the consumer's pom.xml -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.3.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
<version>5.0.0</version>
</dependency>
Graceful degradation​

If a parameter carries @Valid but no Jakarta Validation provider is reachable at runtime (the API JAR is on the classpath but the engine isn't), the integration silently skips validation and emits a one-shot WARNING log per JVM:

Jakarta Bean Validation was requested via @Valid on a REST argument, but no
jakarta.validation.Validator bean is registered and no runtime provider
(e.g. org.hibernate.validator:hibernate-validator + jakarta.el implementation)
is on the classpath. Validation will be silently skipped for this and
subsequent requests. Add a provider dependency or register a Validator bean
to enable validation.

The request continues through the handler with the bean unmodified — the off-by-default contract is preserved even in misconfigured deployments. This matches the existing FQN-detection precedent: feature presence is signaled by the consumer (here via @Valid) but the engine is a runtime resolution that fails open, not a startup-time hard requirement.

Validator resolution order​

The integration consults, in order:

  1. A user-supplied jakarta.validation.Validator bean visible from the resource's BeanStore (typically contributed via @Bean public Validator validator() { ... } on the resource). Use this when the deployment needs custom message interpolators, group sequences, or a non-default ConstraintValidatorFactory.
  2. A lazily-built JVM-wide default obtained from Validation.buildDefaultValidatorFactory(). Cached after the first successful build so the factory cost is paid at most once per JVM.
  3. null — the graceful-degradation path described above.
New API surface​
  • org.apache.juneau.rest.validation.ValidationException — BadRequest (400) subclass carrying an immutable List<ValidationViolation>. Thrown from ContentArg / FormDataArg / RequestBeanArg after the bean is bound but before the handler is called.
  • org.apache.juneau.rest.validation.ValidationViolation — POJO with path, message, constraint, and optional invalidValue fields. Serializable to JSON via Juneau's default serializers.
  • org.apache.juneau.rest.validation.BeanValidator — static dispatcher that performs FQN-based opt-in detection and runs the validator. Acts as the seam between the arg resolvers and the underlying jakarta.validation.Validator.
What's out of scope (v1)​
  • Method-level validation (@Validated on the resource class, constraints on @RestOp return values). The integration validates parameter beans only.
  • Schema-driven validation (JSON-Schema constraints on the request body). That lives in JsonSchemaGenerator territory.
  • Messages.properties bridge between Juneau's Messages and Jakarta's ValidationMessages.properties. The two bundles don't compose automatically; document and configure separately.

See REST Server — Jakarta Bean Validation for the full topic, including worked examples for the opt-in marker, the problem-details and non-problem-details response shapes, custom Validator registration, custom Constraint annotations, and the graceful-degradation contract.

Conditional-GET / ETag helpers​

juneau-rest-server now ships an RFC 7232 conditional-request layer so handlers no longer need to hand-roll ETag / Last-Modified / If-None-Match / If-Match plumbing. The wiring is purely additive — existing handlers continue to work unchanged.

Response-side setters on RestResponse​

Six fluent setters on RestResponse:

  • eTag(String) / eTag(EntityTag) — sets the ETag response header. The string form delegates to EntityTag.of(...) so quoting (strong: "v1"; weak: W/"v1") is validated up front.
  • lastModified(Instant) / lastModified(ZonedDateTime) — sets the Last-Modified response header in RFC 7231 IMF-fixdate format. Values are truncated to second precision and converted to UTC.
  • cacheControl(String) / cacheControl(CacheControl.Builder) — sets the Cache-Control response header. The builder form gives a typed API for the standard directives.
Typed Cache-Control builder​

New CacheControl.Builder in juneau-rest-common offers a typo-resistant fluent API over the directive set: publicCache() / privateCache(), noCache() / noStore() / noTransform(), mustRevalidate() / proxyRevalidate(), immutable(), maxAge(long) / maxAge(Duration), sMaxAge(...), staleWhileRevalidate(...), staleIfError(...), and extension(String) for forward-compat directives. Directives are emitted in a stable order (cacheability → boolean directives → numeric directives → extensions) for snapshot-friendly testing.

Request-side checkPreconditions(RestResponse)​

New RestRequest.checkPreconditions(RestResponse) evaluates the four conditional request headers against the ETag / Last-Modified already set on the response, and returns an Optional<BasicHttpException>:

  • Empty → no precondition matched and the handler may proceed.
  • Non-empty → the contained exception (a BasicHttpException carrying 304 Not Modified or 412 Precondition Failed) should be thrown to short-circuit.

The implementation follows RFC 7232 §6 ordering exactly: If-Match → If-Unmodified-Since → If-None-Match → If-Modified-Since. ETag matching honors RFC 7232 §2.3.2 (strong comparison for If-Match; weak comparison for If-None-Match). If-Modified-Since is only consulted on GET / HEAD. Malformed dates are silently ignored per Postel's law.

Typical handler shape​
@RestGet("/orders/{id}")
public Order get(@Path long id, RestRequest req, RestResponse res) {
var order = repo.find(id);
res.eTag("\"" + order.version() + "\"")
.lastModified(order.updated())
.cacheControl(CacheControl.create().publicCache().maxAge(60).build());
req.checkPreconditions(res).ifPresent(e -> { throw e; });
return order;
}
Notes​
  • The response's ETag / Last-Modified headers must be set before checkPreconditions(...) is called — the check reads from the already-set response headers so it can compare against the values the handler intends to send.
  • If-Match always uses strong comparison: a W/"v1" response ETag never matches an If-Match, even if the opaque tags are identical.
  • If-None-Match uses weak comparison: "v1" and W/"v1" both match. The wildcard * matches when any ETag is set on the response.

See REST Server — Conditional-GET / ETag Helpers for the full topic, including the 16-cell precedence matrix, weak/strong ETag rules, and a worked round-trip example.

Standalone servlet companions for dual-use resources​

The bundled Basic*Resource mixins are designed for composition into a host via @Rest(mixins=...), where each pins its operations at an op-level sub-path (e.g. @RestGet(path="/jsp/*")). That shape is awkward when a resource needs to be deployed on its own as a top-level servlet: mounting a mixin-shaped resource at its own path doubles the prefix (/jsp/jsp/...).

This release adds thin standalone Basic*Servlet companions for the dual-use resources. Each companion declares its mount at the class level via @Rest(paths="/<token>/*") (sibling top-level registration) and pins its op at /*, so it serves at a clean sibling path (e.g. JSPs at /jsp/*) without prefix-doubling. The mixin Basic*Resource forms are unchanged and keep their op-pinned sub-path behavior.

Standalone servletMounts atDelegates to (mixin)
JspServlet/jsp/*JspMixin
FreemarkerServlet/freemarker/*FreemarkerMixin
MustacheServlet/mustache/*MustacheMixin
ThymeleafServlet/thymeleaf/*ThymeleafMixin
StaticFilesServlet/static/*StaticFilesMixin
VersionServlet/version/*VersionMixin
AdminServlet/admin/*AdminMixin
  • Shared logic, no drift. The four view servlets share an abstract base, ViewServlet (in org.apache.juneau.rest.server.view), whose single @RestGet(path="/*") op delegates to a new engine-agnostic RawTemplateDispatcher interface. The four Basic*Resource view mixins implement RawTemplateDispatcher, so the standalone and mixin forms route raw .jsp / .ftlh / .mustache / .html dispatch through the same code and cannot diverge. The non-view companions (StaticFilesServlet, VersionServlet, AdminServlet) hold their matching Basic*Resource as a delegate and forward each op to it.
  • AdminServlet carries the same @Rest(guards=DenyAllGuard.class) default-deny posture as the mixin — every admin path returns 403 until an auth RestGuardList is registered.
  • HealthMixin is intentionally not given a companion: it already extends BasicRestServlet (it is itself a standalone-capable servlet) and mounts at fixed absolute paths (/healthz, /readyz, /livez) that do not suffer the prefix-doubling problem.

See REST Server — Standalone vs Mixin Resources for the full convention.

Servlet self-registration (top-level paths)​

Three mechanisms register a servlet at the top-level paths it self-declares (@Rest(paths) / getPaths()), all reading the same resolution chain via RestContext.resolveTopLevelPaths(Class, Object, BeanStore) — the same resolver the Jetty microservice auto-discovery loop already uses, so mount paths are identical across runtimes:

  • Non-Spring (web.xml / external WAR): a jakarta.servlet.ServletContainerInitializer, JuneauRestServletContainerInitializer (in juneau-rest-server), auto-mounts concrete @Rest-annotated RestServlet subclasses that self-declare top-level paths. It is opt-in via the juneau.rest.auto-register=true context init parameter, so it never collides with servlets declared manually in web.xml.

See REST Server — Self-Registration for the full topic.

juneau-rest-server-springboot​

Servlet self-registration (Spring Boot)​

Two Spring Boot mechanisms register a Juneau servlet at the top-level paths it self-declares (@Rest(paths) / getPaths()), both deriving the mapping from RestContext.resolveTopLevelPaths(Class, Object, BeanStore) — no hard-coded path strings:

  • JuneauRestAutoConfiguration — an opt-in @AutoConfiguration (registered via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) gated behind the property juneau.rest.auto-register=true (matchIfMissing=false). When active, it contributes a single ServletContextInitializer that walks every RestServlet bean and mounts each at its self-declared path, skipping any servlet instance already wired into a manual ServletRegistrationBean (explicit registrations win) and any servlet that resolves to no top-level paths.
  • JuneauServletRegistrations.forServlet(servlet, beanStore) — an always-available factory helper that returns a ServletRegistrationBean whose URL mappings are derived from the servlet's own @Rest(paths) (or getPaths()). Use it from an explicit @Bean method when you want the mount path to come from the servlet rather than a hard-coded string. A servlet that resolves to no top-level paths is rejected with an IllegalArgumentException.

See REST Server — Self-Registration for the full topic.

Spring Environment bridge — @Value reads application.yaml​

  • Added org.apache.juneau.rest.server.springboot.SpringEnvironmentPropertySource — a PropertySource that reads from a Spring Environment so @Value("${spring.app.key}") injections on Juneau-instantiated beans resolve against the same property sources Spring Boot itself uses (application.yaml / application.properties, command-line args, env vars, @PropertySource, etc.).
  • SpringBeanStore's constructor now auto-registers a lazy bridge against the ApplicationContext's Environment: one per context, however many Spring REST servlets share it. The bridge is removed when the context closes (ConfigurableApplicationContext) or when every SpringBeanStore on the context has been clear()ed. It is resolved lazily — the constructor doesn't actually call appContext.getEnvironment() until the first lookup — so callers that mock the ApplicationContext without stubbing getEnvironment() (e.g. unit tests that only exercise bean-lookup paths) are unaffected.
  • The bridge caches keys that Spring does not have and that are not valid Spring Boot property names (Juneau's mixed-case builder keys), so building serializers and parsers in a Spring Boot app no longer walks every Spring property source per key. Source-list changes and system properties are re-checked on every lookup; after an in-place edit of a property source's contents, call SpringBeanStore.invalidateEnvironmentCache(...), or set juneau.spring.missCache.disable=true to turn the cache off.
  • The bridge sits above the classpath-default juneau.cfg bridge in the Settings source stack (later insertion wins), so application.yaml shadows juneau.cfg when both define the same key — matching the typical Spring Boot precedence intuition.

juneau-rest-server-auth-jwt (new module)​

jwksCacheTtl migrated to @Value​

  • JwtTokenValidator.Builder now resolves its default jwksCacheTtl (5 minutes) through @Value("${juneau.jwt.jwksCacheTtl:PT5M}") Duration jwksCacheTtl instead of a hand-rolled initializer. The static factory now routes through BeanInstantiator, allowing the value to flow through the standard Settings precedence chain (system property → env var → registered PropertySource → Settings.setGlobal). Behavior is identical when no override is set. See @Value Framework-Internal Adoption.

A new opt-in REST module, juneau-rest-server-auth-jwt, adds JWT bearer-token verification to juneau-rest-server without bleeding the nimbus-jose-jwt dependency into the core. The nimbus dep is declared in provided scope on the module's POM, so consumers explicitly pick the nimbus version they want.

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i nimbus returns nothing — the containment requirement is verified at build time. See REST Server — AuthN Guards § JWT verification for the full reference.

New Classes​

  • org.apache.juneau.rest.server.auth.jwt.JwtTokenValidator — TokenValidator that fetches keys from a JWKS endpoint, verifies signatures, and enforces iss / aud / exp / nbf with secure defaults. Fluent builder: jwksUrl(URI) (or jwkSource(JWKSource)), issuer(String), audience(String), algorithms(JWSAlgorithm...) (default [RS256, ES256]), clockSkew(Duration) (default 60s, capped at 5m), jwksCacheTtl(Duration) (default 5m), clock(Clock) (test-time injection).
  • org.apache.juneau.rest.server.auth.jwt.JwksCache (package-private) — Single-slot TTL cache wrapping any JWKSource<SecurityContext>. Continues serving the last-known-good key set when the upstream fetch fails past TTL, logging a WARNING so transient JWKS-endpoint outages don't cascade into correlated auth outages.

JWKS-on-kid-miss eager refresh (behavior change)​

JwksCache now performs a single eager JWKS refresh when a fresh-cache key selection returns no matching keys. This eliminates the rotation window where an IdP publishes a new signing key mid-TTL and clients receive spurious 401 responses for perfectly valid tokens (up to the full cache TTL — 5 minutes by default — under the old behavior).

The refresh is bounded by two guards to prevent abuse:

  • Per-cache cooldown (default 10 seconds, @Value key juneau.jwt.jwksEagerRefreshCooldown) — at most one eager refresh fires per cooldown window regardless of how many concurrent kid-miss requests arrive.
  • Single in-flight CompletableFuture — concurrent callers experiencing the same miss wait for (and share) the result of the single in-flight fetch; no thundering herd.

On JWKS-endpoint outage during an eager refresh, the last-known-good key set is retained (valid tokens continue to verify) and the cooldown is still advanced so a down endpoint is not hammered.

This feature is enabled by default (behavior change: fewer spurious 401s during key rotation). To opt out and restore pre-10.0.0 behavior:

JwtTokenValidator.create()
.jwksEagerRefreshOnKidMiss(false) // or -Djuneau.jwt.jwksEagerRefreshOnKidMiss=false
...
.build();

New builder setters and @Value keys:

Builder setter@Value keyDefaultCap / validation
jwksEagerRefreshOnKidMiss(boolean)juneau.jwt.jwksEagerRefreshOnKidMisstrue—
jwksEagerRefreshCooldown(Duration)juneau.jwt.jwksEagerRefreshCooldownPT10Spositive; ≤ jwksCacheTtl; ≤ PT60S

Applies only to jwksUrl(...)-backed caches. A caller-supplied jwkSource(...) is unaffected.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-jwt</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.3</version> <!-- consumer-supplied; provided scope on juneau-rest-server-auth-jwt -->
</dependency>

juneau-rest-server-auth-saml (new module)​

A new opt-in REST module, juneau-rest-server-auth-saml, adds SAML 2.0 single-sign-on (Web Browser SSO Profile) to juneau-rest-server by wrapping OpenSAML 5.x behind the earlier change AuthFilter / TokenValidator SPIs. The org.opensaml:opensaml-* dependency cluster is declared in provided scope on the module's POM, so consumers explicitly pick the OpenSAML 5.x patch they want (default pin: 5.2.2).

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i opensaml returns nothing — the containment requirement is verified at build time. See REST Server — SAML AuthN Support for the full reference.

New Classes​

  • org.apache.juneau.rest.server.auth.saml.SamlAssertionValidator — Validates a SAML 2.0 <samlp:Response> document and returns a ClaimsPrincipal marked with issuerType=SAML. Builder: spEntityId(String), expectedIssuer(String), metadataResolver(MetadataResolver) or signingCredential(Credential) (mutually exclusive), decryptionCredential(Credential) (opt-in for encrypted assertions), algorithms(String...) (default [rsa-sha256, ecdsa-sha256]; SHA-1 permanently rejected), clockSkew(Duration) (default 60s, capped at 5m), clock(Clock).
  • org.apache.juneau.rest.server.auth.saml.SamlAuthFilter — AuthFilter that decodes the inbound SAMLResponse (base64 + DEFLATE for the Redirect binding) at a configurable consumer path and delegates validation. Builder: validator(SamlAssertionValidator), binding(SamlBinding) (default POST), consumerPath(String) (default /saml/acs), rolesClaim(String) (default "roles"), realm(String) (default "saml").
  • org.apache.juneau.rest.server.auth.saml.SamlBinding — Enum of supported HTTP bindings (POST, REDIRECT). Artifact + SOAP bindings deferred to a future iteration.
  • org.apache.juneau.rest.server.auth.saml.SamlMetadataResolvers — Convenience factories file(File|Path) and url(String) that produce initialized MetadataResolver instances ready to hand to SamlAssertionValidator.Builder.metadataResolver(...). No bundled default singleton.

Marker claim​

The returned ClaimsPrincipal carries issuerType=SAML, so downstream code can distinguish SAML-derived principals from JWT-derived principals without a subclass:

if ("SAML".equals(principal.getClaim("issuerType", String.class).orElse(null))) {
// SAML-issued principal
}

Maven note​

OpenSAML 5.x is published to the Shibboleth Maven repository rather than Maven Central. The juneau-rest-server-auth-saml POM declares that repository locally so consumers don't need to configure it in their own settings. Pin the version via the <opensaml.version> POM property (default 5.2.2; do not use 5.0.0).

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-saml</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>5.2.2</version> <!-- consumer-supplied; provided scope on juneau-rest-server-auth-saml -->
</dependency>

juneau-rest-server-auth-oauth (new module)​

A new opt-in REST module, juneau-rest-server-auth-oauth, adds OAuth 2.0 / OIDC bearer-token validation, RFC 7662 token introspection, RFC 6749 client-side grant flows, and OIDC discovery to juneau-rest-server by wrapping the Nimbus OAuth 2.0 SDK (com.nimbusds:oauth2-oidc-sdk) behind the earlier change AuthFilter / TokenValidator SPIs. Same vendor as the nimbus-jose-jwt already used by juneau-rest-server-auth-jwt — the Nimbus SDK pulls nimbus-jose-jwt transitively. The oauth2-oidc-sdk dependency is declared in provided scope on the module's POM, so consumers explicitly pick the SDK version they want (default pin: 11.37.2).

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -iE "(nimbusds|oauth2-oidc)" returns nothing — the containment requirement is verified at build time. See REST Server — OAuth AuthN Support for the full reference.

New Classes​

  • org.apache.juneau.rest.server.auth.oauth.OAuthIntrospectionValidator — TokenValidator that validates opaque OAuth 2.0 tokens via RFC 7662 introspection. Wraps Nimbus's TokenIntrospectionRequest + TokenIntrospectionResponse. Builder: introspectionEndpoint(URI), clientId(String), clientSecret(String) / clientSecretSupplier(Supplier<String>), requiredScopes(String...), tokenCache(TokenCache) (default BoundedLruTokenCache), cacheTtl(Duration) (default 5m, capped at 1h), httpRequestConfigurator(Consumer<HTTPRequest>), clock(Clock).
  • org.apache.juneau.rest.server.auth.oauth.OAuthFilter — AuthFilter that authenticates RFC 6750 Bearer tokens, delegating to any TokenValidator (e.g. OAuthIntrospectionValidator for opaque tokens or JwtTokenValidator from juneau-rest-server-auth-jwt for JWT access tokens). Extracts roles from a configurable claim (default "scope", split on whitespace per RFC 6749 §3.3).
  • org.apache.juneau.rest.server.auth.oauth.OAuthToken — Immutable record returned by every flow helper on a successful token acquisition.
  • org.apache.juneau.rest.server.auth.oauth.TokenCache + BoundedLruTokenCache — SPI for caching principals + tokens; default impl is a thread-safe bounded LRU (1000 entries, per-entry TTL).
  • org.apache.juneau.rest.server.auth.oauth.flow.OAuthClientCredentialsFlow — Wraps Nimbus's ClientCredentialsGrant (RFC 6749 §4.4) with optional TokenCache reuse keyed by (clientId, scope).
  • org.apache.juneau.rest.server.auth.oauth.flow.OAuthAuthorizationCodeFlow — Wraps Nimbus's AuthorizationRequest + AuthorizationCodeGrant + TokenRequest (RFC 6749 §4.1) with mandatory PKCE per RFC 7636 (S256). Exposes buildAuthorizationUrl(state, codeChallenge) and exchange(code, codeVerifier).
  • org.apache.juneau.rest.server.auth.oauth.flow.OAuthRefreshTokenFlow — Wraps Nimbus's RefreshTokenGrant (RFC 6749 §6).
  • org.apache.juneau.rest.server.auth.oauth.flow.OAuthResourceOwnerFlow — Wraps Nimbus's ResourceOwnerPasswordCredentialsGrant (RFC 6749 §4.3). Filed @Deprecated(since = "10.0.0") from day-1 — the resource-owner password-credentials grant was removed from OAuth 2.1 due to long-standing security concerns; prefer OAuthAuthorizationCodeFlow (with PKCE).
  • org.apache.juneau.rest.server.auth.oauth.oidc.OidcDiscoveryClient + OidcMetadata — OIDC .well-known/openid-configuration discovery via Nimbus's OIDCProviderMetadata.resolve(Issuer). Returns a Juneau-native immutable record.

Deferred​

  • OIDC Relying Party login flow — now shipped as the juneau-rest-server-auth-oidc-rp module (see below); the discovery client + auth-code helper here are the building blocks it composes on top of.
  • Device-code grant (RFC 8628) — deferred to a follow-on if/when needed.
  • JWKS-on-kid-miss eager refresh — now shipped; see the juneau-rest-server-auth-jwt section below.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-oauth</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.37.2</version> <!-- consumer-supplied; provided scope on juneau-rest-server-auth-oauth -->
</dependency>

juneau-rest-server-auth-oidc-rp (new module)​

A new opt-in REST module, juneau-rest-server-auth-oidc-rp, turns a Juneau REST server into a complete OpenID Connect Relying Party — the interactive "Log in with Google / Okta / Entra ID / Keycloak" browser flow — on top of juneau-rest-server-auth-oauth. It orchestrates the authorization redirect → callback → session dance (single-use state/nonce, PKCE S256, strict ID-token validation), wrapping the Nimbus OAuth 2.0 SDK (com.nimbusds:oauth2-oidc-sdk) behind Juneau facades. The SDK is declared in provided scope on the module's POM (default pin: 11.37.2, the same line as juneau-rest-server-auth-oauth).

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -iE "(nimbusds|oauth2-oidc)" returns nothing — the containment requirement is verified at build time. See REST Server — OIDC Relying Party Login for the full reference.

New Classes​

  • org.apache.juneau.rest.server.auth.oidc.rp.OidcRelyingParty — Builder + facade the application's @Rest resource calls: startLogin (generate + store state/nonce/PKCE verifier, 302 → IdP), completeLogin (parse callback, single-use-consume state, exchange code, validate ID token, create session, 302 → app), logout (invalidate session + clear cookie + redirect through the IdP end_session_endpoint), refresh (rotating-refresh-token grant), and backChannelLogout (IdP-pushed logout_token → server-side session revocation). Single-IdP per instance. Nimbus is hidden behind the facade; escape hatches are httpRequestConfigurator(Consumer<HTTPRequest>) and authenticationRequestCustomizer(Consumer<AuthenticationRequest.Builder>).
  • org.apache.juneau.rest.server.auth.oidc.rp.SessionStore + OidcSession — Pluggable session-persistence SPI and the immutable session record (principal + roles + tokens + sub/sid + expiry). The SPI exposes supportsServerSideRevocation() / invalidateBySubject(String) / invalidateBySessionId(String) so a server-side-indexed store can satisfy back-channel logout.
  • org.apache.juneau.rest.server.auth.oidc.rp.SignedCookieSessionStore — The documented default. Stateless HMAC-signed (HS256) compact-JWT cookie carrying a minimal claim set (never the raw tokens); scales horizontally and survives restart, size-capped (~4 KB), not server-revocable.
  • org.apache.juneau.rest.server.auth.oidc.rp.InMemorySessionStore — Single-instance / dev option; bounded-LRU map indexed by sub and sid, so it is the bundled store that supports back-channel logout.
  • org.apache.juneau.rest.server.auth.oidc.rp.EphemeralStore — Single-use, TTL-bounded (default 5 min) state → (nonce, codeVerifier, redirectTarget) store with lazy + LRU sweep (the BoundedLruTokenCache eviction shape).
  • org.apache.juneau.rest.server.auth.oidc.rp.OidcSessionAuthFilter — AuthFilter that resolves the session cookie into a ClaimsPrincipal on each request; fail-open to unauthenticated. Roles come from a configurable claim (default "scope"), matching OAuthFilter's convention.
  • org.apache.juneau.rest.server.auth.oidc.rp.IdTokenValidatorAdapter — Wraps Nimbus's IDTokenValidator for full OIDC ID-token validation (signature vs JWKS, iss, aud/azp, exp/iat, nonce) with a strict algorithm allowlist (default [RS256, ES256]; none rejected) and configurable JWKS source (URI / JWKSet / JWKSource).

Building-block extension (juneau-rest-server-auth-oauth)​

  • OAuthAuthorizationCodeFlow.buildAuthenticationUrl(state, codeChallenge, nonce, customizer) — new method emitting an OIDC AuthenticationRequest (carrying the nonce) alongside the existing plain-OAuth buildAuthorizationUrl(...). Required so the relying party can bind a nonce into the authorization request and verify it on the returned ID token.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-auth-oidc-rp</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.37.2</version> <!-- consumer-supplied; provided scope on juneau-rest-server-auth-oidc-rp -->
</dependency>

juneau-rest-server-metrics-micrometer (new module)​

A new opt-in REST module, juneau-rest-server-metrics-micrometer, bridges the new MetricsRecorder SPI (see juneau-rest-server) into a Micrometer MeterRegistry so a Juneau REST service can drop into existing Prometheus / StatsD / JMX scrape pipelines with no hand-rolled instrumentation. Engine-agnostic POM stance: io.micrometer:micrometer-core is declared in provided scope on the module's POM, so consumers explicitly pick the Micrometer version they want.

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i micrometer returns nothing — the containment requirement is verified at build time.

New Classes​

  • org.apache.juneau.rest.metrics.micrometer.MicrometerMetricsRecorder — MetricsRecorder implementation. Builds a Micrometer Timer named http.server.requests (overridable via the two-arg constructor) with four tags per Spring Boot's WebMvcMetricsFilter convention: method (uppercased HTTP method), uri (the @RestOp path template — bounded cardinality), status (HTTP response status), exception (the thrown exception's simple-name, or None on the happy path). Records the elapsed Duration once per call.

Naming convention​

Spring Boot's http.server.requests + {method, uri, status, exception} was chosen over the OTel-native http.server.duration shape because the wider Prometheus / Grafana scrape-config and dashboard ecosystem already standardizes on it. The OTel-native attribute names live in the sibling juneau-rest-server-tracing-otel module (see below).

Wiring​

@Configuration
public class ObservabilityConfig {
@Bean MeterRegistry registry() { return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); }
@Bean MetricsRecorder recorder(MeterRegistry r) { return new MicrometerMetricsRecorder(r); }
}

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-metrics-micrometer</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.13.6</version> <!-- consumer-supplied; provided scope on juneau-rest-server-metrics-micrometer -->
</dependency>

See REST Server — Observability (Micrometer + OpenTelemetry) for the full topic.

juneau-rest-server-tracing-otel (new module)​

A new opt-in REST module, juneau-rest-server-tracing-otel, bridges the new TracerHook SPI (see juneau-rest-server) into an OpenTelemetry Tracer so each @RestOp invocation becomes a server span with HTTP semantic-convention attributes and W3C trace-context propagation. Engine-agnostic POM stance: io.opentelemetry:opentelemetry-api is declared in provided scope on the module's POM, so consumers explicitly pick the OpenTelemetry version they want.

mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i opentelemetry returns nothing — the containment requirement is verified at build time.

New Classes​

  • org.apache.juneau.rest.server.tracing.otel.OtelTracerHook — TracerHook implementation. Each request becomes a single SpanKind.SERVER span named after the HTTP method (GET, POST, …) per the OpenTelemetry HTTP semantic conventions, with attributes http.request.method, http.response.status_code, and http.route (the @RestOp path template, bounded cardinality). On the exception path, the throwable is recorded via Span.recordException(...), the span status is set to StatusCode.ERROR, and an exception.type attribute carries the exception's simple-name. Constructors: no-arg (uses GlobalOpenTelemetry.get() per an earlier change resolved decision #5), OtelTracerHook(OpenTelemetry), and OtelTracerHook(Tracer, TextMapPropagator) for tests.
  • org.apache.juneau.rest.server.tracing.otel.RestRequestTextMapGetter — TextMapGetter<RestRequest> for OpenTelemetry context extraction. Reads the standard W3C traceparent / tracestate headers (and any other propagator-defined headers, e.g. baggage) from the incoming request so the server span is created as a child of the caller's trace.

Attribute naming convention​

OpenTelemetry HTTP semantic conventions (http.request.method, http.response.status_code, http.route) were chosen for the tracer hook to keep the spans interoperable with any OTel collector / backend. The Spring-Boot-style http.server.requests metric shape lives in the sibling juneau-rest-server-metrics-micrometer module (see above).

Wiring​

@Configuration
public class ObservabilityConfig {

// Option A: rely on the JVM-wide GlobalOpenTelemetry (recommended).
@Bean TracerHook tracer() { return new OtelTracerHook(); }

// Option B: pass a specific OpenTelemetry instance (e.g. for tests).
@Bean TracerHook tracer(OpenTelemetry otel) { return new OtelTracerHook(otel); }
}

W3C trace-context propagation​

Incoming traceparent / tracestate request headers are extracted via the configured TextMapPropagator (default: W3CTraceContextPropagator) so the server span continues a caller-supplied distributed trace. Downstream HTTP calls made from inside the handler pick up the active span's context automatically when the outbound HTTP client honors the OTel Context.current() (the standard OTel client instrumentations do).

On the outgoing side, OtelTracerHook.startSpan(...) now also renders the W3C traceparent / tracestate values from the server-started span context at span-start time and stashes them as request attributes (juneau.traceparent / juneau.tracestate). The new TraceContextResponseProcessor in juneau-rest-server writes those onto the HTTP response so callers can read the resulting trace id back — see the juneau-rest-server entry above.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-tracing-otel</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.62.0</version> <!-- consumer-supplied; provided scope on juneau-rest-server-tracing-otel -->
</dependency>

See REST Server — Observability (Micrometer + OpenTelemetry) for the full topic.

Dependency upgrade​

  • Bumped io.opentelemetry:opentelemetry-api from 1.43.0 to 1.62.0. Closes Dependabot alerts #28 and #29 (CVE-2026-45292 / GHSA-rcgg-9c38-7xpx — "OpenTelemetry Java SDK has Unbounded Memory Allocation in W3C Baggage Propagation"; affected range <= 1.61.0). The bridge's API surface (Tracer / Span / Context / Scope / TextMapGetter) is stable across the 1.x line; no consumer-visible behavior change.

juneau-rest-server-view-jsp (new module)​

A new opt-in REST module, juneau-rest-server-view-jsp, adds JSP view-rendering to juneau-rest-server without bleeding the JSP-engine dependency (Apache Jasper) into the core. The new View interface (see juneau-rest-server) lives in core; this module ships the JSP-specific implementation. Engine-agnostic POM stance: the bridge module declares the JSP API + JSTL impl in provided scope only — consumers add the engine matching their container (Jetty 12 EE11's jetty-ee11-apache-jsp, embedded Tomcat / Spring Boot default's tomcat-embed-jasper, or rely on the deployment container's bundled engine for external WAR deploys).

New Classes​

  • org.apache.juneau.rest.server.view.jsp.JspMixin — REST mixin attachable via @Rest(mixins=JspMixin.class). Adds a default /jsp/* mount that forwards raw .jsp requests to the container's JSP engine, and contributes JspViewRenderer to the mixin's response-processor chain. Builder API: JspMixin.create().basePath("/WEB-INF/views/").build().
  • org.apache.juneau.rest.server.view.jsp.JspView — View implementation. Immutable value class; fluent builder: JspView.of("hello.jsp").attr("name", name).header("Cache-Control", "no-store"). attr(...) rejects null values because Servlet-spec setAttribute(name, null) deletes the binding rather than setting it.
  • org.apache.juneau.rest.server.view.jsp.JspViewRenderer — ResponseProcessor that detects JspView-typed return values and dispatches via ServletContext.getRequestDispatcher(...).forward(...). When no JSP engine is on the classpath, surfaces NO_ENGINE_DIAGNOSTIC naming the missing dependency.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-jsp</artifactId>
<version>10.0.0</version>
</dependency>
<!-- pick ONE of these (engine-agnostic stance): -->
<dependency>
<groupId>org.eclipse.jetty.ee11</groupId>
<artifactId>jetty-ee11-apache-jsp</artifactId>
<version>12.1.8</version> <!-- Jetty 12 EE11 -->
</dependency>
<!-- OR -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>10.1.34</version> <!-- embedded Tomcat / Spring Boot default -->
</dependency>

Composition example​

@Rest(path="/app", mixins=JspMixin.class)
public class AppResource extends RestServlet {

@Bean
JspMixin jsp() {
return JspMixin.create()
.basePath("/WEB-INF/views/")
.build();
}

@RestGet("/hello/{name}")
public View hello(@Path String name) {
return JspView.of("hello.jsp").attr("name", name);
}
}

Typed-handler View-return integration (JspView_TypedHandler_Test)​

@RestGet methods returning JspView now reach JspViewRenderer rather than being bean-serialized, thanks to the ResponseProcessorList partition pass. To use JspView from a host class without the mixin, add JspViewRenderer.class to the host's responseProcessors:

@Rest(responseProcessors={JspViewRenderer.class})
public class AppResource extends BasicRestServlet {
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return JspView.of("hello.jsp").attr("name", name);
}
}

Full JSP rendering requires a real servlet container; the MockRest test verifies that the renderer is reached and the response body is not a bean-JSON dump of JspView.

See JSP View Support for the full topic — engine-selection matrix, Spring Boot fat-jar caveats, and known limitations.

juneau-rest-server-view-thymeleaf (new module)​

A new opt-in REST module, juneau-rest-server-view-thymeleaf, adds Thymeleaf view-rendering to juneau-rest-server — sibling to juneau-rest-server-view-jsp, but for the engine that Spring Boot autoconfigures by default. The same View interface (see juneau-rest-server) shipped with 10.0.0 backs both bridges. Engine-agnostic POM stance: the bridge module declares org.thymeleaf:thymeleaf in provided scope only — consumers add the engine matching their deployment (spring-boot-starter-thymeleaf for Spring Boot, org.thymeleaf:thymeleaf for Juneau microservices / Jetty, or any custom @Bean TemplateEngine for advanced resolver / dialect configurations).

Unlike JSP, Thymeleaf's core engine has zero servlet-container dependencies — it renders directly to a java.io.Writer. The raw-template mount under /thymeleaf/* works fully under MockRest, Jetty microservices, and Spring Boot uniformly. The bridge picks up a Spring-autoconfigured SpringTemplateEngine automatically via BeanStore.getBean(TemplateEngine.class); when no engine bean is registered the bridge constructs a default TemplateEngine with a single ClassLoaderTemplateResolver (prefix=basePath, suffix=".html", templateMode=HTML, cacheable=true).

New Classes​

  • org.apache.juneau.rest.server.view.thymeleaf.ThymeleafMixin — REST mixin attachable via @Rest(mixins=ThymeleafMixin.class). Adds a default /thymeleaf/* mount that renders raw .html templates under the configured base path, and contributes ThymeleafViewRenderer to the mixin's response-processor chain. Builder API: ThymeleafMixin.create().basePath("/templates/").cacheTemplates(false).templateMode(TemplateMode.HTML).build(). Configurable mount path via SVL ${juneau.thymeleaf.path:thymeleaf} (system property / env var / Config key). Path-traversal protection via FileUtils.resolveVirtualPathSafely(...) — both direct and nested .. traversal returns HTTP 403 at the handler boundary.
  • org.apache.juneau.rest.server.view.thymeleaf.ThymeleafView — View implementation. Immutable value class; fluent builder: ThymeleafView.of("hello").attr("name", name).header("Cache-Control", "no-store"). attr(...) rejects null values because a Thymeleaf Context silently drops null-valued bindings, which masks caller bugs.
  • org.apache.juneau.rest.server.view.thymeleaf.ThymeleafViewRenderer — ResponseProcessor that detects ThymeleafView-typed return values and asks the configured org.thymeleaf.TemplateEngine to render them directly onto the response writer. When no Thymeleaf engine is on the classpath, surfaces NO_ENGINE_DIAGNOSTIC naming the missing dependency.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-thymeleaf</artifactId>
<version>10.0.0</version>
</dependency>
<!-- pick ONE of these (engine-agnostic stance): -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<!-- Spring Boot autoconfigured SpringTemplateEngine -->
</dependency>
<!-- OR -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.5.RELEASE</version> <!-- Juneau microservice / Jetty -->
</dependency>

Composition example​

@Rest(path="/app", mixins=ThymeleafMixin.class)
public class AppResource extends RestServlet {

@Bean
ThymeleafMixin thymeleaf() {
return ThymeleafMixin.create()
.basePath("/templates/")
.build();
}

@RestGet("/hello/{name}")
public View hello(@Path String name) {
return ThymeleafView.of("hello").attr("name", name);
}
}

Typed-handler View-return integration (ThymeleafView_TypedHandler_Test)​

@RestGet methods returning ThymeleafView now reach ThymeleafViewRenderer rather than being bean-serialized. Because Thymeleaf renders directly to a Writer with no servlet-container dependency, attribute interpolation (th:text="${name}" → "Alice") is fully assertable under MockRest. Add ThymeleafViewRenderer.class to the host's responseProcessors to use ThymeleafView without the mixin.

See Thymeleaf View Support for the full topic — engine-selection matrix, Spring Boot integration notes, and known limitations.

Dependency upgrade​

  • Bumped org.thymeleaf:thymeleaf from 3.1.3.RELEASE to 3.1.5.RELEASE. Closes Dependabot alerts #24, #25, #26, and #27 — CVE-2026-40477 / GHSA-r4v4-5mwr-2fwr (improper restriction of accessible-object scope), CVE-2026-40478 / GHSA-xjw8-8c5c-9r79 (unauthorized expression syntax), and CVE-2026-41901 / GHSA-c9ph-gxww-7744 (sandboxed-expression syntax-recognition bypass). All four are sandbox-scope vulnerabilities; the Juneau bridge does not enable sandboxed expressions explicitly, but consumers that do are now on a fixed engine version.

juneau-rest-server-view-mustache (new module)​

A new opt-in REST module, juneau-rest-server-view-mustache, adds Mustache view-rendering to juneau-rest-server — sibling to juneau-rest-server-view-jsp and juneau-rest-server-view-thymeleaf, but for Mustache's intentionally logic-less template syntax that's portable across JavaScript, Go, Python, and Ruby front ends. The same View interface (see juneau-rest-server) shipped with 10.0.0 backs all three bridges. Engine-agnostic POM stance: the bridge module declares com.github.spullara.mustache.java:compiler in provided scope only — consumers add the engine matching their deployment.

Mustache's core engine has zero servlet-container dependencies — it compiles to bytecode and renders directly to a java.io.Writer. The raw-template mount under /mustache/* works fully under MockRest, Jetty microservices, and Spring Boot uniformly. The bridge picks up a user-supplied MustacheFactory automatically via BeanStore.getBean(MustacheFactory.class); when no factory bean is registered the bridge constructs a default DefaultMustacheFactory anchored on the classpath directory derived from basePath (e.g. basePath("/templates/") → resource root templates).

New Classes​

  • org.apache.juneau.rest.server.view.mustache.MustacheMixin — REST mixin attachable via @Rest(mixins=MustacheMixin.class). Adds a default /mustache/* mount that renders raw .mustache templates under the configured base path, and contributes MustacheViewRenderer to the mixin's response-processor chain. Builder API: MustacheMixin.create().basePath("/templates/").templateSuffix(".mustache").build(). Configurable mount path via SVL ${juneau.mustache.path:mustache} (system property / env var / Config key). Path-traversal protection via FileUtils.resolveVirtualPathSafely(...) — both direct and nested .. traversal returns HTTP 403 at the handler boundary.
  • org.apache.juneau.rest.server.view.mustache.MustacheView — View implementation. Immutable value class; fluent builder: MustacheView.of("hello").attr("name", name).header("Cache-Control", "no-store"). attr(...) rejects null values to surface caller bugs early.
  • org.apache.juneau.rest.server.view.mustache.MustacheViewRenderer — ResponseProcessor that detects MustacheView-typed return values and asks the configured com.github.mustachejava.MustacheFactory to compile and execute them directly onto the response writer. When no Mustache engine is on the classpath, surfaces NO_ENGINE_DIAGNOSTIC naming the missing dependency.

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-mustache</artifactId>
<version>10.0.0</version>
</dependency>
<!-- engine: -->
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.14</version>
</dependency>

Composition example​

@Rest(path="/app", mixins=MustacheMixin.class)
public class AppResource extends RestServlet {

@Bean
MustacheMixin mustache() {
return MustacheMixin.create()
.basePath("/templates/")
.build();
}

@RestGet("/hello/{name}")
public View hello(@Path String name) {
return MustacheView.of("hello").attr("name", name);
}
}

Typed-handler View-return integration (MustacheView_TypedHandler_Test)​

@RestGet methods returning MustacheView now reach MustacheViewRenderer rather than being bean-serialized. mustache.java renders directly to a Writer with no servlet-container dependency, so {{name}} → "Alice" is fully assertable under MockRest. Add MustacheViewRenderer.class to the host's responseProcessors to use MustacheView without the mixin.

See Mustache View Support for the full topic — engine-selection matrix, Spring Boot integration notes (Spring Boot's official starter ships jmustache, not mustache.java), and known limitations.

juneau-rest-server-view-freemarker (new module)​

A new opt-in REST module, juneau-rest-server-view-freemarker, adds Apache FreeMarker view-rendering to juneau-rest-server — sibling to juneau-rest-server-view-jsp, juneau-rest-server-view-thymeleaf, and juneau-rest-server-view-mustache. The same View interface (see juneau-rest-server) shipped with 10.0.0 backs all four bridges; with this module, the Phase B sibling lineup is complete. Engine-agnostic POM stance: the bridge module declares org.freemarker:freemarker in provided scope only — consumers add the engine matching their deployment, and Spring Boot apps get autoconfiguration for free via spring-boot-starter-freemarker.

FreeMarker is the engine behind countless admin consoles, code generators, email templates, and reporting tools — Apache-family alignment (ASF-maintained engine, same license as Juneau itself), richer expression syntax than logic-less Mustache, and native HTML auto-escaping via the .ftlh template flavor. The core engine has zero servlet-container dependencies; it asks freemarker.template.Configuration for a Template and template.process(dataModel, writer) streams the rendered output directly. The raw-template mount under /freemarker/* works fully under MockRest, Jetty microservices, and Spring Boot uniformly. The bridge picks up a user-supplied freemarker.template.Configuration automatically via BeanStore.getBean(Configuration.class) (Spring Boot's spring-boot-starter-freemarker autowires one out of the box); when no configuration bean is registered the bridge constructs a default Configuration anchored on a classpath resource root derived from basePath, pinning IncompatibleImprovements to VERSION_2_3_34, defaulting encoding to UTF-8, and selecting HTMLOutputFormat so HTML escaping is the natural target.

New Classes​

  • org.apache.juneau.rest.server.view.freemarker.FreemarkerMixin — REST mixin attachable via @Rest(mixins=FreemarkerMixin.class). Adds a default /freemarker/* mount that renders raw .ftl / .ftlh templates under the configured base path, and contributes FreemarkerViewRenderer to the mixin's response-processor chain. Builder API: FreemarkerMixin.create().basePath("/templates/").templateSuffix(".ftlh").cacheTemplates(true).build(). Configurable mount path via SVL ${juneau.freemarker.path:freemarker} (system property / env var / Config key). Path-traversal protection via FileUtils.resolveVirtualPathSafely(...) — both direct and nested .. traversal returns HTTP 403 at the handler boundary. The cacheTemplates(boolean) flag configures the bridge-default Configuration's TemplateUpdateDelayMilliseconds (cache forever vs. re-check every render).
  • org.apache.juneau.rest.server.view.freemarker.FreemarkerView — View implementation. Immutable value class; fluent builder: FreemarkerView.of("hello.ftlh").attr("name", name).header("Cache-Control", "no-store"). attr(...) rejects null values to surface caller bugs early.
  • org.apache.juneau.rest.server.view.freemarker.FreemarkerViewRenderer — ResponseProcessor that detects FreemarkerView-typed return values and asks the configured freemarker.template.Configuration to getTemplate(name).process(dataModel, writer) directly onto the response writer. When no FreeMarker engine is on the classpath, surfaces NO_ENGINE_DIAGNOSTIC naming the missing dependency (with a pointer to spring-boot-starter-freemarker for Spring Boot apps).

Dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-freemarker</artifactId>
<version>10.0.0</version>
</dependency>
<!-- engine: -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>

Composition example​

@Rest(path="/app", mixins=FreemarkerMixin.class)
public class AppResource extends RestServlet {

@Bean
FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.templateSuffix(".ftlh")
.build();
}

@RestGet("/hello/{name}")
public View hello(@Path String name) {
return FreemarkerView.of("hello").attr("name", name);
}
}

Typed-handler View-return integration (FreemarkerView_TypedHandler_Test)​

@RestGet methods returning FreemarkerView now reach FreemarkerViewRenderer rather than being bean-serialized. FreeMarker renders directly to a Writer with no servlet-container dependency, so ${name} → "Alice" is fully assertable under MockRest. Add FreemarkerViewRenderer.class to the host's responseProcessors to use FreemarkerView without the mixin. Spring Boot's spring-boot-starter-freemarker autoconfigures a freemarker.template.Configuration bean that the bridge picks up automatically via BeanStore.getBean(Configuration.class) — no additional wiring required.

See FreeMarker View Support for the full topic — engine-selection matrix, Spring Boot integration notes (spring-boot-starter-freemarker autoconfigures a Configuration bean the bridge picks up automatically), .ftl vs .ftlh auto-escape selection, template caching, and known limitations.