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);
+       }
 }

Reply via email to