Replace ASM + Gizmo with Java ClassFile API backport#4282
Open
evanchooly wants to merge 18 commits into
Open
Conversation
Replaces org.ow2.asm and io.quarkus.gizmo dependencies with io.github.dmlloyd:jdk-classfile-backport:25.1 in the critter bytecode generation pipeline. Key changes: - Add jdk-classfile-backport dependency; remove gizmo/asm-tree/asm-util from compile scope (keep asm as test-scoped for ClassfileOutput) - New FieldInfo/MethodInfo records replacing ASM FieldNode/MethodNode - BaseGenerator, AddFieldAccessorMethods, AddMethodAccessorMethods rewritten using ClassFile API transforming pattern - PropertyAccessorGenerator, VarHandleAccessorGenerator rewritten with ClassFile.of().build() - PropertyModelGenerator, GizmoEntityModelGenerator rewritten using ClassFile API; now use Java reflection for annotation/type data instead of ASM SignatureReader/AnnotationNode - GizmoExtensions: new ClassFile API utilities (emitAnnotationOnStack, emitClassRef, emitTypeData, asClass, rawTypeDesc, typeDataFromDesc) - AnnotationNodeExtensions.kt simplified to generate a stub class that no longer depends on ASM/Gizmo - PropertyFinder, ExtensionFunctions, CritterParser updated to use ClassModel/FieldInfo/MethodInfo - Tests updated: TypesTest uses ClassDesc, TestGizmoGeneration removes Gizmo-specific tests, uses ClassFile API for MethodInfo discovery https://claude.ai/code/session_01TLEDKhoUzorXDYRAqVLLYE
136782c to
fc3c714
Compare
- Filter ACC_BRIDGE methods in PropertyFinder.isGetter() and PropertyModelGenerator.findMethod() to prevent compiler-generated covariant bridge methods from overriding the real getter's return type - Remove checkcast to non-public property types in VarHandleAccessorGenerator.set() to avoid IllegalAccessError when accessor classes in CritterClassLoader access inner entity classes - Fix VarHandleAccessorGenerator.set() final-field path to not dead-reference the entity class - Fix GizmoExtensions.emitClassRef() for primitive types using getstatic WrapperClass.TYPE - Merge setter annotations into PropertyModelGenerator's annotation map so annotations like @Version and @text on setter methods are captured for METHODS-mode property discovery - Add CritterPropertyModel.registerFieldAnnotations/registerMethodAnnotations to register non-Morphia annotations (e.g. @nonnull) via reflection in generated property model constructors - Wrap CritterParser lists in Collections.unmodifiableList and fix getter field-type check
Contributor
There was a problem hiding this comment.
Pull request overview
This PR migrates Morphia’s critter bytecode generation pipeline away from ASM/Gizmo to the JDK ClassFile API backport (io.github.dmlloyd:jdk-classfile-backport), updating both the runtime generators and associated tests/build tooling to match the new model.
Changes:
- Introduces
FieldInfo/MethodInforecords and switches class parsing from ASMClassNode/*NodetoClassModel/attributes. - Rewrites accessor/model generators to emit/transform bytecode using
ClassFile.of().build()andtransformClass(...). - Removes ASM/Gizmo-based build-plugin generators and updates tests to validate the new parsing/generation approach.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pom.xml | Adds backport dependency/version management (and currently still manages Gizmo). |
| core/pom.xml | Swaps compile dependency from Gizmo to classfile-backport; moves ASM artifacts to test scope. |
| core/src/test/java/dev/morphia/critter/parser/TypesTest.java | Updates descriptor/class conversion tests to use ClassDesc. |
| core/src/test/java/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.java | Updates generator tests to parse methods/annotations via ClassFile API. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/critter/CritterPropertyModel.java | Adds reflection-based annotation registration helpers for generated models. |
| core/src/main/java/dev/morphia/critter/parser/PropertyFinder.java | Replaces ASM-based property discovery with ClassModel + attributes. |
| core/src/main/java/dev/morphia/critter/parser/MethodInfo.java | New record replacing ASM MethodNode as a carrier. |
| core/src/main/java/dev/morphia/critter/parser/java/CritterParser.java | Removes ASMifier utilities; keeps descriptor helpers. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/VarHandleAccessorGenerator.java | Rewrites VarHandle accessor generation using ClassFile API. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/PropertyModelGenerator.java | Rewrites property model generation using ClassFile API + reflection for types/annotations. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/PropertyAccessorGenerator.java | Rewrites synthetic-method delegating accessors using ClassFile API. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/GizmoExtensions.java | Replaces ASM/Gizmo utilities with ClassFile-based emission helpers (annotations, TypeData, class refs). |
| core/src/main/java/dev/morphia/critter/parser/gizmo/GizmoEntityModelGenerator.java | Rewrites entity model generation using ClassFile API and reflection-sourced annotations. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/CritterGizmoGenerator.java | Switches orchestration to ClassModel parsing and new generators. |
| core/src/main/java/dev/morphia/critter/parser/gizmo/BaseGizmoGenerator.java | Removes Gizmo ClassCreator plumbing; becomes a lightweight base. |
| core/src/main/java/dev/morphia/critter/parser/FieldInfo.java | New record replacing ASM FieldNode as a carrier. |
| core/src/main/java/dev/morphia/critter/parser/ExtensionFunctions.java | Updates getter→property naming helper to use MethodInfo + MethodTypeDesc. |
| core/src/main/java/dev/morphia/critter/parser/asm/BaseGenerator.java | Replaces ASM read/filter setup with ClassFile parsing entry point. |
| core/src/main/java/dev/morphia/critter/parser/asm/AddMethodAccessorMethods.java | Rewrites method-based accessor injection using transformClass + drop/endHandler. |
| core/src/main/java/dev/morphia/critter/parser/asm/AddFieldAccessorMethods.java | Rewrites field-based accessor injection using transformClass + drop/endHandler. |
| core/src/main/java/dev/morphia/critter/Critter.java | Replaces stored annotation types with descriptor strings. |
| build-plugins/src/main/kotlin/util/AnnotationNodeExtensions.kt | Removed (ASM/Gizmo-based annotation builder generator no longer needed). |
| build-plugins/src/main/java/util/KotlinAnnotationExtensions.java | Removed (no longer required for old annotation emission approach). |
| build-plugins/src/main/java/util/AsmBuilders.java | Removed (ASM builder generation removed). |
Comment on lines
116
to
121
| private ClassModel readClassModel(Class<?> type) { | ||
| String resourceName = "%s.class".formatted(type.getName().replace('.', '/')); | ||
| InputStream inputStream = type.getClassLoader().getResourceAsStream(resourceName); | ||
| ClassLoader cl = type.getClassLoader() != null ? type.getClassLoader() | ||
| : ClassLoader.getSystemClassLoader(); | ||
| InputStream inputStream = cl.getResourceAsStream(resourceName); | ||
| if (inputStream == null) { |
Comment on lines
42
to
51
| public GizmoEntityModelGenerator generate(Class<?> type, CritterClassLoader critterClassLoader, boolean runtimeMode) { | ||
| ClassNode classNode = new ClassNode(); | ||
| String resourceName = "%s.class".formatted(type.getName().replace('.', '/')); | ||
| java.io.InputStream inputStream = type.getClassLoader().getResourceAsStream(resourceName); | ||
| InputStream inputStream = type.getClassLoader().getResourceAsStream(resourceName); | ||
| if (inputStream == null) { | ||
| throw new IllegalArgumentException("Could not find class file for %s".formatted(type.getName())); | ||
| } | ||
| ClassModel classModel; | ||
| try { | ||
| new ClassReader(inputStream).accept(classNode, 0); | ||
| classModel = ClassFile.of().parse(inputStream.readAllBytes()); | ||
| } catch (IOException e) { |
Comment on lines
+28
to
+33
| /** | ||
| * Returns the access flags used when declaring the generated class. | ||
| * | ||
| * @return the ACC_PUBLIC | ACC_SUPER access flags | ||
| * Reads the class file bytes for the given entity, excluding any existing __read/__write synthetic methods. | ||
| */ | ||
| protected int accessFlags() { | ||
| return ACC_PUBLIC | ACC_SUPER; | ||
| } | ||
|
|
||
| /** | ||
| * Reads a class into the classWriter, filtering out any existing __read/__write synthetic | ||
| * methods so that accessor generation is idempotent across repeated plugin runs. | ||
| * | ||
| * @param entity the class to read | ||
| */ | ||
| protected void readClassFiltering(Class<?> entity) { | ||
| protected ClassModel readClassFiltering() { | ||
| String resourceName = "%s.class".formatted(entity.getName().replace('.', '/')); | ||
| java.io.InputStream inputStream = entity.getClassLoader().getResourceAsStream(resourceName); | ||
| InputStream inputStream = entity.getClassLoader().getResourceAsStream(resourceName); |
Comment on lines
+28
to
+32
| Field field = current.getDeclaredField(fieldName); | ||
| for (Annotation ann : field.getDeclaredAnnotations()) { | ||
| model.annotation(ann); | ||
| } | ||
| return; |
Comment on lines
+46
to
+53
| for (Method m : current.getDeclaredMethods()) { | ||
| if (m.getName().equals(getterName) && m.getParameterCount() == 0 && !m.isBridge()) { | ||
| for (Annotation ann : m.getDeclaredAnnotations()) { | ||
| model.annotation(ann); | ||
| } | ||
| return; | ||
| } | ||
| } |
Comment on lines
419
to
433
| private void emitLoadClass(io.github.dmlloyd.classfile.CodeBuilder cod, String typeName, ClassDesc desc) { | ||
| if (isPrimitive()) { | ||
| cod.loadConstant(desc); | ||
| } else { | ||
| emitGetterHandleLookup(tryBlock, privateLookup, entityClass, handleDesc); | ||
| if (setterHandleDesc != null) { | ||
| emitSetterHandleLookup(tryBlock, privateLookup, entityClass, setterHandleDesc); | ||
| } | ||
| GizmoExtensions.emitClassRef(cod, classForName(typeName)); | ||
| } | ||
|
|
||
| var catchBlock = tryBlock.addCatch(ReflectiveOperationException.class); | ||
| ResultHandle ex = catchBlock.getCaughtException(); | ||
| ResultHandle wrapped = catchBlock.newInstance( | ||
| ofConstructor(RuntimeException.class, Throwable.class), ex); | ||
| catchBlock.throwException(wrapped); | ||
|
|
||
| constructor.returnVoid(); | ||
| constructor.close(); | ||
| } | ||
|
|
||
| /** | ||
| * Emits bytecode to obtain a private {@link MethodHandles.Lookup} for the entity class. | ||
| * The entity class is loaded via the thread context class loader to avoid classloader mismatches. | ||
| * | ||
| * @return a two-element array: {@code [entityClass, privateLookup]} | ||
| */ | ||
| private ResultHandle[] emitPrivateLookup(TryBlock tryBlock) { | ||
| ResultHandle callerLookup = tryBlock.invokeStaticMethod( | ||
| ofMethod(MethodHandles.class, "lookup", MethodHandles.Lookup.class)); | ||
|
|
||
| // Load entity class via TCCL to avoid classloader mismatch | ||
| ResultHandle currentThread = tryBlock.invokeStaticMethod( | ||
| ofMethod(Thread.class, "currentThread", Thread.class)); | ||
| ResultHandle tccl = tryBlock.invokeVirtualMethod( | ||
| ofMethod(Thread.class, "getContextClassLoader", ClassLoader.class), | ||
| currentThread); | ||
| ResultHandle entityClass = tryBlock.invokeStaticMethod( | ||
| ofMethod(Class.class, "forName", Class.class, String.class, boolean.class, ClassLoader.class), | ||
| tryBlock.load(entity.getName()), | ||
| tryBlock.load(true), | ||
| tccl); | ||
| ResultHandle privateLookup = tryBlock.invokeStaticMethod( | ||
| ofMethod(MethodHandles.class, "privateLookupIn", MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class), | ||
| entityClass, | ||
| callerLookup); | ||
| return new ResultHandle[] { entityClass, privateLookup }; | ||
| } | ||
|
|
||
| private void emitVarHandleLookup(TryBlock tryBlock, ResultHandle privateLookup, ResultHandle entityClass, | ||
| FieldDescriptor handleDesc) { | ||
| ResultHandle fieldTypeClass = tryBlock.loadClass(propertyType); | ||
| ResultHandle handle = tryBlock.invokeVirtualMethod( | ||
| ofMethod(MethodHandles.Lookup.class, "findVarHandle", VarHandle.class, Class.class, String.class, Class.class), | ||
| privateLookup, | ||
| entityClass, | ||
| tryBlock.load(propertyName), | ||
| fieldTypeClass); | ||
| tryBlock.writeInstanceField(handleDesc, tryBlock.getThis(), handle); | ||
| } | ||
|
|
||
| private void emitGetterHandleLookup(TryBlock tryBlock, ResultHandle privateLookup, ResultHandle entityClass, | ||
| FieldDescriptor handleDesc) { | ||
| ResultHandle returnTypeClass = tryBlock.loadClass(propertyType); | ||
| ResultHandle getterMethodType = tryBlock.invokeStaticMethod( | ||
| ofMethod(MethodType.class, "methodType", MethodType.class, Class.class), | ||
| returnTypeClass); | ||
| ResultHandle getterHandle = tryBlock.invokeVirtualMethod( | ||
| ofMethod(MethodHandles.Lookup.class, "findVirtual", MethodHandle.class, Class.class, String.class, MethodType.class), | ||
| privateLookup, | ||
| entityClass, | ||
| tryBlock.load(getterName), | ||
| getterMethodType); | ||
| tryBlock.writeInstanceField(handleDesc, tryBlock.getThis(), getterHandle); | ||
| } | ||
|
|
||
| private void emitSetterHandleLookup(TryBlock tryBlock, ResultHandle privateLookup, ResultHandle entityClass, | ||
| FieldDescriptor setterHandleDesc) { | ||
| ResultHandle voidClass = tryBlock.loadClass(void.class); | ||
| ResultHandle paramTypeClass = tryBlock.loadClass(propertyType); | ||
| ResultHandle setterMethodType = tryBlock.invokeStaticMethod( | ||
| ofMethod(MethodType.class, "methodType", MethodType.class, Class.class, Class.class), | ||
| voidClass, | ||
| paramTypeClass); | ||
| ResultHandle setterHandle = tryBlock.invokeVirtualMethod( | ||
| ofMethod(MethodHandles.Lookup.class, "findVirtual", MethodHandle.class, Class.class, String.class, MethodType.class), | ||
| privateLookup, | ||
| entityClass, | ||
| tryBlock.load(setterName), | ||
| setterMethodType); | ||
| tryBlock.writeInstanceField(setterHandleDesc, tryBlock.getThis(), setterHandle); | ||
| } | ||
|
|
||
| private void get(FieldDescriptor handleDesc) { | ||
| var method = getCreator().getMethodCreator( | ||
| ofMethod(generatedType, "get", Object.class.getName(), Object.class.getName())); | ||
| method.setSignature( | ||
| forMethod() | ||
| .addTypeParameter(typeVariable("S")) | ||
| .setReturnType(classType(propertyType)) | ||
| .addParameterType(typeVariable("S")) | ||
| .build()); | ||
| method.setParameterNames(new String[] { "model" }); | ||
|
|
||
| ResultHandle model = method.getMethodParam(0); | ||
|
|
||
| // Guard against null model (e.g. unwrapped lazy proxy for a missing/deleted reference) | ||
| BranchResult nullCheck = method.ifNull(model); | ||
| try (BytecodeCreator nullBranch = nullCheck.trueBranch()) { | ||
| nullBranch.returnValue(nullBranch.loadNull()); | ||
| } | ||
|
|
||
| ResultHandle handleRef = method.readInstanceField(handleDesc, method.getThis()); | ||
|
|
||
| ResultHandle result; | ||
| if (isFieldBased) { | ||
| result = method.invokeVirtualMethod( | ||
| ofMethod(VarHandle.class, "get", Object.class.getName(), Object.class.getName()), | ||
| handleRef, | ||
| model); | ||
| } else { | ||
| result = method.invokeVirtualMethod( | ||
| ofMethod(MethodHandle.class, "invoke", Object.class.getName(), Object.class.getName()), | ||
| handleRef, | ||
| model); | ||
| private static Class<?> classForName(String name) { | ||
| try { | ||
| return Class.forName(name); | ||
| } catch (ClassNotFoundException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
|
|
||
| ResultHandle boxed = isPrimitive() ? method.smartCast(result, getWrapperType()) : result; | ||
| method.returnValue(boxed); | ||
| } |
Comment on lines
+364
to
+372
| }, catches -> catches.catching(ClassDesc.of("java.lang.Exception"), catchBody -> { | ||
| catchBody.astore(3); | ||
| catchBody.new_(rteDesc); | ||
| catchBody.dup(); | ||
| catchBody.ldc("Failed to set final field '%s'".formatted(propertyName)); | ||
| catchBody.invokespecial(rteDesc, "<init>", | ||
| MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V")); | ||
| catchBody.athrow(); | ||
| })); |
Comment on lines
+374
to
+383
| <groupId>io.github.dmlloyd</groupId> | ||
| <artifactId>jdk-classfile-backport</artifactId> | ||
| <version>${classfile.backport.version}</version> | ||
| </dependency> |
…type resolution hasSetter() and emitLoadClass() both called the single-argument Class.forName(), which uses the caller's (system) classloader rather than the entity's classloader. For property types only available in a child classloader (typical in app-server deployments), this caused emit() to crash with ClassNotFoundException and hasSetter() to silently return false, making the property read-only. Fix: use entity.getClassLoader() in hasSetter(), and emit Class.forName(name, false, tccl) in the generated constructor bytecode so property types are resolved at runtime via TCCL, consistent with how the entity class itself is already resolved. Regression test added in TestVarHandleAccessor that dynamically generates an entity whose property type lives only in a child classloader and asserts get/set round-trips.
…THODS mode findSetter() and findSetterInHierarchy() matched setter methods by name and descriptor only, without checking ACC_STATIC. In PropertyDiscovery.METHODS mode a static setXxx method was accepted as the property setter, causing the property to be treated as method-based. VarHandleAccessorGenerator's hasSetter() then correctly rejected the static method (it has a reflection-level isStatic guard), leaving the property with no setter handle — so set() threw UnsupportedOperationException even though the backing field was writable. Fix: add (flags & ACC_STATIC) == 0 guard to both findSetter and findSetterInHierarchy so static methods are never selected as property setters. Properties with getter + static setter (no instance setter) now fall back to field-based VarHandle discovery as expected.
consolidate packages
CritterGenerator.generate() called type.getClassLoader() directly without a null guard, causing NPE for bootstrap-loaded entity classes. Extracted GenerationUtils.safeClassLoader() with the correct null fallback and applied it at both sites (CritterGenerator and VarHandleAccessorGenerator). Consolidated three sets of duplicated code into GenerationUtils: PRIMITIVE_TO_WRAPPER (was in both accessor generators), typeClassName() (same), and emitBooleanMethod() (was in both model generators). All callers updated to use the shared versions.
…iscovery isGetter() accepted any method starting with "is" or "get" regardless of length. A no-arg non-void method named exactly "is" or "get" passed all checks, then getterPropertyName() computed an empty property name and threw StringIndexOutOfBoundsException on charAt(0), aborting property discovery for the entire entity. Added an early-exit guard that rejects exact matches before parsing the property name.
emitAnnotationOnStack used value.equals(defaultValue) to skip builder setter calls for annotation elements matching their defaults. Array-typed elements (String[], Class[], annotation[]) return a fresh defensive copy on every annotation proxy invocation, so two logically-identical arrays are never the same instance and equals() always returned false. Effect: setter calls were emitted for every array element regardless of whether the value matched the default, inflating generated bytecode. Fix: replaced equals() with Objects.deepEquals(), which compares array contents recursively.
…eration Generate a concrete annotation implementation class per annotation instance at code-generation time using the ClassFile API. This eliminates all runtime reflection from property/entity model constructors — Morphia and non-Morphia annotations alike are materialized as bytecode constants, fixing NonNull and other third-party annotations being silently dropped. Also fix AnnotationBuilders equals() using field access instead of method calls, and add Hotel test fixture with varargs/hashCode/equals to exercise the generation pipeline.
…tDeclaredAnnotation for non-Morphia Morphia annotations in generated <init> are emitted via AnnotationBuilder factory/setter chains, encoding all values as bytecode constants with zero runtime reflection. Non-Morphia annotations are embedded via RuntimeVisibleAnnotationsAttribute so getDeclaredAnnotation() works at runtime.
…ative entities Generates readable bytecode text files under target/critter-bytecode/ with full package/directory hierarchy for Example, MethodExample, Author, Book, and CritterMapperTestEntity; replaces the narrower DumpBytecodeTest.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 49 out of 50 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
core/src/main/java/dev/morphia/critter/parser/generator/CritterGenerator.java:51
generate()reads the classfile from anInputStreambut never closes it. Wrap the stream in try-with-resources to avoid leaking jar/file handles during generation.
Comment on lines
6
to
10
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| import com.mongodb.lang.NonNullApi; | ||
|
|
Comment on lines
+29
to
33
| @Override | ||
| public boolean equals(Object obj) { | ||
| return super.equals(obj); | ||
| } | ||
| } |
Comment on lines
+116
to
131
| private ClassModel readClassModel(Class<?> type) { | ||
| String resourceName = "%s.class".formatted(type.getName().replace('.', '/')); | ||
| InputStream inputStream = type.getClassLoader().getResourceAsStream(resourceName); | ||
| ClassLoader cl = type.getClassLoader() != null ? type.getClassLoader() | ||
| : ClassLoader.getSystemClassLoader(); | ||
| InputStream inputStream = cl.getResourceAsStream(resourceName); | ||
| if (inputStream == null) { | ||
| LOG.debug("Bytecode resource not found for {}; hierarchy traversal stops here", type.getName()); | ||
| return null; | ||
| } | ||
| ClassNode node = new ClassNode(); | ||
| try { | ||
| new ClassReader(inputStream).accept(node, 0); | ||
| byte[] bytes = inputStream.readAllBytes(); | ||
| return ClassFile.of().parse(bytes); | ||
| } catch (IOException e) { | ||
| LOG.warn("Failed to read bytecode for {}: {}", type.getName(), e.getMessage()); | ||
| return null; | ||
| } |
Comment on lines
+31
to
+45
| protected ClassModel readClassFiltering() { | ||
| String resourceName = "%s.class".formatted(entity.getName().replace('.', '/')); | ||
| InputStream inputStream = entity.getClassLoader().getResourceAsStream(resourceName); | ||
| if (inputStream == null) { | ||
| throw new IllegalArgumentException("Could not find class file for %s".formatted(entity.getName())); | ||
| } | ||
| try { | ||
| byte[] bytes = inputStream.readAllBytes(); | ||
| ClassModel model = ClassFile.of().parse(bytes); | ||
| return model; | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to read class %s".formatted(entity.getName()), e); | ||
| } | ||
| } | ||
| } |
Comment on lines
374
to
378
| @@ -375,6 +376,11 @@ | |||
| <artifactId>gizmo</artifactId> | |||
| <version>${gizmo.version}</version> | |||
| </dependency> | |||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #4273: replace
org.ow2.asmandio.quarkus.gizmowithio.github.dmlloyd:jdk-classfile-backport:25.1in Morphia's critter bytecode generation pipeline.jdk-classfile-backport; removed gizmo, asm-tree, asm-util from compile scope (asm kept as test-scope forClassfileOutput)FieldInfoandMethodInforeplace ASMFieldNode/MethodNodethroughoutAddFieldAccessorMethods,AddMethodAccessorMethods): rewritten usingClassFile.of().transformClass()with dropping + endHandler patternPropertyAccessorGenerator,VarHandleAccessorGenerator): rewritten usingClassFile.of().build()PropertyModelGenerator,GizmoEntityModelGenerator): rewritten using ClassFile API; use Java reflection for annotation instances and type data instead of ASMSignatureReader/AnnotationNodeGizmoExtensions: new utility class withemitAnnotationOnStack,emitClassRef,emitTypeData,asClass,rawTypeDesc,typeDataFromDescAnnotationNodeExtensions.kt: simplified to generate a minimal stub (no longer depends on ASM/Gizmo since annotation handling is now done via Java reflection)TypesTestusesClassDesc,TestGizmoGenerationremoves Gizmo-specific tests and uses ClassFile API for method discoveryTest plan
TestGizmoGeneration— 8 tests (type data parsing, annotation building, full entity model generation, method-based accessors)TypesTest— 44 tests (ClassDesc ↔ Class conversion)TestVarHandleAccessor— 6 tests (VarHandle-based runtime accessors)TestCritterMapper— 18 tests (full critter mapper integration)https://claude.ai/code/session_01TLEDKhoUzorXDYRAqVLLYE
Generated by Claude Code