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(...), soMyService.host,MY_SERVICE_HOST, andmy.service.hostare all equivalent. - Same source-chain precedence as
@Value— thread-local/global test overrides, then any caller-scopedPropertySourcebeans, then the process-wideSettingschain (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/@Injecton 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 viabeanStore.getBean(...)or plain@Injectanywhere 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
ContentSecurityPolicyHTTP header bean + fluent builder injuneau-rest-common(org.apache.juneau.http.header), with typed directive setters (defaultSrc,scriptSrc,styleSrc,imgSrc,connectSrc,fontSrc,objectSrc,baseUri,frameAncestors,reportUri/reportTo), a genericdirective(name, sources...)escape hatch, source-expression helpers, report-only mode, and a ready-made nonce-based "strict starter" preset. - Per-response
SecureRandomnonce generation: the same nonce is injected into the emittedContent-Security-Policyheader 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(withSec-Fetch-Siteas a fallback) must match the request's own scheme, host and port. A literalnullorigin 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-Tokenby default). HTML form posts cannot send a header, so they are unsupported by design. - Non-browser clients. A request with neither
OriginnorSec-Fetch-Siteis admitted without a token only when a browser could not have sent it cross-site without a preflight (for client authors: send JSON, or sendOriginplus the token).NonBrowser.REJECTandNonBrowser.REQUIRE_TOKENtighten that. - Proxies.
OriginSource.HOST_HEADER(the default) never readsX-Forwarded-*;OriginSource.CONTAINERtakes the origin from the container's own forwarded-header support (JettyForwardedRequestCustomizer, TomcatRemoteIpValve, Spring Bootforward-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/*forREQUESTdispatches. - The microservice admin samples.
AdminBoundaryinjuneau-microservice-examplesregisters aCsrfBoundaryoverConfigResource,LogsResourceandShutdownResource, 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 aCsrfBoundary. A missing,nullor malformedOriginis refused with HTTP 403 before the upgrade; a well-formed cross-site or mismatchedOriginis refused after the upgrade with close code4403, before any frame is sent or any bus session is touched. The handshake has no server name, so it judges the rawHostheader (over HTTP/2, the request URI's authority) plus the request URI scheme, whateverOriginSourceis set. Behind a proxy that rewritesHostwith no container-side host rewriting, same-origin upgrades are refused; preserveHostat the proxy.BusWebSockets.registerrefuses any boundary type other thanLoopbackBoundaryandCsrfBoundary. Verified on Jetty and Tomcat; on Tomcat,RemoteIpValveforwards the scheme to the handshake but not theHostheader, so a forwarded host is refused even with the valve'shostHeaderset. - Opt-in trusted forwarded origin.
CsrfBoundary.Builder.trustedForwarding(X_FORWARDED | FORWARDED)andtrustedHops(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, thenth 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 newBoundaryReasonvalues, HTTP 403; handshakes close with4403).X-Forwarded-Portis 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
Hostheader are judged on the container's:authorityview.CsrfBoundaryno longer refuses them withHOST_MISSING, andLoopbackBoundaryno longer refuses them withHOST_MISMATCH. Bus WebSocket handshakes over HTTP/2 (RFC 8441 extended CONNECT, which Jetty enables by default with HTTP/2) are judged on their:authorityinstead of closing with 4403. UnderHOST_HEADER, HTTP/2 requests are judged on the container's server name, which container forwarding support may rewrite. HTTP/1.x requests withoutHostare unchanged. LoopbackBoundaryjudges the effective method. AGETcarrying?method=DELETEor anX-HTTP-Method-Overrideheader now takes the write checks, even when the resource it reaches would not honor the override (it now does honor it whereallowedMethodHeadersis set; see the next bullet).- Method overrides: all four headers honored, conflicts refused.
X-HTTP-Method-Override,X-HTTP-MethodandX-Method-Overrideare now honored under@Rest(allowedMethodHeaders)(and the globalJ_ALLOWED_METHOD_HEADERS), exactly asX-Methodis; 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 answered400; identical repeats are fine. Themethodquery parameter is now matched case-insensitively (?METHOD=PUTworks) and read from the query string only, so a form-bodymethod=field is no longer honored. The MCP bearer gate treats aPOSTclaim on any of these channels as aPOST. - BREAKING: method overrides on safe requests are restricted. A
GET,HEAD,OPTIONSorTRACErequest 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 exampleGET ?method=DELETE,GET ?method=POSTon an RRPC operation, orGET ?method=FOOreaching amethod="*"operation, is refused with405 Method Not Allowedand anAllowheader, instead of being dispatched. Overrides onPOST(for examplePOST ?method=PATCHfor clients withoutPATCH) are unchanged. Defaults are unaffected:allowedMethodParamsstill defaults toHEAD,OPTIONS. Migration: mark read-only custom verbs@RestOp(read=true); change links that used?method=to trigger a write into aPOST(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=trueonPOST/PUT/PATCH/DELETE/CONNECT,*, RRPC, or together with@Mutatingfails at startup. An MCP server mounted as a mixin now answers an unauthenticatedGET ?method=POSTwith405rather than401. Every framework405now carries anAllowheader, 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 withCache-Control: no-store. Code that stamps a token into a response (ViewTable.CSRF_ATTR,RegionTable.of(region, csrfToken),PageSpec.csrf(...)) must callRequestBoundaryFilter.markTokenBearing(response), which setsCache-Control: private, no-store. - No token in a committed response. A token is never embedded into a response that is already committed, including
csrfattributes an author put on the page's body element. The page's writes then fail visibly. - Breaking (unreleased API): the nested
LoopbackBoundary.ReasonandLoopbackBoundary.Resulttypes are now the top-levelBoundaryReasonandBoundaryResult, 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 wasX-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'sManifestFilemain 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 (agit-commit-id+maven-jar-pluginrecipe 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. Drivesjava.util.loggingby 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 MicrometerMeterRegistryresolved from the bean store; degrades to501when no scrapeable registry is present. Never auto-registers a default registry./threaddump+/heapdump(juneau-rest-server) — server-agnosticThreadMXBeanthread dump and HotSpot.hprofheap dump, both deny-by-default on security grounds (opt in viaDumpsSettings).
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;
ROOTkey for the root logger); set is process-lifetime-only (no config-file rewrite). Level names are backend-native; an invalid name fails400. - Drives the concrete backend, not the SLF4J facade:
LogbackLogBackendfor SLF4J→Logback,Log4j2LogBackendfor Log4j2; anslf4j-jdk14binding is JUL, so the default backend already covers it. Custom backends implementLogBackenddirectly.
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.
mixinDefscoexists with the existingmixins=Class<?>[]; a@Mixin(type=X.class)with no overrides is exactly equivalent to a baremixins=X.classentry, and existing usages are untouched. - Override slots mirror the composition-shaped
@Restattributes:guards,roleGuard/rolesDeclared,converters,encoders,serializers/parsers,responseProcessors,restOpArgs,partSerializer/partParser,messages, the default request/response headers + attributes,produces/consumes/defaultAccept/defaultContentType/defaultCharset, andmaxInput. - 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.
@Mixincarries its ownnoInherit(unioned with the mixin class's@Rest(noInherit=...)) so the host override is authoritative but overridable bynoInherit. - Host-chosen re-mount:
@Mixin(path=...)/paths={...}re-mount the mixin's endpoints under host-chosen prefix(es), reusing the existingpathTokennormalization. - 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— neverguards,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 ownViewreturns through the engine with nomergeIntoHostor explicitresponseProcessors=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.
childrenDefscoexists with the existingchildren=Class<?>[]; a@Child(type=X.class)with no seed members is exactly equivalent to a barechildren=X.classentry, 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
@Mixinoverride 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@Restdeclaration always wins. noInheritcuts 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/defaultContentTypewere 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
@Beanof the same type and name takes precedence over the inherited one. - Framework-managed beans stay per-context. Serializers, parsers, the call logger,
RestChildrenand similar are not exported; each child still builds its own from its own@Restconfiguration. - 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-resourcesObservationhandle), anObserverthat 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-pathmetricName/metricTagsmodel. No@Observedannotation — 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 anINTERNAL-kind span, nested under any active request span). juneau-observability-otlp-bundle: a curated dependency bundle (underjuneau-bundles) that pulls both Juneau observability bridges plus a version-aligned OTLP exporter stack —opentelemetry-exporter-otlp(+ SDK + autoconfigure) for traces andmicrometer-registry-otlpfor metrics — in one dependency. Metrics + traces for v1 (OTLP logs export deferred). A dependency aggregator only; you still wire the@Bean MetricsRecorder/@Bean TracerHookexplicitly.- Log correlation: the JUL
LogEntryFormattergains optional{traceId}/{spanId}placeholders, sourced reflectively from the active OTelSpan.current()(no hard OTel dependency — they render empty when OTel is absent).MdcAsyncListeneradditionally folds the active trace/span id into the propagated SLF4J MDC snapshot (keystrace_id/span_id) so correlation survives the async-completion hop, gated byisMdcAsyncPropagation()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 theLogEntryFormatterregex 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
Authenticatorcontract — a newAuthenticatorfunctional interface (Optional<AuthResult> authenticate(HttpServletRequest)) is now implemented by bothAuthFilterandAuthFilterChain, so every existing filter (SAML / OAuth / JWT / API-key / bearer) is reusable verbatim at the resource layer.RestAuthenticator.of(Authenticator)adapts any of them. AuthResultmerge modes —AuthResultgains an immutable merge mode:of(...)=ADD(union roles, principal required — unchanged),ofRoles(...)= roles-onlyADD(null principal — keeps the inherited identity),replacing(...)=REPLACE(swap identity outright). The existingof(...)factories and behavior are preserved (backward compatible).- Registration & DI — resolved per
RestContextwith precedence registered bean ›@Beanmethod ›@Rest(authenticator=...)›RestServlet.createAuthenticator(BeanStore). BothAuthenticator-into-RestAuthenticatorandRestAuthenticator-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.
Queryableon BeanQuery — thesearch/view/sort/position/limit/optsrequest parameters (bound byBeanQueryRequest) run against anInMemoryBeanQueryContextbuilt from the response's row type (one context per element class, cached for the life of the converter;Maprows 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 (mirroringIntrospectableSettings) 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=trueaddsX-BeanQuery-TotalandX-BeanQuery-Matchedresponse headers (each only when that count was computed). The body stays a bare list. - Central error mapping —
RestContext.convertThrowablemapsBeanQuerySyntaxExceptionto HTTP 400 with anX-BeanQuery-Error: <CODE>header (BeanQueryRequest.ERROR_HEADER) and, with problem details on, a"code"member in the body;BeanQueryExecutionExceptionmaps to HTTP 500 with the fixed bodyQuery execution failed.(the cause is logged, never sent to the client). The DataTables adapter uses the same contract (next bullet). juneau-console-datatablesmodule (org.apache.juneau:juneau-console-datatables) — a new opt-in add-on (not bundled injuneau-distrib, mirroring thejuneau-rest-server-management-loggingprecedent) with:DataTablesQuery(org.apache.juneau.console.datatables.adapter) — maps the DataTables server-side contract onto BeanQuery:DataTablesQuery.toBeanQuery(req, ctx)builds aBeanQueryfrom aDataTablesRequest(per-column search, order, paging, and a global search that expands to an$orof a per-SearchTypeleaf —$contains/$prefix/$eqic/$eq— over every searchable, resolvable column, not just text), andDataTablesQuery.run/runArraysexecute 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 newJuneauDataTables.ajax(url, extra?)helper injuneau-datatables.js, or thedata-juneau-datatable-ajaxauto-init attribute. A malformed request (unknown column, amaxSearchClauses/maxSortKeyscap exceeded, a bad search expression) raisesBeanQuerySyntaxException, which the adapter does not catch: the central mapping answers HTTP 400 with anX-BeanQuery-Error: <CODE>header and the message in the body, exactly like any otherQueryableendpoint.JuneauDataTables.ajaxinstalls anxhr.dtlistener that shows that server message through DataTables' normal error path (theerror.dtevent, thenDataTable.ext.errMode) instead of a generic "Ajax error"; other failed requests keep the DataTables default. An execution failure is HTTP 500 via the centralBeanQueryExecutionExceptionmapping. See DataTables.- Views runtime server-mode tables now ride the same wire format. A
juneau-console-viewstable withdataMode: "server"now getsserverSide: trueandajax: JuneauDataTables.ajax(url, {...})(injuneau-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@RestPostmethods taking@Content DataTablesRequest, not GET handlers reading query-string parameters. Column-scoped ribbon options now ride the JSON body as that column'ssearch.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 throughbeforeSend. Client-mode tables are unchanged. A page must loadjuneau-datatables.jsbeforejuneau-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}(+ optionalerror) envelope bean, rendered by the negotiated serializer (no hardcoded JSON).- Browser-side helpers —
DataTablesColumns(generatecolumns:[…]from bean-property order +@Schematitles),DataTablesTable(build a DataTables-ready HTML5TableDOM bean, withMarshallingContextoverloads), andDataTablesMixin(serves a first-partyjuneau-datatables.jsauto-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—ConsoleChromeMixinserves a dynamically themedchrome.css(the module's shipped structural stylesheet plus a:rootblock built from validated--jc-*theme tokens);Themevalidates every token value through a normalize-then-accept-known-safe pipeline (reject C0/C1/DEL control chars → strip CSS comments → case-insensitively reject anyurl(production → match an allowlist grammar for colors/fonts/lengths/gradients) rather than a denylist, so a token value can never smuggle aurl()-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 viaurl()anyway since the grammar structurally excludes that production. The console logo is a staticchrome.cssconstant, 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 consolebase.ftlh(see Admin Console Chrome).- Console theming is FTL-only: custom palettes are authored with
<@theme>/<@token>(value=for leaves,alias=for livevar()references).ThemePack,ThemeSettings,ThemePackSettings,ConsoleChromeMixin.Builder.pack(...)/pageBackgroundImage(...)and the/juneau-console/assets/logo/page-bgendpoints were removed;ConsoleChromeMixin.create().theme("<stock-name>")selects a stock palette for the servedchrome.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-blockand--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-packsmodule was removed. juneau-console-freemarker—ConsoleFreemarkerMixin extends FreemarkerMixin(register it typed asFreemarkerMixin, mirroring the load-bearing-return-type ruleFreemarkerMixinbeans already follow) augments the resolvedConfiguration'sTemplateLoaderwith a second, classpath-root-rootedClassTemplateLoader, without shadowing the consumer's ownbasePath-relative templates. This resolves the reservedorg/apache/juneau/console/base.ftlhtemplate (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-suppliedConfigurationbean (Spring Boot autoconfig, etc.) is honored untouched, not augmented.DataTablesTableadditive@Html(render=...)honoring (in the existingjuneau-console-datatablesmodule) —DataTablesTable.of(MarshallingContext, String, Collection<?>, Class<?>)now resolves and applies each row property's@Html(render=...)the same way the ordinaryHtmlSerializer/HtmlDocSerializerpath 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 thisDataTablesTableoverload, where it previously rendered the raw value.Map-based rows are unaffected (noBeanPropertyMetato resolve a render from).juneau-console-freemarker— shareable-URL state and more datatables catalog keys. Theviewstoolkit pack now includesjuneau-urlstate.js(afterjuneau-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 passcopyLink,rowActions,rowClassRulesandpausePollingWhileEditingthrough onto the view, andselectionandbulkthrough onto the slot; previously these keys were silently dropped on FTL-built pages.juneau-console-views/TableSpec—urlStatekey and theday(YYYY-MM-DD)directive. A table view can now opt in to built-in shareable-URL directives withTableSpec.urlState(String...)(orurlState: {day: {}}in a catalog; the page schema validates it).daystores a calendar date in?state=and passes it to the server as adayquery parameter (rename it withTableSpec.urlStateDayParam(String param), emitted asurlState.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 asrc=attribute. A bare-URLdatatablesbody is removed (it is now E-25), andsearchTypevalidation moves to render time (E-32). The bulk wire contract is"2"(aggregate mode,BulkResult).<@card type="datatables">is the one table surface.QuickStatgains meter and segmented kinds.QuickStat.meter(id, label, value, max)paints a filled track withrole="meter", andQuickStat.segmented(id, label).segment(...)paints a proportional bar with a text legend. Both require a non-blank label. The newstatusTone(StatusTone)sets an item tone from the shared status palette;tone(QuickStat.Tone)is unchanged.- Inline datatables catalogs refuse a malformed
quickStatsstrip (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="…"/>, andwindow.JuneauConsole(contract,card,registerCard,mount,chrome, and thejuneau:console-mountedevent). - Loud failures: template mistakes fail the render with E-1 to E-18; contract problems in the
browser show a
.jc-console-errorbanner (E-JS-1 to E-JS-12, then E-JS-14). - Fix: a
@BeanreturningConsoleFreemarkerMixin(rather thanFreemarkerMixin) 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
versionkey is renamedcontractVersion, 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.jsonnow requirescontractVersionand forbidsversion,juneau-console.jsreads onlycontractVersion(JuneauConsole.SUPPORTED_VERSIONSis nowSUPPORTED_CONTRACT_VERSIONS),PageContractAssert.hasVersion(String)is renamedhasContractVersion(String), andassertPage(...)throws if a contract still carriesversion. Pages are generated by<@console>, so upgrading Juneau is the only step; code that hand-builds a page contract, or callshasVersion, must be updated. Browsers that cached the previousjuneau-console.js(served with a one-daymax-age) show anE-JS-2banner until the cache expires or the page is hard-reloaded. - BREAKING: the FTL globals
pageBody,pageTab,pageInit,pageCss,pageToolkit,pageToolkitCss,pageToolkitJsandpageThemeCssare removed (usejcHasToolkit("views")). The legacy chrome templates that did not use<@console>are removed; a<@page>whose chrome renders no<@console>is an error. The<@card>typesjs,jsonandcalendarare removed (usetype="html"with a body or a registered card type), as are thedata-juneau-card-sidecarscripts (no directive emits them any more; the orphanedjuneau-page-cards.jsasset is deleted).juneau-chrome.jsis absorbed intojuneau-console.js. Server HTML no longer contains nav, header, footer or card wrappers; tests that matched that HTML must move toPageContractAssert.
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 withdependsOnand aVENDOR/RUNTIMEkind), the built-in packsjquery,datatables,datatables-glueanddatatables-buttons,<@card requires=...>, andConsoleFreemarkerMixin.BuildermethodsregisterToolkitPack(ToolkitPack),cardRequires(type, packs...)andprovidedPacks(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:
WebJarsMixinandWebJarResolverinjuneau-rest-serverserve/webjars/*from the classpath.juneau-console-datatablesnow 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 andjuneau-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 composesWebJarsMixinas well asDataTablesMixin. - BREAKING (10.0 snapshots only):
juneau-datatables.jsleft theviewspack.ToolkitPackRegistry.Resolvedis split intovendorCss,vendorJs,runtimeCssandruntimeJs, and theresolve(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 throughinit=orcss=is skipped only when its file name matches the pack's; an app with its own copies should name the packs inprovidedPacks(...)or mountWebJarsMixin. - Removed (10.0 snapshots only): APIs left unused once every consumer moved to card-declared packs.
DataTablesMixin.JQUERY_CDN_URL,DATATABLES_JS_CDN_URLandDATATABLES_CSS_CDN_URLare gone; jQuery and DataTables now come from the WebJars.PageCapture.hasServerModeTable()is gone, because every datatables card now requires thedatatables-gluepack. OnToolkitPackRegistry, the 2-argresolve(names, req)and theregister(name, cssPaths, jsPaths[, resolver])overloads are gone; useresolve(names, req, pageUrls, template)andregister(ToolkitPack).ConsoleFreemarkerMixin.Builder.registerToolkitPack(name, cssPaths, jsPaths)andprovidedPacks(...)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.Loggerlevel 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), oneLogRecordper 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 toFINESTwithout 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
INFOor finer is set explicitly on the resource's logger (or an enclosing logger such as its package), andFINE/FINESTadd 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
RestDebugFormatterSPI (org.apache.juneau.rest.server.logging) — a per-tier formatter interface (formatBasic/formatHeaders/formatBody) with a shippedBasicRestDebugFormatterdefault. 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).
BasicRestDebugFormatterno longer dumps bodies just because a logger was raised toFINEST. 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 asExec timeare recorded on the completion path. Synchronous responses and theMockRestClient/non-async-container fallback are unchanged (still emitted during synchronous finish), and@RestEndCallstill runs on the request thread. Exactly oneINFOrecord is emitted per request on every path. - Logger level controls verbosity only. REST-driven marshalling debug behavior and
EchoMixinreachability are decoupled from the JUL level (see the dedicated entry below).RestRequest.isDebug()still exists, now derived and read-only (truewhen the resolved logger isFINE-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).
EchoMixinis disabled by default./echo/*now returns404 Not Foundregardless of logger level. Enable it explicitly:EchoMixin.create().enabled().build(). Enablement is tri-state — an explicitenabled(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 toFINEsilently 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, aStringon 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 viaRestRequest.isDebugMarshalling(). A serializer/parser configured with its own marshall-layerContext.debugremains 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_BODIESenvironment variable (environment-only, no system-property fallback, read once/cached, trim-then-parse truthy kill-switch semantics). With the gate unset,FINESTemits a suppression placeholder + byte count instead of body content. Even opted in, bodies render only atFINESTand only for renderable text content (via the newisBodyRenderable(String)predicate); non-renderable/binary content and non-identityContent-Encodingyield a distinct placeholder. RestDebugBodyScrubberSPI (org.apache.juneau.http) — an optional, gate-subordinate, fail-closed transform (String scrub(String contentType, String body), registered viabodyScrubber(...)) that chooses scrubbed-vs-raw body text once dumping is permitted; a throw ornullresult 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 sharedRedactedHeaders.DEFAULTplusX-Auth-Token,X-Authorization,WWW-Authenticate,Referer,Location) matched with separator-normalized folding (soX-Auth-Token/X_Auth_Token/XAuthTokenall match).RedactedHeaders.DEFAULTis unchanged. New additiveaddRedactedHeaders(...)knob alongside the replacingredactedHeaders(...). - 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), andmaxUriLength(2048) knobs bound header and URI output; the existingbodyCap()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-Idrequest header is honored when present; otherwise a fresh time-orderedUuid7is minted. The resolved id is echoed on theX-Request-Idresponse header. Exactly onerequestIdLogContextscope 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 structuredrequestIdfield, and the rendered message is prefixed[requestId=<id>]. The id also rides the async-completion snapshot, so true-async completion records correlate identically. RequestIdSettingsbean (public write-surface, resolved byRestContextvia agetBean→@Bean-factory → default ladder):validator(Predicate<String>)defaults to sanitize-and-accept (strip log-forging sequences, accept the bounded remainder — not reject-and-remint), plusattributeKey(String)andidSupplier(Supplier<String>).RequestIdFilterreconciled to a thin façade. Itsapply()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 onRequestIdSettings.- Auth non-influence.
X-Request-Idhas 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=0to get the whole history. - New
lastEventIdquery parameter onGET /juneau-jobs/{jobId}/stream. A non-emptyLast-Event-IDheader beats it, so a browser's own reconnect resumes where it left off. 400for a bad or past-the-end resume value. The status checks run in the order404, then400, then429.- An ended job still sends its
resulton every attach, fresh attaches included. - Job-log card: connects with
?lastEventId=0and 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
defaultOrderinstead 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 ofcolumns,search,sortandoptions. - Accessibility. Dialog tabs carry
aria-selected/aria-controlswith 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
columnSearchToggleribbon 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
schemaVersionis now2, andNS.config.readViewSettingsreturns{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 withoutcolumnConfig. - BREAKING:
NS.config.defaultSortMembershipis removed. - BREAKING: the Options blob gains
autoRefreshMs(old blobs read as Off), andisPollSuspendednow also returnstruewhile the View Settings dialog is open, which pauses any existingpausePollingWhileEditingview.
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(packageorg.apache.juneau.terminal.report) holdsTerminalEvent, 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-viewsdepends on it. - Events:
TerminalEventfactories forstep,end,suite,test,replace,noteanddone(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) andFileTerminalViewSource(a growing JSON Lines file, with410 Goneafter truncation). ImplementTerminalViewSourcefor anything else. - Endpoint:
TerminalViewMixinserves/juneau-terminal-view/{runId}/events;TerminalViewEndpointsgives 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, andReportResult.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.jsjoins theviewstoolkit pack and publishesJuneauViews.terminalView.poll: falseregions take events throughappend, 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 onjuneau-console-viewsand the xterm.js WebJarorg.webjars.npm:xterm__xterm6.0.0. - Endpoints:
TerminalMixinserves/juneau-terminal/{id}/bytes(raw bytes by offset, withTerm-*headers) and/raw;TerminalEndpointsgives the same logic for custom paths. - Sources:
FileTerminalSource(a growing log with a.sizesidecar) andMemoryTerminalSource(a ring buffer). - Launcher:
TerminalProcessruns a command underjuneau_terminal.py --ptyand serves its log and events. juneau_terminal.py: new--pty,--size COLSxROWSand--events PATHoptions, plus--input stdinand--prompts stdioto 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 withanswer(jsonLine); closing it cancels the job. - Terminal view:
step,suiteandtestevents gain an optionalrawOffset;rawHreftakes{offset}andonBlockClickgetsoffset.rawLine,{line}andinfo.lineare gone. The contract version stays"1". - Console UI: the
terminalcard type and thextermandterminaltoolkit packs.terminalis now a reserved card type name. It replaces the console-output card. - xterm options: the client takes an allowlisted
terminalobject (convertEol,scrollback,fontFamily) inmountand on theterminalcard; other keys are ignored. Server-built cards set it through the newTerminalDef.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(orJUNEAU_TERMINAL_CONSOLE), one row per step with a progress bar, counts and a summary line, andJUNEAU_TERMINAL_DETAIL(summary,actionable,modules,all) for how much a finished step keeps.--full-logand--board-logwrite 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=1prints##terminal <json>events for a runner UI, andTERMINAL_ARTIFACTSkeeps copies of the test reports. - Maven rows: one block per module. A single-project build (a one-module tree, or
-plwithout--also-make) draws one module and counts1/1on success. A build that stops beforetest(goals up totest-compile, or-DskipTests) still draws its module bar, counting modules as they compile, with no test totals. - Pytest plugin:
-p juneau_terminal_pytestis added to pytest runs (opt out withJUNEAU_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_REPORTERbeside the config's reporters (opt out withJUNEAU_TERMINAL_PLAYWRIGHT_REPORTER=0). It gives one block per test file, counts a retried test once, writes<step>-playwright.json, and off a terminal setsPLAYWRIGHT_HTML_OPEN=never. - PTY mode:
--pty,--size,--events,--input stdinand--prompts stdio; this is whatTerminalProcessruns (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_scriptsgives push and release scripts git helpers, a session with exit codes, tool runners, resumable steps, locks, notifications and prompts, loaded through the vendoredbootstrap.py. - Juneau build rows:
test.pydraws Compile, Java tests, Python tests and Browser tests (always shown, marked skipped with the reason when not run);push.pyadds 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 leftfigure 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.
chartcard: line, bar and donut charts drawn as inline SVG byjuneau-charts.js, from asrc=endpoint or inlinedata, with optionalrefreshMs, click-through, a data-table view and an open kind registry (JuneauCharts.register). Built from Java with the newChartDefbuilder.job-logcard: a form that starts anAsyncJobsMixinjob, 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'sLast-Event-IDsupport (see "Async job streams resume fromLast-Event-ID" above). Built from Java withCardSpec.of("job-log", id).- Reserved:
job-logis 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, withonStart/onProgress/onResult/onEndhooks and an optionalpaint: "none"),fetchKey,active,pendingandcancel. It allows one job per row, takes the row id only from the row's stamp, and substitutes{id}only."jobs"joinsNS.HOOK_NAMESPACES.helpers.jobButton: a button that runs a row job throughJuneauViews.jobs.JobKeyLedgerandRowJobSubmit: 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 with409write-guard:idempotency-binding-mismatch; a spent key replays its job or answerswrite-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 withJobLogRegion). It gainssubmit.envelope/submit.action,hiddenfields,source: 'row.<field>'presets andoutput, which writes a result value into the card or panel. A datatables card with a job-log region loads thejoblogpack by itself, through the newCardTypeHandler.requiredPacks(JsonMap).window.JuneauJobLog.mountmounts one programmatically.JobLogChecksholds 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 asctx.declared.config, including from a datatables card'sdetail.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.jobsrun or another row action's job) is refused withrow-job-runningand 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-jobmarker, 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 itNS.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'svisibleWhentherefore 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'sdetail.regionnow carries the samelazyvalue as the builder path (falseunless 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:
CalendarLayoutremoved. Calendar lane and chip layout is computed byjuneau-calendar.jsonly.CalendarDefand the calendar card are unchanged.RegionDef.serializeParams(...)/resolvedDataUrl()removed. Region data URLs are built client-side byjuneau-regions.js.- Saved views removed.
SavedViewsMixin,SavedViewStore,InMemorySavedViewStoreandSavedViewQuotaExceptionare gone, along with the server persistence provider injuneau-config.js(createServerProvider,data-juneau-saved-views). Column configuration persists tolocalStorageonly. - Built-in operator help text moved to JS. Built-in
SearchOperators no longer carryhelpon the wire. The column-search popover reads built-in help fromJuneauViews.search.SearchOperators.help(name). Custom operators still supply their ownhelp.
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
defaultsetting. Usedefault: trueon anoption, ordefault: '<memberId>'on anoptionGroup, to choose the initial selection when nothing is persisted. A stored choice always wins. Nothing auto-selects withoutdefault. optis 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) andRibbonCorpus_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 arefresh()item, can now carry an id. The id is emitted on the item's ribbon entry and is rejected ondividerandexportitems.- Ribbon
optionGrouptitle: the grouptitleis now rendered asrole="group"witharia-label, so assistive technology announces it. No visible caption is drawn. Render.meta(String, Object): the value may now be aNumberorBoolean, which stays a JSON number or boolean (for examplemeta("places", 2)). It wasString-only.Column.href(...)impliesrender("linked"): a column with anhrefand norendernow emits thelinkedrenderer, in both Java (Column.toCatalogMap()) and FreeMarker (DatatablesCardType). An explicitrenderalways wins. Migration:Column.toCatalogMap()used to throwIllegalArgumentExceptionfor anhrefwithoutrender("linked"). That throw is gone, so a test that expected it must change, andrender("linked")may be dropped.RowDetailregion is optional:RowDetail.toMap()no longer requires a region. A detail with none is a header-only panel whosetitleandiconare filled from the endpoint'sfields, and its entry carries noregionkey.openConfirmOnlyDialogpasses the row to a confirm renderer: a per-row action'sconfirmRenderernow receivesrows(the one row),labelFieldandrowIdField, the same way the bulk path passes them, so it can name the row it guards. A ribbon-hosted dialog has no row, sorowsis empty. Migration: a customconfirmRendererthat assumedrowswas always empty or absent for a single-row action must handle a one-elementrows.- Same-origin path checks (stricter): a card
srcmust be a relative or root-relative same-origin path, so an absolutehttps://this-host/...srcis 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. Badgesrcandhrefmust be same-origin paths. Migration: change an absolute cardsrcto 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 registeredCardTypeHandlervalidates it and returns the fragment the card carries, and an invalid body fails the render with the<@card>message. Markup passed tocard(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>, itssubscribes/publisheskeys are wired onto the card (refused when theCardSpecsets 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,templateorsrc, andsanitizeis 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 callingtoFragmentitself) 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()andparser()attributes removed from@Query,@Header,@FormData,@Path,@PathRemainder,@Request,@Response. Use the new@HttpPartMarshallingannotation (injuneau-marshall) to specify customHttpPartSerializer/HttpPartParserclasses.@Repeatableremoved from all HTTP annotations. Dynamic annotation stacking viaon/onClassis no longer supported.on()andonClass()attributes removed from all HTTP annotations.@ContextApplyremoved from all HTTP annotations.- 14 XAnnotation companion classes deleted (
ContentAnnotation,FormDataAnnotation,HasFormDataAnnotation,HasQueryAnnotation,HeaderAnnotation,PathAnnotation,PathRemainderAnnotation,QueryAnnotation,RequestAnnotation,StatusCodeAnnotation).ContactAnnotation,LicenseAnnotation,TagAnnotation, andResponseAnnotationremain 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 areHEADER_JuneauSerializerOptions,HEADER_JuneauParserOptions,QUERY_juneauSerializerOptions, andQUERY_juneauParserOptions. Add morepublic static finalfields when other cross-module constants are needed. (This supersedes the earlierRestSessionOptionWiretype.)
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-HttpServletchild-resource base), cascading toBasicRestObject→BasicRestResourceandBasicRestObjectGroup→BasicRestResourceGroup.- The mixin pack drops
Basicand standardizes on theMixinsuffix: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 mixinsBasicJspResource/BasicMustacheResource/BasicThymeleafResource/BasicFreemarkerResource→JspMixin/MustacheMixin/ThymeleafMixin/FreemarkerMixin. - The servlet companions drop
Basictoo:BasicStaticFilesServlet→StaticFilesServlet,BasicAdminServlet→AdminServlet,BasicVersionServlet→VersionServlet, and the abstractBasicViewServlet→ViewServlet(withBasic*Servletview 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 aStringmember 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'sRestChildrenmatcher list. URL matching works immediately; the fullRestContextis 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-entryObject lockwith avolatile RestContext materializedfield. Two simultaneous first-requests serialize on the lock; the second thread reads the already-built context without re-constructing. A materialization that throws leaves thematerializedfieldnull— the next request retries. - Observability: Materialization is logged at
INFOlevel:Lazy REST child materializing: com.example.ChildResource at path '/child'/materialized. - Lifecycle:
RestContext.destroy()(viaRestChildren.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) — aResponseProcessorprepended to the defaultResponseProcessorList. It reads the renderedtraceparent/tracestatestrings that aTracerHookbridge 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 aFINEmessage and skips the write — it never throws. The processor carries no OpenTelemetry dependency, so corejuneau-rest-serverstays OTel-agnostic; it works with anyTracerHookbridge that populates the two attributes.RestContext.responseTraceparent(new env-driven default,${RestContext.responseTraceparent:true}) — controls whetherTraceContextResponseProcessoris registered. Defaults totrue(on-when-tracer); set it tofalseto keep the processor out of the chain entirely even when a tracer is active. Exposed viaRestContext.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@Beanis wired, is silent otherwise)."true"— strict mode: theRestContext/RestOpContextconstructor throwsInternalServerErrorat startup if no non-NoOpMetricsRecorderorTracerHook@Beanis 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 inRestOpInvokeris 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 atRestContextbuild 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 toMetricsRecorder.record(...)as themetricNameparameter;MicrometerMetricsRecorderuses it as the MicrometerTimername instead of the defaulthttp.server.requests. -
metricTags(default"") — per-op additional metric tags in comma-separatedkey=valueformat (e.g."team=payments,region=us-east"). Passed toMetricsRecorder.record(...)as themetricTagsparameter;MicrometerMetricsRecorderadds these as extraTimertags alongside the standardmethod/uri/status/exceptiontags.
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 aPropertySourcenamed"rest.config"on the resource'sBeanStore— not on the process-wideSettings.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 whoseBeanStoreparent-walks to the resource's) consult the scopedPropertySourcebetween theSettingslocal/global override stores and the globalSettingssources chain. Resolution order for these sites is now:Settings.setLocal(...)/Settings.setGlobal(...)overrides (still win — test override semantics are preserved).- The resource's
@Rest(config=...)Config(s). - The global
Settingssources list (system properties, env vars, classpathConfigPropertySourceProvider, microserviceConfig, SpringEnvironment, …). - The
@Value("${key:default}")literal default branch.
- The
BeanStore-scoped registration meansMockRestClient's staticRestContextcache (and any other long-lived cache) cannot leakConfigPropertySources into the globalSettingssingleton — the 6.7× full-suite regression seen during an earlier prototype is structurally impossible. A newSettings_NoLeakedSources_Test(200×MockRestClient.buildcycle) asserts the source count stays flat across the suite. @Value Supplier<String>field types compose with the scoped chain: theBeanStorereference is captured (by-reference, not snapshotted) by the supplier so every.get()re-evaluates against the current state of the resource'sPropertySources. Hot-reload of the underlyingConfig(orSettings.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, andTestBeanStore(a fluentBasicBeanStoreoverlay-builder). - New
BeanStoreOverridable<B>marker interface injuneau-commonsplusoverridingBeanStore(BeanStore)setters onMockRestClient.Builder,Microservice.Builder,SerializerSet.Builder,ParserSet.Builder, andEncoderSet.Builder. The overlay is installed in theoverridingParentslot of the builder's bean store, putting overrides at tier 1 of the resolution chain. - New
Args.overridingParentslot onRestContext.Argsfor threading the overlay into the bean store theRestContextconstructs 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 isattach(...)-ed to a long-lived SUT's bean store; per-test overlays are push'd at@BeforeEach/@BeforeAlland pop'd at@AfterEach/@AfterAll. Backed by newWritableBeanStore.pushOverlay(BeanStore)/popOverlay(Snapshot)primitives.
- Per-test and per-class scopes via
@TestBean(scope = METHOD)(default) and@TestBean(scope = CLASS)(must be onstaticmembers). 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 resolvedPrincipalonRequestAttributesunderRestServerConstants.PRINCIPAL_ATTR. Throws 401 withWWW-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 manualCookieheader parsing when the underlying container (e.g.MockRestRequest) doesn't populategetCookies().org.apache.juneau.rest.auth.TokenValidatorSPI — single-method functional interface (Principal validate(String token) throws AuthenticationException) for plugging in opaque-token, JWT, or custom validators.org.apache.juneau.rest.auth.ApiKeyStoreSPI — 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.Authannotation — marks an@RestOp-method parameter for principal injection. The newAuthArgRestOpArgresolver also supports type-driven resolution: a barePrincipal(orClaimsPrincipal) parameter is injected with no annotation needed.org.apache.juneau.rest.auth.ClaimsPrincipal—Principalsubclass 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 fluentwwwAuthenticate(String)setter (RFC 7235 §4.1).RestServerConstantsadditions:PRINCIPAL_ATTR(request-attribute key for the stashed principal),API_KEY_HEADER(default header name"X-API-Key").AuthArgregistered inDefaultConfig.restOpArgsso type-drivenPrincipalinjection works out of the box.- Composition with
AdminMixin. Swapping the mixin's defaultDenyAllGuardfor aBearerTokenGuardis a zero-mixin-source-change migration — a single@Bean RestGuardList adminGuards(...)on the host wins via the existing guard-override seam (seeBasicAdminResource_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 implementingjakarta.servlet.Filter. Concrete subclasses implementOptional<AuthResult> authenticate(HttpServletRequest req) throws AuthenticationException. Three states:Optional.empty()= no credentials for this filter (pass through), throwAuthenticationException= invalid credentials (abort with 401),Optional.of(AuthResult)= success.org.apache.juneau.rest.auth.AuthResult— immutable value type holding the resolvedPrincipaland an unmodifiableSet<String>of roles. Factory methods:AuthResult.of(Principal, String...)andAuthResult.of(Principal, Set<String>).org.apache.juneau.rest.auth.AuthenticatedRequestWrapper(public) —HttpServletRequestWrapperthat overridesgetUserPrincipal(),isUserInRole(String),getRemoteUser(), andgetAttribute(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.Filterorchestrator. Each entry carries anAuthFilterand an optionalUrlPathMatcherpattern.doFilter(...)semantics: select matching filters, iterate in declaration order, first-success principal wins, all-success roles aggregate, all-failure exceptions aggregate into a single401with combinedWWW-Authenticate. If no filter matches the path, or all matching filters returnOptional.empty(), the request passes through unchanged.- Builder:
AuthFilterChain.create(BeanStore).append(AuthFilter).append(AuthFilter, String pattern).build().
- Builder:
org.apache.juneau.rest.auth.BearerTokenAuthFilter— concreteAuthFilterthat extractsAuthorization: Bearer <token>, delegates to aTokenValidator, and extracts roles from aClaimsPrincipalclaim (default:"roles"). Builder:BearerTokenAuthFilter.create().validator(TokenValidator).realm(String).rolesClaim(String).build().org.apache.juneau.rest.auth.ApiKeyAuthFilter— concreteAuthFilterthat reads an API key from a header / query param / cookie (configurable; default:X-API-Keyheader), delegates to anApiKeyStore, and extracts roles from aClaimsPrincipalclaim. Builder:ApiKeyAuthFilter.create().store(ApiKeyStore).fromHeader(String).fromQuery(String).fromCookie(String).realm(String).rolesClaim(String).build().@Bean AuthFilterChainauto-mount —JettyServerComponent.onStart(...)scans theBeanStorefor anAuthFilterChainbean and registers it at/*before any servlet is mounted. No explicitaddFilter(...)call required for the common case.- Spring Boot note — Spring Security's
SecurityFilterChainis the recommended Spring path.BearerTokenAuthFilter/ApiKeyAuthFiltercan be registered viaFilterRegistrationBeanif desired; the@Beanauto-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-bucketRestGuardwith a fluent builder:permitsPerSecond(int)/permitsPerMinute(int)/permitsPerHour(int),burst(int),keyBy(Function<RestRequest,String>)(default: client IP fromreq.getRemoteAddr()),xForwardedForAware(boolean)(default:false— only enable behind a trusted proxy that re-writes the header),exemptPaths(String...)(default:/healthz,/readyz,/livezto matchHealthMixin),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 RequestswithRetry-Afterin 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.StorageSPI. Pluggable backend for distributed deployments; v1 ships only the in-memory default. The token-bucket math usesSystem.nanoTime()deltas so it is immune to wall-clock drift. -
org.apache.juneau.rest.server.filter.RequestIdFilter—X-Request-Idmint / 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 byRequestIdFilter.
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 methodrecord(String opName, String httpMethod, String uriTemplate, int statusCode, Duration elapsed, Throwable error, String metricName, String metricTags). Called once per@RestOpinvocation (happy path and exception path). TheuriTemplateis the@RestOppath template (/users/{id}) — not the raw concrete URI — so tag cardinality stays bounded. ThemetricNameandmetricTagsparameters (added in an earlier change) carry the per-op overrides from@RestOp(metricName=...)/@RestOp(metricTags=...); both are""when not set. If you implementMetricsRecorderdirectly, add these two trailing parameters to yourrecord(...)override.org.apache.juneau.rest.metrics.NoOpMetricsRecorder— process-wide singletonINSTANCE; resolved when no bean is registered. Drops events on the floor with zero allocations.org.apache.juneau.rest.server.tracing.TracerHook—@FunctionalInterfacewithScope startSpan(RestRequest request). The framework opens the scope before parameter binding and always closes it in afinallyblock; the resolved HTTP status and any thrown throwable are delivered viaScope.setStatusCode(int)/Scope.setError(Throwable)beforeScope.close().org.apache.juneau.rest.server.tracing.Scope—AutoCloseablewithsetStatusCode(int),setError(Throwable), andclose(). The framework guaranteesclose()runs exactly once per call (including parameter-resolution failures and 5xx paths).org.apache.juneau.rest.server.tracing.NoOpTracerHook— process-wide singletonINSTANCE(andNoOpTracerHook.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.AsyncResponseProcessorslotted ahead ofSerializedPojoProcessorin the defaultResponseProcessorList. DetectsCompletionStage<T>return values, callsreq.startAsync()to obtain anAsyncContext, and registers awhenComplete((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 onFuture.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 boundedCompletableFuture.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 anAtomicBooleanstate 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 is504 Gateway Timeoutand theAsyncContextis aborted. 0disables 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+),
RestOpInvokerdispatches the entire handler call — parameter binding, the user method body, and the standard exception-mapping path — onto a virtual thread submitted to a per-resourceExecutors.newVirtualThreadPerTaskExecutor(). The platform "carrier" thread that received the request is parked onCompletableFuture.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
WARNINGis logged viaLogger.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 behindRuntime.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
nulland 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
finallyblock still runs — MDC is cleared even when the callback throws. - SLF4J-only. The bridge covers SLF4J MDC (
org.slf4j.MDC). Log4j2ThreadContextdirect 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=falseto disable globally, or callRestContext.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) — returnstruewhen MDC propagation is enabled. Defaults totrue; overridden by theRestContext.mdcAsyncPropagationenv-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 ajava.util.concurrent.Executorbean by name fromBeanStore. 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
ExecutorinBeanStore,RestContextconstruction throws immediately (misconfiguration is never silently ignored). - MDC propagation compatibility — the
MdcAsyncListenerwraps 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 — newStringannotation member.RestContext.Builder.asyncCompletionExecutor(String)— programmatic setter.RestContext.getAsyncCompletionExecutor()— resource-level resolved executor (ornull).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.
ReactiveResponseProcessoris not wired intoDefaultConfig. It ships injuneau-rest-server-reactivewith aMETA-INF/services/org.apache.juneau.rest.server.processor.ResponseProcessorprovider file;RestContextdiscovers module-contributed response processors viaServiceLoaderand front-loads them ahead ofAsyncResponseProcessor. On a barejuneau-rest-serverthe discovery finds nothing and the processor chain is byte-for-byte the pre-feature default. ReactiveResponseProcessor(org.apache.juneau.rest.reactive, in the opt-injuneau-rest-server-reactivemodule) is the single shared spine. Once on the classpath it is front-loaded ahead ofAsyncResponseProcessorand natively understandsFlow.Publisher. Synchronous returns (String, beans,byte[], …) are untouched.- Single-value reactive types (
Mono,Single,Maybe,Completable) are adapted to aCompletionStageand collapse onto the existingAsyncResponseProcessorpath — 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 requestAccept):- Buffer (default) — all elements collected into a
Listand serialized through the normal chain (e.g. a JSON array). Wrapped in aCompletableFutureso a slow producer never blocks the request thread. - SSE (
text/event-stream) — each element emitted as a Server-Sent-Events frame;SseEventelements are written verbatim, any other element type is JSON-encoded into thedata:field.
- Buffer (default) — all elements collected into a
- Behavior change: reactive NDJSON responses now echo the negotiated
application/jsonl/application/json5ltype (aliases canonicalised) instead of alwaysapplication/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, andrxjavaare all declaredprovidedinjuneau-rest-server-reactive-reactor. Adependency:treeonjuneau-rest-servernever surfaces them. Adapters are discovered lazily viaServiceLoaderand skipped at runtime if their backing library is absent, so a consumer addsjuneau-rest-server-reactive-reactorplus only the reactive library (and version) they actually use. - Extensibility — the
ReactiveStreamsAdapterSPI +Adaptationvalue type let third parties bridge any other reactive library to the coreFlow.Publisher/CompletionStageplumbing without modifyingjuneau-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 viaServiceLoaderwhen thejuneau-rest-server-reactivemodule is on the classpath (noDefaultConfigwiring).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.
| Adapter | Handles | Adapts to |
|---|---|---|
ReactorReactiveAdapter | Mono<T> / Flux<T> | Mono → CompletionStage (via toFuture()); Flux → Flow.Publisher (via FlowAdapters) |
RxJavaReactiveAdapter | Single / Maybe / Completable / Flowable / Observable | single types → CompletionStage; Flowable → Flow.Publisher; Observable → Flowable(BackpressureStrategy.BUFFER) → Flow.Publisher |
ReactiveStreamsPublisherAdapter | generic 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:
HealthIndicatorfunctional SPI (Health check()).Healthvalue type with static builders:Health.up(name),Health.down(name, throwable),Health.unknown(name), plus structured detail entries.HealthProbe/HealthStatusenums for probe categorization and aggregate state.HealthMixinexposing/healthz,/readyz, and/livezwith aggregate body format:{status, components:{name:{status,details}}}.- HTTP status mapping follows Kubernetes conventions:
503if any component isDOWN, else200. HealthProbeSettingsintroduces per-indicator timeout control (default1s); exceptions/timeouts are converted toDOWNwithdetails.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
HealthProbeConfigurationauto-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@Restchain 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 +noInheritopt-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/@RestDestroynow 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.
Messagesdoesn't ride the@Restwalk (it has its own parent-resource-bundle chain semantics), so the newMessages.chain(child, parent)static factory composes two pre-built bundles into a single parent-chained bundle without mutating either input. Themessagesmemoizer 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. DefaultConfigsynthesis is host-only. The framework's synthesizedDefaultConfigentry on a bare@Restresource 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'sDefaultConfigdefault 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.Argsconstructor. The legacy 8-arg back-compat constructor was removed; the canonical 9-arg form now takes an explicitmixinContextflag (host=false, mixin sub-context=true). No external callers exist; all internal call sites injuneau-rest-server,juneau-rest-mock, andjuneau-commonswere 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):
- Programmatic —
RestContext.Args.paths()/RestContext.Builder.paths(String...).nullmeans "inherit from lower rungs";new String[0]is the explicit-clear sentinel. getPaths()virtual getter — added to bothRestServletandRestResource(defaultnull). Returningnullinherits the lower rung;new String[0]is the explicit-clear sentinel; non-empty wins.@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.yamlintegration is planned under a separate work item. Once that lands,$C{key}in@Rest(paths=...)elements will transparently consult Spring'sEnvironmentunder Spring Boot via a Juneau-Config-to-Environmentbridge — no API change here, just an additional resolution source registered with theConfigbean.
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:
| Mixin | SVL variable | Default 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 HTMLrel="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:
| Mixin | Primary default | Removed 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 form | Example | Effective mount |
|---|---|---|
| Bare token | jsp | /jsp/* |
| Leading slash | /jsp | /jsp/* |
| Trailing slash | jsp/ | /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.openapipackage withOpenApiProviderSPI,BasicOpenApiProvider, andBasicOpenApiProviderSession. 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 andRestContext.getOpenApiProvider()/RestContext.getOpenApi(Locale)parallel the Swagger family. - New
RedocUIswap (injuneau-bean-openapi-v3) renders OpenAPI 3.1 documents as a two-column Redoc-style HTML view fortext/htmlrequests. - 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.schemasreuse.BasicOpenApiProviderSessionruns 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 intocomponents.schemasand each occurrence is rewritten to a{"$ref":"#/components/schemas/<name>"}pointer. Names are derived from the schema'stitle(when present and unique) and fall back to synthesizedSchema<N>keys with collision avoidance against existing entries. Bean-typed slots already flow throughdefinitions→components.schemasvia the existing transform, so this primarily covers ad-hoc inline schemas that didn't fall underuseBeanDefs="true".- YAML round-trip coverage.
OpenApiYamlRoundTrip_TestexercisesYamlSerializer.DEFAULT_READABLE→YamlParser.DEFAULTover (a) a hand-builtOpenApibean, (b) the live document produced by aBasicRestServlet-based resource (mounted via the api-docs mixin pack), and (c) the/openapiendpoint served withAccept: application/yaml. Each path asserts structural equality acrossopenapi,info,servers,paths, andcomponents.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:
| Mixin | Default paths | Owns |
|---|---|---|
SwaggerMixin | /api | Swagger v2 spec + SwaggerUI swap. Standard content negotiation. |
SwaggerUiMixin | /swagger | HTML-first mount; @Rest(defaultAccept="text/html") so bare browser requests render the Swagger-UI view. Transitively pulls in SwaggerMixin. |
OpenApiMixin | /openapi, /openapi.json, /openapi.yaml | OpenAPI 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 | /redoc | HTML-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:
| Removed | Replacement |
|---|---|
@Rest(apiFormat="swagger" | "openapi" | "both") | Pick mixins. Both /api and /openapi mount when both spec mixins are present. |
juneau.rest.apiFormat system property | Removed. 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_apiFormat | Removed. |
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 classes | Removed (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-brandedjuneau-favicon.icoshipped on the framework classpath;Cache-Control: max-age=2592000, public(30 days). Builder methods:bytes(byte[]),classpath(String),cacheControl(String).SeoMixin— serves/robots.txtand/sitemap.xml. Default robots body is the RFC 9309 deny-allUser-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 fromMETA-INF/MANIFEST.MF,git.properties, and the running JVM version. Default builder chain isfromManifest().fromGitProperties().fromJavaVersion(); programmatic overrides viaentry(String, String)/entries(Map)suppress the defaults so the importer opts into eachfromXxx()call explicitly. Spring Boot fat-jar tip: passBuilder.fromManifest(importerClassLoader)to read the app's manifest instead of the framework's.WellKnownMixin— reserves/.well-known/security.txt. Returns404 Not Foundby default (RFC 9116 makes the absence ofsecurity.txtitself meaningful); set a body viaBuilder.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 onorg.apache.juneau.rest.server.OpSwaggerthat excludes an operation from the published Swagger / OpenAPI specification. Honored byBasicSwaggerProviderSession(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 legacyBasicRestOperations.getHtdoc(...)method now also carries@OpSwagger(ignore = true)soBasicRestServlet-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 anyHttpResourcereturn value now suppresses the response body when the request method isHEAD, per RFC 7231 §4.3.2: headers (Content-Type, Content-Length, Cache-Control, plus any resource-supplied headers) are emitted identically to the equivalentGET, but the body is not written. This benefits any@RestOp-annotated handler that returns anHttpResource, 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. Default404 Not Founduntil the host's@Rest(debug=...)chain unlocks it (@Rest(debug="conditional")paired with aDebug: truerequest header is the recommended posture). When debug is on, returns a JSON body reflecting the inbound method, path, query string, headers (withAuthorization/Cookie/Set-Cookie/Proxy-Authorization/X-API-Keyredacted to[REDACTED]by default), query params, attributes, and bounded body capture (1 MB cap, configurable viaBuilder.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+MemoryMXBeanstats),POST /admin/cache/flush(run registered hooks), and/admin/ratelimit(registeredRateLimitGuardbeans — both static configuration and live per-key bucket snapshot; see theRateLimitGuard.Storage.snapshot()SPI extension below). Default-deny via the neworg.apache.juneau.rest.server.guard.DenyAllGuarduntil the host registers a@Bean RestGuardListfactory; 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...). Returns404 Not Foundwhen noRateLimitGuardbean is registered.RouteIndexMixin— serves/optionsand/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 haspath,methods(single-element list of the HTTP method),summary,description, anddeprecated. 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). CompanionRestGuardin the existingorg.apache.juneau.rest.server.guardpackage that rejects every request with403 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 RestGuardListoverride 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()onRateLimitGuard.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.BucketStaterecord. Token-bucket-vocabulary per-bucket snapshot:String key,double tokens(current fractional fill level),int remaining(floor(tokens)— mirrors theX-RateLimit-Remainingadvisory header),boolean throttled(convenience flag fortokens < 1.0),Instant lastRequest(wall-clock timestamp of the bucket's last activity). - Bundled
Storage.inMemory()overridessnapshot(). Walks the internalConcurrentHashMap<String,Bucket>and emits oneBucketStateper 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 theConcurrentHashMapweakly-consistent iteration semantics). Bucketgained alastWallMillisfield alongside the existinglastNanossoBucketState.lastRequestis 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 conveniencesnapshot()that delegates togetStorage().snapshot(). Expose existing builder state for operator-facing dashboards. /admin/ratelimitresponse shape —bucketsrenamed tosnapshot,configenriched. The 10.0.0 ops-pack mixin emitted{ "guards": { "<bean-name>": { "config": {"class": "..."}, "buckets": [] } } }with the emptybucketsarray 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": "..."} ] } } }—snapshotentries are sorted ascending bykeyfor stable operator-tooling output. Storage backends that don't overrideStorage.snapshot()(e.g. Redis-backed impls) emit"snapshot": []for their bean entry. Thebucketsfield name was a placeholder that only ever emitted[]; the rename tosnapshotmatches 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()returnsSseResponseSupportfor fluent event/comment writes (sendEvent(...),comment(...),flush(),sendFrom(...)).SseBroadcaster/SseSubscriptionprovide in-memory fan-out for per-connection subscriptions with bounded subscriber queues.SseHeartbeatprovides optional scheduled: pingcomments (when aScheduledExecutorServicebean is available).- New
RestOpArgresolvers allow directSseBroadcasterandSseSubscriptionmethod-parameter injection in@RestOphandlers.
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 withSettingskeys. Serializer/parser session-option settings (juneau.rest.sessionOptions.*) use fields such asSETTING_sessionOptions_rejectWhenAllowlistEmptyandSETTING_sessionOptions_failOnInvalidAllowlistEntry. HTTP wire names for those options are fields onRestSharedConstantsin juneau-rest-common. This supersedes theRestSessionOptionsSettingstype.
RestOpContext: RestOp-group annotations
RestOpContext.getRestOpAnnotations()/findRestOpAnnotations()— Returns every annotation in the@RestOpgroup (including@RestGet,@RestPost, etc.) on the operation method and matching superclass/interface methods, in child-to-parent order; results are memoized untilreset(). TypedAnnotationProvider.find(RestOp.class, MethodInfo)matches only a literal@RestOp, not grouped specializations such as@RestGet; useAnnotationInfo.isInGroup(RestOp.class)(as these methods do) when you need the full group.RestOpContext.getNoInheritKeys()/findNoInherit()— Aggregates non-emptynoInherittokens from everyRestOp-group annotation on the override chain (after SVL resolution and comma expansion), not only the nearest declaration.isInherited(String)onRestOpContextuses this combined set when deciding whether to mergeallowedParserOptions/allowedSerializerOptionskeys from the enclosingRestContext.RestOpContext.getAllowedParserOptionsKeys()/findAllowedParserOptionsKeys()— Annotation-only parser allowlist keys: merges fromRestContextwhenisInherited(PROPERTY_allowedParserOptions), then aggregatesallowedParserOptionsfrom everyRestOp-group annotation on the method override chain (parallel toRestContext, butRestContextuses 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:
defaultRequestAttributesfrom@Restand@RestOpannotations are stored inRequestAttributesand accessed viaRestRequest.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@RestOpannotation, the operation will not inheritallowedSerializerOptionsfrom the enclosing@Restclass annotation - Method-level inheritance:
allowedSerializerOptionsfrom parent method@RestOpannotations (on overridden methods in parent classes) are always inherited, regardless ofnoInheritsettings
@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@RestInithooks or the resource constructor. Replace eachbuilder.xxx(...)call with the equivalent@Rest(xxx=...)attribute or a@Bean(name="xxx")-annotated bean-supplier method.RestOpContext.Builder— same removal. Replacebuilder.guards(...),builder.converters(...), etc. with@RestOp(guards=...),@RestOp(converters=...), etc.RestContextInitrecord replaces the oldRestContext.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 optionalConsumer<BasicBeanStore>hook are bundled in one immutable record. The common top-level case becomesnew RestContext(new RestContextInit(MyResource.class, () -> new MyResource())).@RestInit(RestContext.Builder b)/@RestInit(RestOpContext.Builder b)injection hooks — removed. The supported@RestInitparameter shapes are nowServletConfig,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 onRestContext/RestOpContextthat walks the@Rest/@RestOpannotation chain, system properties, and the@Beanbean store. Results are cached in aMemoizer<T>and invalidated byRestContext.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:
| Tier | 9.4 and earlier | 10.0+ |
|---|---|---|
| 1 (highest) | @Bean (inject) method on the resource | Spring @Bean (or any bean reachable through the bootstrap / overriding-parent bean store) |
| 2 | Spring @Bean (via SpringBeanStore) | @Bean (inject) method on the resource |
| 3 (lowest) | Memoizer-backed framework default | Memoizer-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 duringgetBean(...)/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.RestContextregisters 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 aBasicBeanStore, 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_NAMESskip lists inRestContextare 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 throughbeanStore.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@ConditionalOnMissingBeansemantics on the bean factory. - Skip the Spring
@Bean. Don't declare the type as a Spring@Beanand 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@ConditionalOnPropertyso Spring itself picks the right candidate. The bean store will then surface that candidate toRestContext. - Programmatic registrations that flow through
args.beanStoreConfigurer()(theConsumer<BasicBeanStore>hook onRestContextInit) 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 customBasicBeanStoresubclass).
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 (viasetContent(Object)or equivalent), bypassingAcceptheader negotiation. Passnullto clear the override.Content-Typeis still set by the response processor from the serializer when appropriate; you can callsetContentType(String)first if needed.
Download / Content-Disposition: attachment
RestResponse.downloadAs(String filename)— SetsContent-Disposition: attachmentwith a quotedfilenameparameter so browsers typically download the response body (e.g.res.downloadAs("example.pdf").setContent(bytes)).ContentDisposition.attachment(String)andHttpHeaders.contentDispositionAttachment(String)(juneau-rest-common) — Build a safe header value (escapes\and"in the filename; rejects null/blank names and CR/LF). RFC 5987filename*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)—Stringattribute, tri-state ("true"/"false"/""inherits). When"true", uncaughtBasicHttpExceptions on the resource are rendered asapplication/problem+jsonregardless of the clientAcceptheader (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 defaultresponseProcessorschain afterThrowableProcessor. Handles four content shapes:- A returned
Problembean — serialized directly. Honors clientAcceptstrictly. - A returned or thrown
ProblemException— unwrapped viaProblemException.getProblem(). HonorsAccepton non-opted-in ops (back-compat); flips to error-path semantics (ignoresAccept) on opted-in ops. - A thrown
BasicHttpException— gated byisProblemDetails()(per-op resolution); on error path ignoresAcceptand adapts via the registeredProblemMapper(if any matches) orProblemAdapters.fromException(...)otherwise. - Any other thrown
Throwable— only when an explicitProblemMappermatches its class hierarchy. Mappers that returnnullare skipped and the chain continues; if no mapper matches, the processor returnsNEXT.
- A returned
- Status precedence — when
Problem.getStatus()is non-null, the processor callsRestResponse.setStatus(int)with that value. Otherwise it leaves the existing status alone (RestSession.run()normalizes0→200so per-op default codes still flow through). typefield policy — serialized as-is. Anulltypeis 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—RuntimeExceptioncarrying aProblempayload. Lets handlers throw a custom problem without manually building aBasicHttpException:
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 injuneau-rest-common. Adapts aBasicHttpException(status code, reason phrase,getMessage()) into aProblembean. Used by the processor's default error-path fallback when noProblemMappermatches. 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 customProblemshapes without writing a processor. Two methods:Class<T> getExceptionType()(used for hierarchy-aware dispatch) andProblem map(T exception). Returningnulldefers to the next-most-specific mapper, and ultimately to the built-inProblemAdapters.fromException(...)fallback forBasicHttpExceptionsubclasses.org.apache.juneau.bean.rfc7807.ProblemMapperList— aggregator bean for registering more than one mapper on the same resource. The Juneau@Beanwalk pairs each@Beanfactory with its declared return type, so multiple@Bean public ProblemMapper foo()methods on one class collapse onto the singleProblemMapper.classslot; wrap them in aProblemMapperListto keep them all reachable. The single-mapper case (@Bean public ProblemMapper<MyException> mapper()) keeps working unchanged viaBeanStore.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 ofProblem.title/Problem.detail. The default in-tree implementation isProblemLocalizationStrategy.IDENTITY(pass-through); the processor consults the bean store for a registered strategy on every emission and falls back toIDENTITYwhen 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. TheIDENTITYdefault 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/ thrownProblemException/ mappedThrowableon opted-in ops): emitsapplication/problem+jsonregardless of the clientAccept— the spec encourages this on errors. - Success path (returned
Problem/ returnedProblemException): honors the clientAcceptstrictly. The processor only emitsapplication/problem+jsonifAcceptmatches 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.Validon 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.Validatedand the legacy@javax.validation.Validare 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 Requestwithapplication/jsonbody of the form{ "status": 400, "errors": [ { "path": "...", "message": "...", "constraint": "..." }, ... ] }. The shape mirrors Spring'sMethodArgumentNotValidExceptionpayload at the top level for migration friendliness. - With
@Rest(problemDetails="true")(the RFC 7807 / 9457 path documented above):400 application/problem+jsonwith the standard problem fields (status,title,detail) plus anerrors[]extension carrying the sameValidationViolationlist.
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:
- A user-supplied
jakarta.validation.Validatorbean visible from the resource'sBeanStore(typically contributed via@Bean public Validator validator() { ... }on the resource). Use this when the deployment needs custom message interpolators, group sequences, or a non-defaultConstraintValidatorFactory. - 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. null— the graceful-degradation path described above.
New API surface
org.apache.juneau.rest.validation.ValidationException—BadRequest(400) subclass carrying an immutableList<ValidationViolation>. Thrown fromContentArg/FormDataArg/RequestBeanArgafter the bean is bound but before the handler is called.org.apache.juneau.rest.validation.ValidationViolation— POJO withpath,message,constraint, and optionalinvalidValuefields. 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 underlyingjakarta.validation.Validator.
What's out of scope (v1)
- Method-level validation (
@Validatedon the resource class, constraints on@RestOpreturn values). The integration validates parameter beans only. - Schema-driven validation (JSON-Schema constraints on the request body). That lives in
JsonSchemaGeneratorterritory. Messages.propertiesbridge between Juneau'sMessagesand Jakarta'sValidationMessages.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 theETagresponse header. The string form delegates toEntityTag.of(...)so quoting (strong:"v1"; weak:W/"v1") is validated up front.lastModified(Instant)/lastModified(ZonedDateTime)— sets theLast-Modifiedresponse header in RFC 7231 IMF-fixdate format. Values are truncated to second precision and converted to UTC.cacheControl(String)/cacheControl(CacheControl.Builder)— sets theCache-Controlresponse 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
BasicHttpExceptioncarrying304 Not Modifiedor412 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-Modifiedheaders must be set beforecheckPreconditions(...)is called — the check reads from the already-set response headers so it can compare against the values the handler intends to send. If-Matchalways uses strong comparison: aW/"v1"response ETag never matches anIf-Match, even if the opaque tags are identical.If-None-Matchuses weak comparison:"v1"andW/"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 servlet | Mounts at | Delegates 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(inorg.apache.juneau.rest.server.view), whose single@RestGet(path="/*")op delegates to a new engine-agnosticRawTemplateDispatcherinterface. The fourBasic*Resourceview mixins implementRawTemplateDispatcher, so the standalone and mixin forms route raw.jsp/.ftlh/.mustache/.htmldispatch through the same code and cannot diverge. The non-view companions (StaticFilesServlet,VersionServlet,AdminServlet) hold their matchingBasic*Resourceas a delegate and forward each op to it. AdminServletcarries the same@Rest(guards=DenyAllGuard.class)default-deny posture as the mixin — every admin path returns403until an authRestGuardListis registered.HealthMixinis intentionally not given a companion: it already extendsBasicRestServlet(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): ajakarta.servlet.ServletContainerInitializer,JuneauRestServletContainerInitializer(injuneau-rest-server), auto-mounts concrete@Rest-annotatedRestServletsubclasses that self-declare top-level paths. It is opt-in via thejuneau.rest.auto-register=truecontext init parameter, so it never collides with servlets declared manually inweb.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 viaMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) gated behind the propertyjuneau.rest.auto-register=true(matchIfMissing=false). When active, it contributes a singleServletContextInitializerthat walks everyRestServletbean and mounts each at its self-declared path, skipping any servlet instance already wired into a manualServletRegistrationBean(explicit registrations win) and any servlet that resolves to no top-level paths.JuneauServletRegistrations.forServlet(servlet, beanStore)— an always-available factory helper that returns aServletRegistrationBeanwhose URL mappings are derived from the servlet's own@Rest(paths)(orgetPaths()). Use it from an explicit@Beanmethod 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 anIllegalArgumentException.
See REST Server — Self-Registration for the full topic.
Spring Environment bridge — @Value reads application.yaml
- Added
org.apache.juneau.rest.server.springboot.SpringEnvironmentPropertySource— aPropertySourcethat reads from a SpringEnvironmentso@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 theApplicationContext'sEnvironment: one per context, however many Spring REST servlets share it. The bridge is removed when the context closes (ConfigurableApplicationContext) or when everySpringBeanStoreon the context has beenclear()ed. It is resolved lazily — the constructor doesn't actually callappContext.getEnvironment()until the first lookup — so callers that mock theApplicationContextwithout stubbinggetEnvironment()(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 setjuneau.spring.missCache.disable=trueto turn the cache off. - The bridge sits above the classpath-default
juneau.cfgbridge in theSettingssource stack (later insertion wins), soapplication.yamlshadowsjuneau.cfgwhen both define the same key — matching the typical Spring Boot precedence intuition.
juneau-rest-server-auth-jwt (new module)
jwksCacheTtl migrated to @Value
JwtTokenValidator.Buildernow resolves its defaultjwksCacheTtl(5 minutes) through@Value("${juneau.jwt.jwksCacheTtl:PT5M}") Duration jwksCacheTtlinstead of a hand-rolled initializer. The static factory now routes throughBeanInstantiator, allowing the value to flow through the standardSettingsprecedence chain (system property → env var → registeredPropertySource→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—TokenValidatorthat fetches keys from a JWKS endpoint, verifies signatures, and enforcesiss/aud/exp/nbfwith secure defaults. Fluent builder:jwksUrl(URI)(orjwkSource(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 anyJWKSource<SecurityContext>. Continues serving the last-known-good key set when the upstream fetch fails past TTL, logging aWARNINGso 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,
@Valuekeyjuneau.jwt.jwksEagerRefreshCooldown) — at most one eager refresh fires per cooldown window regardless of how many concurrentkid-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 key | Default | Cap / validation |
|---|---|---|---|
jwksEagerRefreshOnKidMiss(boolean) | juneau.jwt.jwksEagerRefreshOnKidMiss | true | — |
jwksEagerRefreshCooldown(Duration) | juneau.jwt.jwksEagerRefreshCooldown | PT10S | positive; ≤ 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 aClaimsPrincipalmarked withissuerType=SAML. Builder:spEntityId(String),expectedIssuer(String),metadataResolver(MetadataResolver)orsigningCredential(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—AuthFilterthat decodes the inboundSAMLResponse(base64 + DEFLATE for the Redirect binding) at a configurable consumer path and delegates validation. Builder:validator(SamlAssertionValidator),binding(SamlBinding)(defaultPOST),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 factoriesfile(File|Path)andurl(String)that produce initializedMetadataResolverinstances ready to hand toSamlAssertionValidator.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—TokenValidatorthat validates opaque OAuth 2.0 tokens via RFC 7662 introspection. Wraps Nimbus'sTokenIntrospectionRequest+TokenIntrospectionResponse. Builder:introspectionEndpoint(URI),clientId(String),clientSecret(String)/clientSecretSupplier(Supplier<String>),requiredScopes(String...),tokenCache(TokenCache)(defaultBoundedLruTokenCache),cacheTtl(Duration)(default 5m, capped at 1h),httpRequestConfigurator(Consumer<HTTPRequest>),clock(Clock).org.apache.juneau.rest.server.auth.oauth.OAuthFilter—AuthFilterthat authenticates RFC 6750Bearertokens, delegating to anyTokenValidator(e.g.OAuthIntrospectionValidatorfor opaque tokens orJwtTokenValidatorfromjuneau-rest-server-auth-jwtfor 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'sClientCredentialsGrant(RFC 6749 §4.4) with optionalTokenCachereuse keyed by(clientId, scope).org.apache.juneau.rest.server.auth.oauth.flow.OAuthAuthorizationCodeFlow— Wraps Nimbus'sAuthorizationRequest+AuthorizationCodeGrant+TokenRequest(RFC 6749 §4.1) with mandatory PKCE per RFC 7636 (S256). ExposesbuildAuthorizationUrl(state, codeChallenge)andexchange(code, codeVerifier).org.apache.juneau.rest.server.auth.oauth.flow.OAuthRefreshTokenFlow— Wraps Nimbus'sRefreshTokenGrant(RFC 6749 §6).org.apache.juneau.rest.server.auth.oauth.flow.OAuthResourceOwnerFlow— Wraps Nimbus'sResourceOwnerPasswordCredentialsGrant(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; preferOAuthAuthorizationCodeFlow(with PKCE).org.apache.juneau.rest.server.auth.oauth.oidc.OidcDiscoveryClient+OidcMetadata— OIDC.well-known/openid-configurationdiscovery via Nimbus'sOIDCProviderMetadata.resolve(Issuer). Returns a Juneau-native immutable record.
Deferred
- OIDC Relying Party login flow — now shipped as the
juneau-rest-server-auth-oidc-rpmodule (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 thejuneau-rest-server-auth-jwtsection 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@Restresource calls:startLogin(generate + storestate/nonce/PKCE verifier, 302 → IdP),completeLogin(parse callback, single-use-consumestate, exchange code, validate ID token, create session, 302 → app),logout(invalidate session + clear cookie + redirect through the IdPend_session_endpoint),refresh(rotating-refresh-token grant), andbackChannelLogout(IdP-pushedlogout_token→ server-side session revocation). Single-IdP per instance. Nimbus is hidden behind the facade; escape hatches arehttpRequestConfigurator(Consumer<HTTPRequest>)andauthenticationRequestCustomizer(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 exposessupportsServerSideRevocation()/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 bysubandsid, 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 (theBoundedLruTokenCacheeviction shape).org.apache.juneau.rest.server.auth.oidc.rp.OidcSessionAuthFilter—AuthFilterthat resolves the session cookie into aClaimsPrincipalon each request; fail-open to unauthenticated. Roles come from a configurable claim (default"scope"), matchingOAuthFilter's convention.org.apache.juneau.rest.server.auth.oidc.rp.IdTokenValidatorAdapter— Wraps Nimbus'sIDTokenValidatorfor full OIDC ID-token validation (signature vs JWKS,iss,aud/azp,exp/iat,nonce) with a strict algorithm allowlist (default[RS256, ES256];nonerejected) 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 OIDCAuthenticationRequest(carrying thenonce) alongside the existing plain-OAuthbuildAuthorizationUrl(...). Required so the relying party can bind anonceinto 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—MetricsRecorderimplementation. Builds a MicrometerTimernamedhttp.server.requests(overridable via the two-arg constructor) with four tags per Spring Boot'sWebMvcMetricsFilterconvention:method(uppercased HTTP method),uri(the@RestOppath template — bounded cardinality),status(HTTP response status),exception(the thrown exception's simple-name, orNoneon the happy path). Records the elapsedDurationonce 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—TracerHookimplementation. Each request becomes a singleSpanKind.SERVERspan named after the HTTP method (GET,POST, …) per the OpenTelemetry HTTP semantic conventions, with attributeshttp.request.method,http.response.status_code, andhttp.route(the@RestOppath template, bounded cardinality). On the exception path, the throwable is recorded viaSpan.recordException(...), the span status is set toStatusCode.ERROR, and anexception.typeattribute carries the exception's simple-name. Constructors: no-arg (usesGlobalOpenTelemetry.get()per an earlier change resolved decision #5),OtelTracerHook(OpenTelemetry), andOtelTracerHook(Tracer, TextMapPropagator)for tests.org.apache.juneau.rest.server.tracing.otel.RestRequestTextMapGetter—TextMapGetter<RestRequest>for OpenTelemetry context extraction. Reads the standard W3Ctraceparent/tracestateheaders (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-apifrom1.43.0to1.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.jsprequests to the container's JSP engine, and contributesJspViewRendererto the mixin's response-processor chain. Builder API:JspMixin.create().basePath("/WEB-INF/views/").build().org.apache.juneau.rest.server.view.jsp.JspView—Viewimplementation. Immutable value class; fluent builder:JspView.of("hello.jsp").attr("name", name).header("Cache-Control", "no-store").attr(...)rejectsnullvalues because Servlet-specsetAttribute(name, null)deletes the binding rather than setting it.org.apache.juneau.rest.server.view.jsp.JspViewRenderer—ResponseProcessorthat detectsJspView-typed return values and dispatches viaServletContext.getRequestDispatcher(...).forward(...). When no JSP engine is on the classpath, surfacesNO_ENGINE_DIAGNOSTICnaming 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.htmltemplates under the configured base path, and contributesThymeleafViewRendererto 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 /Configkey). Path-traversal protection viaFileUtils.resolveVirtualPathSafely(...)— both direct and nested..traversal returns HTTP 403 at the handler boundary.org.apache.juneau.rest.server.view.thymeleaf.ThymeleafView—Viewimplementation. Immutable value class; fluent builder:ThymeleafView.of("hello").attr("name", name).header("Cache-Control", "no-store").attr(...)rejectsnullvalues because a ThymeleafContextsilently dropsnull-valued bindings, which masks caller bugs.org.apache.juneau.rest.server.view.thymeleaf.ThymeleafViewRenderer—ResponseProcessorthat detectsThymeleafView-typed return values and asks the configuredorg.thymeleaf.TemplateEngineto render them directly onto the response writer. When no Thymeleaf engine is on the classpath, surfacesNO_ENGINE_DIAGNOSTICnaming 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:thymeleaffrom3.1.3.RELEASEto3.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.mustachetemplates under the configured base path, and contributesMustacheViewRendererto 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 /Configkey). Path-traversal protection viaFileUtils.resolveVirtualPathSafely(...)— both direct and nested..traversal returns HTTP 403 at the handler boundary.org.apache.juneau.rest.server.view.mustache.MustacheView—Viewimplementation. Immutable value class; fluent builder:MustacheView.of("hello").attr("name", name).header("Cache-Control", "no-store").attr(...)rejectsnullvalues to surface caller bugs early.org.apache.juneau.rest.server.view.mustache.MustacheViewRenderer—ResponseProcessorthat detectsMustacheView-typed return values and asks the configuredcom.github.mustachejava.MustacheFactoryto compile and execute them directly onto the response writer. When no Mustache engine is on the classpath, surfacesNO_ENGINE_DIAGNOSTICnaming 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/.ftlhtemplates under the configured base path, and contributesFreemarkerViewRendererto 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 /Configkey). Path-traversal protection viaFileUtils.resolveVirtualPathSafely(...)— both direct and nested..traversal returns HTTP 403 at the handler boundary. ThecacheTemplates(boolean)flag configures the bridge-defaultConfiguration'sTemplateUpdateDelayMilliseconds(cache forever vs. re-check every render).org.apache.juneau.rest.server.view.freemarker.FreemarkerView—Viewimplementation. Immutable value class; fluent builder:FreemarkerView.of("hello.ftlh").attr("name", name).header("Cache-Control", "no-store").attr(...)rejectsnullvalues to surface caller bugs early.org.apache.juneau.rest.server.view.freemarker.FreemarkerViewRenderer—ResponseProcessorthat detectsFreemarkerView-typed return values and asks the configuredfreemarker.template.ConfigurationtogetTemplate(name).process(dataModel, writer)directly onto the response writer. When no FreeMarker engine is on the classpath, surfacesNO_ENGINE_DIAGNOSTICnaming the missing dependency (with a pointer tospring-boot-starter-freemarkerfor 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.