MorphiaOrg / MorphiaOrg/morphia
Replace ASM + Gizmo with the Java Class File API (backport)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.7k
- Forks
- 449
- Avg merge
- 7m
- Merged PRs (30d)
- 10
Description
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 toCritterGenerator)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 andabstract hasLifecycle()fromCritterEntityModel, and changesctor()to useGizmoExtensions.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+aastoreloop +invokestatic Arrays.asList()emitGetTypeData()— recursiveTypeData(Class, TypeData[])constructor emissionsregisterAnnotations()— depends on Item 8emitClassRef()— non-public class fallback viaClass.forName(); identical logic, different instruction API
Note: PR #4270 (Consolidate
emitTypeData/emitClassRefintoGizmoExtensions) moves the private copies of these utilities toGizmoExtensions, making it the sole owner. Review before implementing.Note: PR #4271 (Replace hand-rolled signature parser with ASM
SignatureReader) replaces the manualtypeData()/balanced()string-parsing withSignatureReader+SignatureVisitor. If merged before this work starts, the target is to replaceSignatureReader/SignatureVisitor(ASM) with the Class File API'sSignaturetype 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'sSignatureAPI
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-linector()intoemitPrivateLookup(),emitVarHandleLookup(),emitGetterHandleLookup(), andemitSetterHandleLookup(). 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 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; unchanged- Drop the
AnnotationNodebranch inload()
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 |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the pending PRs listed in the issue, then inspect core/pom.xml, BaseGizmoGenerator, CritterGizmoGenerator, and the named generator classes to map the dependency and API migration. Run ./mvnw test -pl :morphia-core -Dmorphia.mapper=critter -Ddeploy.skip=true as a baseline. Done means ASM and Gizmo are removed from the critter generators and all critter tests pass with equivalent generated bytecode contracts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, kotlin
- Domain
- build-system, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100