Goal
Replace all direct ASM and Gizmo usage in the critter code generator with David Lloyd's Class File API backport (io.github.dmlloyd:classfile-backport). The backport runs on Java 17 and is API-identical to the standard java.lang.classfile.* package finalized in JDK 24 (JEP 484) — only the package prefix differs. When the project eventually raises its Java baseline to 24, the backport is dropped and import statements are updated.
Net dependency change in core/pom.xml:
- Remove:
org.ow2.asm:asm-tree, org.ow2.asm:asm-util, io.quarkus.gizmo:gizmo
- Add:
io.github.dmlloyd:classfile-backport
ASM will no longer appear as a direct dependency. (Confirm exact backport Maven coordinates before starting.)
Background
Critter currently uses ASM and Gizmo in two distinct roles:
Reading — parse entity .class files into ClassNode / FieldNode / MethodNode / AnnotationNode trees to discover fields, methods, and annotations.
Writing — generate new class bytecode for entity models, property models, and accessor classes. The ASM-level generators (AddFieldAccessorMethods, AddMethodAccessorMethods) inject synthetic methods directly into entity bytecode using raw opcodes. The Gizmo generators (GizmoEntityModelGenerator, PropertyModelGenerator, PropertyAccessorGenerator, VarHandleAccessorGenerator) build new classes via Gizmo's ClassCreator / MethodCreator abstraction.
The Class File API replaces both roles with a single, consistent API: ClassFile.of().parse(bytes) for reading and ClassFile.of().build(classDesc, classBuilder -> ...) / ClassFile.of().transformClass(...) for writing.
Work items
1. Add backport dependency, update pom files
Add io.github.dmlloyd:classfile-backport to the parent BOM and core/pom.xml. Remove the three outgoing dependencies listed above.
2. New base generator: BaseClassFileGenerator (replaces BaseGizmoGenerator)
BaseGizmoGenerator wraps ClassCreator.Builder and routes bytes to critterClassLoader.register(). Replace with a base that calls ClassFile.of().build(classDesc, classBuilder -> ...) and registers the resulting byte[] directly.
3. Rewrite class reading
Replace new ClassReader(stream).accept(classNode, 0) with ClassFile.of().parse(bytes) returning a ClassModel. Affects:
CritterGizmoGenerator.generate() (rename class to CritterGenerator)
PropertyFinder.readClassNode()
GizmoEntityModelGenerator.registerAnnotations() parent-hierarchy walk
The central data carriers (ClassNode / FieldNode / MethodNode / AnnotationNode) are replaced by ClassModel / FieldModel / MethodModel / java.lang.classfile.attribute.Annotation throughout. Every constructor and method signature that currently takes a FieldNode or MethodNode is updated accordingly.
| ASM |
Class File API |
ClassNode |
ClassModel |
FieldNode |
FieldModel |
MethodNode |
MethodModel |
AnnotationNode |
classfile.attribute.Annotation |
Type.getType(c) |
ClassDesc.of(c.getName()) |
type.getDescriptor() |
classDesc.descriptorString() |
type.getInternalName() |
classDesc.internalName() |
Type.getReturnType(desc) |
MethodTypeDesc.ofDescriptor(desc).returnType() |
Type.getArgumentTypes(desc) |
MethodTypeDesc.ofDescriptor(desc).parameterList() |
type.getSort() == ARRAY |
classDesc.isArray() |
4. Rewrite class writing: AddFieldAccessorMethods + AddMethodAccessorMethods
Replace ClassWriter + MethodVisitor + raw opcodes with ClassFile.of().transformClass(originalBytes, ClassTransform). The idempotency filter that strips pre-existing __read/__write methods moves into a MethodTransform that drops matching method elements. Instruction emission via CodeBuilder (named methods; no opcode constants).
5. Rewrite GizmoEntityModelGenerator
Generated methods are all constant-return or short invocation sequences; they map directly to CodeBuilder. loadProperties() and registerAnnotations() become repeated new + constructor-call + invokevirtual patterns. Depends on Item 8 for annotation bytecode emission.
Note: PR #4268 (Fix CritterMapper lifecycle callbacks) removes the hard-coded hasLifecycle() override and abstract hasLifecycle() from CritterEntityModel, and changes ctor() to use GizmoExtensions.emitClassRef() for non-public entity classes. Review those changes before implementing this item.
6. Rewrite PropertyModelGenerator
Largest single piece (~530 lines). Simple boolean/String-return methods are trivial. Key patterns:
getLoadNames() — anewarray + aastore loop + invokestatic Arrays.asList()
emitGetTypeData() — recursive TypeData(Class, TypeData[]) constructor emissions
registerAnnotations() — depends on Item 8
emitClassRef() — non-public class fallback via Class.forName(); identical logic, different instruction API
Note: PR #4270 (Consolidate emitTypeData/emitClassRef into GizmoExtensions) moves the private copies of these utilities to GizmoExtensions, making it the sole owner. Review before implementing.
Note: PR #4271 (Replace hand-rolled signature parser with ASM SignatureReader) replaces the manual typeData() / balanced() string-parsing with SignatureReader + SignatureVisitor. If merged before this work starts, the target is to replace SignatureReader/SignatureVisitor (ASM) with the Class File API's Signature type hierarchy (ClassTypeSig, TypeVarSig, etc.), which provides the same structured parse tree without ASM.
7. Rewrite PropertyAccessorGenerator
Short (~160 lines). get() and set() become CodeBuilder sequences: checkcast, invokevirtual __readXxx, typed return. Gizmo's smartCast for primitives becomes explicit box/unbox instructions — a small per-primitive helper covers all eight cases.
8. Rewrite VarHandleAccessorGenerator
Most structurally complex due to exception handling and conditional branching.
TryBlock / addCatch → CodeBuilder.trying(tryHandler, catchesHandler)
BranchResult / ifNull → CodeBuilder.ifThen() / ifThenElse()
SignatureBuilder.forClass().addInterface(parameterizedType(...)) → ClassSignature.of(...) from the backport's Signature API
The VarHandle/MethodHandle runtime logic (MethodHandles.privateLookupIn, findVarHandle, findVirtual) is unchanged — those are runtime calls being emitted, not compile-time API choices.
Note: PR #4267 (Refactor VarHandleAccessorGenerator.ctor() into private helpers) splits the 76-line ctor() into emitPrivateLookup(), emitVarHandleLookup(), emitGetterHandleLookup(), and emitSetterHandleLookup(). Review that structure before implementing.
9. Rewrite GizmoExtensions
With both ASM and Gizmo gone this class becomes a pure Class File API utility:
annotationBuilder(classfile.Annotation, CodeBuilder) — emits bytecode that calls the annotation's builder
load(CodeBuilder, Type, Object) — emits constant loads for various value types
emitTypeData(CodeBuilder, TypeData<?>) — emits TypeData constructor calls
asClass(ClassDesc, ClassLoader) — primitive type detection via ClassDesc.isPrimitive() / .isArray()
rawType() / attributeType() — pure reflection utilities; no API dependency; unchanged
- Drop the
AnnotationNode branch in load()
10. Update build-plugins/AnnotationNodeExtensions.kt (the code generator)
This Kotlin Maven Mojo generates AnnotationNodeExtensions.java at build time. The generated file currently hard-codes both ASM and Gizmo types in every per-annotation method:
// currently generated:
void setBuilderValues(AnnotationNode, MethodCreator, ResultHandle)
<T> T toMorphiaAnnotation(AnnotationNode)
These need to change to:
void setBuilderValues(classfile.attribute.Annotation, CodeBuilder)
<T> T toMorphiaAnnotation(classfile.attribute.Annotation)
The processTypeJava() and processArrayTypeJava() Kotlin helpers must be updated for the different annotation value access patterns of the Class File API:
| Value kind |
ASM (AnnotationNode.values flat list) |
Class File API (AnnotationElement) |
| Enum |
((String[]) val)[1] |
((AnnotationValue.OfEnum) val).constantName() |
| Class |
((org.objectweb.asm.Type) val).getClassName() |
((AnnotationValue.OfClass) val).classSymbol().displayName() |
| Nested annotation |
(AnnotationNode) val |
((AnnotationValue.OfAnnotation) val).annotation() |
| String/int/boolean |
same cast |
same cast |
| String array |
(List<String>) val |
((AnnotationValue.OfArray) val).values() then cast each element |
setBuilderValues currently generates Gizmo invokeVirtualMethod(MethodDescriptor, ...) calls; it needs to generate CodeBuilder.invokevirtual(ClassDesc, String, MethodTypeDesc) calls instead.
Suggested sequencing
Item 1 (deps)
→ Item 2 (base generator)
→ Item 3 (class reading + data model swap)
→ Item 10 (code generator for AnnotationNodeExtensions)
→ Items 4, 5, 6, 7, 8, 9 (can be done in any order)
Items 4–9 can be parallelised once Item 10 is done, since they depend on the generated AnnotationNodeExtensions signature but are otherwise independent.
Acceptance criterion
All critter tests pass with -Dmorphia.mapper=critter:
./mvnw test -pl :morphia-core -Dmorphia.mapper=critter -Ddeploy.skip=true
The generated bytecode contracts are identical to today's — only the generators change.
Pending PRs that affect this scope
The following open PRs touch files in scope. Review their changes before implementing the corresponding items above:
| PR |
Affects |
#4267 — Refactor VarHandleAccessorGenerator.ctor() into helpers |
Item 8 |
| #4268 — Fix CritterMapper lifecycle callbacks |
Item 5 |
#4269 — Fix PropertyFinder inherited getter discovery |
Item 3 |
#4270 — Consolidate emitTypeData/emitClassRef into GizmoExtensions |
Items 6, 9 |
#4271 — Replace hand-rolled signature parser with ASM SignatureReader |
Item 6 |
| #4272 — Add explicit tests for critter coverage gaps |
Testing baseline |
Goal
Replace all direct ASM and Gizmo usage in the critter code generator with David Lloyd's Class File API backport (
io.github.dmlloyd:classfile-backport). The backport runs on Java 17 and is API-identical to the standardjava.lang.classfile.*package finalized in JDK 24 (JEP 484) — only the package prefix differs. When the project eventually raises its Java baseline to 24, the backport is dropped and import statements are updated.Net dependency change in
core/pom.xml:org.ow2.asm:asm-tree,org.ow2.asm:asm-util,io.quarkus.gizmo:gizmoio.github.dmlloyd:classfile-backportASM will no longer appear as a direct dependency. (Confirm exact backport Maven coordinates before starting.)
Background
Critter currently uses ASM and Gizmo in two distinct roles:
Reading — parse entity
.classfiles intoClassNode/FieldNode/MethodNode/AnnotationNodetrees to discover fields, methods, and annotations.Writing — generate new class bytecode for entity models, property models, and accessor classes. The ASM-level generators (
AddFieldAccessorMethods,AddMethodAccessorMethods) inject synthetic methods directly into entity bytecode using raw opcodes. The Gizmo generators (GizmoEntityModelGenerator,PropertyModelGenerator,PropertyAccessorGenerator,VarHandleAccessorGenerator) build new classes via Gizmo'sClassCreator/MethodCreatorabstraction.The Class File API replaces both roles with a single, consistent API:
ClassFile.of().parse(bytes)for reading andClassFile.of().build(classDesc, classBuilder -> ...)/ClassFile.of().transformClass(...)for writing.Work items
1. Add backport dependency, update pom files
Add
io.github.dmlloyd:classfile-backportto the parent BOM andcore/pom.xml. Remove the three outgoing dependencies listed above.2. New base generator:
BaseClassFileGenerator(replacesBaseGizmoGenerator)BaseGizmoGeneratorwrapsClassCreator.Builderand routes bytes tocritterClassLoader.register(). Replace with a base that callsClassFile.of().build(classDesc, classBuilder -> ...)and registers the resultingbyte[]directly.3. Rewrite class reading
Replace
new ClassReader(stream).accept(classNode, 0)withClassFile.of().parse(bytes)returning aClassModel. Affects:CritterGizmoGenerator.generate()(rename class toCritterGenerator)PropertyFinder.readClassNode()GizmoEntityModelGenerator.registerAnnotations()parent-hierarchy walkThe central data carriers (
ClassNode/FieldNode/MethodNode/AnnotationNode) are replaced byClassModel/FieldModel/MethodModel/java.lang.classfile.attribute.Annotationthroughout. Every constructor and method signature that currently takes aFieldNodeorMethodNodeis updated accordingly.ClassNodeClassModelFieldNodeFieldModelMethodNodeMethodModelAnnotationNodeclassfile.attribute.AnnotationType.getType(c)ClassDesc.of(c.getName())type.getDescriptor()classDesc.descriptorString()type.getInternalName()classDesc.internalName()Type.getReturnType(desc)MethodTypeDesc.ofDescriptor(desc).returnType()Type.getArgumentTypes(desc)MethodTypeDesc.ofDescriptor(desc).parameterList()type.getSort() == ARRAYclassDesc.isArray()4. Rewrite class writing:
AddFieldAccessorMethods+AddMethodAccessorMethodsReplace
ClassWriter+MethodVisitor+ raw opcodes withClassFile.of().transformClass(originalBytes, ClassTransform). The idempotency filter that strips pre-existing__read/__writemethods moves into aMethodTransformthat drops matching method elements. Instruction emission viaCodeBuilder(named methods; no opcode constants).5. Rewrite
GizmoEntityModelGeneratorGenerated methods are all constant-return or short invocation sequences; they map directly to
CodeBuilder.loadProperties()andregisterAnnotations()become repeatednew+ constructor-call +invokevirtualpatterns. Depends on Item 8 for annotation bytecode emission.6. Rewrite
PropertyModelGeneratorLargest single piece (~530 lines). Simple boolean/String-return methods are trivial. Key patterns:
getLoadNames()—anewarray+aastoreloop +invokestatic Arrays.asList()emitGetTypeData()— recursiveTypeData(Class, TypeData[])constructor emissionsregisterAnnotations()— depends on Item 8emitClassRef()— non-public class fallback viaClass.forName(); identical logic, different instruction API7. Rewrite
PropertyAccessorGeneratorShort (~160 lines).
get()andset()becomeCodeBuildersequences:checkcast,invokevirtual __readXxx, typed return. Gizmo'ssmartCastfor primitives becomes explicit box/unbox instructions — a small per-primitive helper covers all eight cases.8. Rewrite
VarHandleAccessorGeneratorMost structurally complex due to exception handling and conditional branching.
TryBlock/addCatch→CodeBuilder.trying(tryHandler, catchesHandler)BranchResult/ifNull→CodeBuilder.ifThen()/ifThenElse()SignatureBuilder.forClass().addInterface(parameterizedType(...))→ClassSignature.of(...)from the backport'sSignatureAPIThe VarHandle/MethodHandle runtime logic (
MethodHandles.privateLookupIn,findVarHandle,findVirtual) is unchanged — those are runtime calls being emitted, not compile-time API choices.9. Rewrite
GizmoExtensionsWith both ASM and Gizmo gone this class becomes a pure Class File API utility:
annotationBuilder(classfile.Annotation, CodeBuilder)— emits bytecode that calls the annotation's builderload(CodeBuilder, Type, Object)— emits constant loads for various value typesemitTypeData(CodeBuilder, TypeData<?>)— emitsTypeDataconstructor callsasClass(ClassDesc, ClassLoader)— primitive type detection viaClassDesc.isPrimitive()/.isArray()rawType()/attributeType()— pure reflection utilities; no API dependency; unchangedAnnotationNodebranch inload()10. Update
build-plugins/AnnotationNodeExtensions.kt(the code generator)This Kotlin Maven Mojo generates
AnnotationNodeExtensions.javaat build time. The generated file currently hard-codes both ASM and Gizmo types in every per-annotation method:These need to change to:
The
processTypeJava()andprocessArrayTypeJava()Kotlin helpers must be updated for the different annotation value access patterns of the Class File API:AnnotationNode.valuesflat list)AnnotationElement)((String[]) val)[1]((AnnotationValue.OfEnum) val).constantName()((org.objectweb.asm.Type) val).getClassName()((AnnotationValue.OfClass) val).classSymbol().displayName()(AnnotationNode) val((AnnotationValue.OfAnnotation) val).annotation()(List<String>) val((AnnotationValue.OfArray) val).values()then cast each elementsetBuilderValuescurrently generates GizmoinvokeVirtualMethod(MethodDescriptor, ...)calls; it needs to generateCodeBuilder.invokevirtual(ClassDesc, String, MethodTypeDesc)calls instead.Suggested sequencing
Items 4–9 can be parallelised once Item 10 is done, since they depend on the generated
AnnotationNodeExtensionssignature but are otherwise independent.Acceptance criterion
All critter tests pass with
-Dmorphia.mapper=critter:The generated bytecode contracts are identical to today's — only the generators change.
Pending PRs that affect this scope
The following open PRs touch files in scope. Review their changes before implementing the corresponding items above:
VarHandleAccessorGenerator.ctor()into helpersPropertyFinderinherited getter discoveryemitTypeData/emitClassRefintoGizmoExtensionsSignatureReader