This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
commit e3adb833a70ce3ba85f24a7eb59e842611b8421a Author: James Bognar <[email protected]> AuthorDate: Sun Aug 16 15:32:57 2026 -0400 READY-363: Fix RestContext annotationWork init-ordering NPE in framework-bean memoizers --- .../org/apache/juneau/rest/server/RestContext.java | 24 ++++++++++++++++------ .../rest/server/RestContext_Construction_Test.java | 18 ++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java index 96fae5558c..9c253f5843 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java @@ -2537,6 +2537,20 @@ public class RestContext extends Context { // dependencies through the bean store. registerFrameworkDefaults(beanStore); + var rci2 = ClassInfo.of(resourceClass); + + // Build the annotation work list as early as possible in construction — immediately after + // registerFrameworkDefaults() registers the framework-bean default suppliers above, and + // before any step below (the @Bean field/method scan, @RestInit hooks, or the @Bean field + // back-fill) that can trigger a beanStore lookup and force-evaluate a creator/builder memoizer + // that reads annotationWork directly (partSerializerCreator, partParserCreator, + // jsonSchemaGeneratorBuilder, beanContextBuilder). Building it this late used to leave those + // memoizers exposed to an NPE whenever something forced them early — e.g. a @Bean static + // witness field of type HttpPartSerializer, or an @RestInit method parameter of one of the + // affected framework types — since annotationWork was still null at that point. + var vrs = getBootstrapVarResolver().createSession(); + annotationWork = AnnotationWorkList.of(vrs, rstream(AnnotationProvider.INSTANCE.find(rci2)).filter(CONTEXT_APPLY_FILTER)); + // For mixin sub-contexts, the bean store is parent-linked to the host's full beanStore so that // host-declared @Bean factory results (e.g. @Bean(name="db") HealthIndicator dbIndicator()) are // visible through the mixin's lookup chain. But the parent walk also picks up the host's @@ -2560,8 +2574,6 @@ public class RestContext extends Context { // tier-4 default / parent-wins semantics, so a Spring/parent-supplied bean still wins for them. beanStore.addBean(RestContextProperties.class, getRestContextProperties()); - var rci2 = ClassInfo.of(resourceClass); - // Register @Bean fields that already have a value. // @formatter:off rci2.getAllFields().stream() @@ -2682,10 +2694,10 @@ public class RestContext extends Context { // Config injection visible to SVL without firing the full runtime VarResolver memoizer. paths = resolveMountPaths(builder, resource.get(), getBootstrapVarResolver(), beanStore, getRestAnnotations()); - // Build annotation work list, then trigger beanContextBuilder (which applies it). - var vrs = getBootstrapVarResolver().createSession(); - annotationWork = AnnotationWorkList.of(vrs, rstream(AnnotationProvider.INSTANCE.find(rci2)).filter(CONTEXT_APPLY_FILTER)); - beanContextBuilder.get(); // force init with annotationWork now set + // annotationWork was already built above (right after registerFrameworkDefaults(), well before + // any step that could force-evaluate a memoizer reading it) — force-init beanContextBuilder + // here so annotation work is applied before the builder is used by any other memoizer. + beanContextBuilder.get(); // @formatter:off beanStore diff --git a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/RestContext_Construction_Test.java b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/RestContext_Construction_Test.java index 7874ca984a..dd7631449c 100644 --- a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/RestContext_Construction_Test.java +++ b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/RestContext_Construction_Test.java @@ -24,6 +24,7 @@ import java.util.logging.*; import org.apache.juneau.commons.inject.*; import org.apache.juneau.commons.svl.*; +import org.apache.juneau.marshall.httppart.*; import org.apache.juneau.marshall.oapi.*; import org.apache.juneau.rest.server.metrics.*; import org.apache.juneau.rest.server.openapi.*; @@ -311,4 +312,21 @@ class RestContext_Construction_Test extends org.apache.juneau.TestBase { assertEquals(Fix_Bare.class.getName(), ctx.getLogger().getName()); assertNotNull(Level.INFO); } + + //----------------------------------------------------------------------------------------------------------- + // m - a @Bean static witness field forces a framework-bean creator memoizer (partSerializerCreator) during + // the @Bean field back-fill step, well before annotationWork used to be assigned in the constructor + //----------------------------------------------------------------------------------------------------------- + + @Rest + static class Fix_PartSerializerWitness { + @Bean static HttpPartSerializer partSerializerCapture; + } + + @Test void m01_beanStaticWitnessField_forcesPartSerializerCreator_duringBackfill_doesNotNpe() throws Exception { + Fix_PartSerializerWitness.partSerializerCapture = null; + var ctx = new RestContext(argsOf(Fix_PartSerializerWitness.class, Fix_PartSerializerWitness::new)); + assertNotNull(Fix_PartSerializerWitness.partSerializerCapture); + assertSame(ctx.getPartSerializer(), Fix_PartSerializerWitness.partSerializerCapture); + } }
