From 5e707706b3524983a635a0c9fa616e02b0c78fd6 Mon Sep 17 00:00:00 2001 From: evanchooly Date: Mon, 5 Oct 2026 10:27:26 -0400 Subject: [PATCH] Wrap failures in the woven final-field writer like the runtime accessor The __writeXxx method woven for final fields called Field.set without handling its checked IllegalAccessException, so a failure escaped undeclared. Wrap the call in a try/catch that rethrows RuntimeException("Failed to set final field ''", cause), matching NestmateAccessorGenerator. Fixes #4355 --- .../generator/AddFieldAccessorMethods.java | 36 ++++++++++++++----- .../parser/generator/TestGeneration.java | 21 +++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/dev/morphia/critter/parser/generator/AddFieldAccessorMethods.java b/core/src/main/java/dev/morphia/critter/parser/generator/AddFieldAccessorMethods.java index 8624037d4f8..ee5317f2ff3 100644 --- a/core/src/main/java/dev/morphia/critter/parser/generator/AddFieldAccessorMethods.java +++ b/core/src/main/java/dev/morphia/critter/parser/generator/AddFieldAccessorMethods.java @@ -24,6 +24,7 @@ public class AddFieldAccessorMethods extends AccessorMethods { private static final ClassDesc CD_FIELD = ClassDesc.of("java.lang.reflect.Field"); private static final ClassDesc CD_CRITTER = ClassDesc.of("dev.morphia.critter.Critter"); + private static final ClassDesc CD_RUNTIME_EXCEPTION = ClassDesc.of("java.lang.RuntimeException"); private final List fields; /** @@ -123,15 +124,32 @@ private static void writeFinalField(ClassBuilder classBuilder, ClassDesc entityD cod.dup(); cod.putstatic(entityDesc, cacheName, CD_FIELD); cod.labelBinding(cached); - cod.aload(0); - cod.loadLocal(kind, 1); - if (kind != TypeKind.REFERENCE) { - ClassDesc wrapper = ClassDesc.of(GenerationUtils.PRIMITIVE_TO_WRAPPER.get(fieldDesc.displayName())); - cod.invokestatic(wrapper, "valueOf", MethodTypeDesc.of(wrapper, fieldDesc)); - } - cod.invokevirtual(CD_FIELD, "set", MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object, - ConstantDescs.CD_Object)); - cod.return_(); + int fieldSlot = cod.allocateLocal(TypeKind.REFERENCE); + cod.astore(fieldSlot); + // Wrap failures the same way the runtime accessor (NestmateAccessorGenerator) does, so both tiers fail alike + // and Field.set's checked IllegalAccessException doesn't escape undeclared. + cod.trying(tryBody -> { + tryBody.aload(fieldSlot); + tryBody.aload(0); + tryBody.loadLocal(kind, 1); + if (kind != TypeKind.REFERENCE) { + ClassDesc wrapper = ClassDesc.of(GenerationUtils.PRIMITIVE_TO_WRAPPER.get(fieldDesc.displayName())); + tryBody.invokestatic(wrapper, "valueOf", MethodTypeDesc.of(wrapper, fieldDesc)); + } + tryBody.invokevirtual(CD_FIELD, "set", MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_Object, + ConstantDescs.CD_Object)); + tryBody.return_(); + }, catches -> catches.catching(ConstantDescs.CD_Exception, catchBody -> { + int exceptionSlot = catchBody.allocateLocal(TypeKind.REFERENCE); + catchBody.astore(exceptionSlot); + catchBody.new_(CD_RUNTIME_EXCEPTION); + catchBody.dup(); + catchBody.ldc("Failed to set final field '%s'".formatted(field.name())); + catchBody.aload(exceptionSlot); + catchBody.invokespecial(CD_RUNTIME_EXCEPTION, "", + MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String, ConstantDescs.CD_Throwable)); + catchBody.athrow(); + })); }); } } diff --git a/core/src/test/java/dev/morphia/critter/parser/generator/TestGeneration.java b/core/src/test/java/dev/morphia/critter/parser/generator/TestGeneration.java index 4f447ba3c38..bd9080b58ed 100644 --- a/core/src/test/java/dev/morphia/critter/parser/generator/TestGeneration.java +++ b/core/src/test/java/dev/morphia/critter/parser/generator/TestGeneration.java @@ -1,6 +1,7 @@ package dev.morphia.critter.parser.generator; import java.io.File; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -171,6 +172,26 @@ public void testGeneratorFinalFields() throws Exception { assertRoundTrip(model, entity, "count", 42); } + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testGeneratorFinalFieldWriteFailureIsWrapped() throws Exception { + EntityModel model = generateModel(FinalFieldsExample.class); + Class woven = critterClassLoader.loadClass(FinalFieldsExample.class.getName()); + Object entity = woven.getConstructor(String.class, int.class).newInstance("initial", 1); + + // Seed the woven writer's Field cache with a copy that was never made accessible, so Field.set throws the + // checked IllegalAccessException. + Field cache = woven.getDeclaredField("__fieldCode"); + cache.setAccessible(true); + cache.set(null, woven.getDeclaredField("code")); + + PropertyAccessor accessor = model.getProperty("code").getAccessor(); + Throwable thrown = Assertions.assertThrows(Throwable.class, () -> accessor.set(entity, "updated")); + Assertions.assertEquals(RuntimeException.class, thrown.getClass()); + Assertions.assertEquals("Failed to set final field 'code'", thrown.getMessage()); + Assertions.assertInstanceOf(IllegalAccessException.class, thrown.getCause()); + } + @Test public void testRuntimeGeneratorFinalFields() throws Exception { // A fresh loader: runtime mode leaves the entity class alone, so it must resolve to the application's copy.