Skip to content

Replace ASM + Gizmo with the Java Class File API (backport) #4273

Description

@evanchooly

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 / addCatchCodeBuilder.trying(tryHandler, catchesHandler)
  • BranchResult / ifNullCodeBuilder.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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions