feat: construct records through generated instantiators instead of reflection#255
Merged
Conversation
…flection The metamodel generators now emit an Instantiator implementation per record type that invokes the canonical constructor (the primary constructor for Kotlin data classes) directly, registered through META-INF/services/st.orm.mapping.Instantiator. The row mapper resolves the instantiator once per constructor and dispatches to it instead of Constructor.newInstance, falling back to reflective construction for types without one, so models compiled without the generators keep working unchanged. This removes the last reflective call from the row mapping hot path. The motivation is portability rather than performance: native images need no reflection configuration for entity records, and modular applications need no opens clauses for their model packages. The public st.orm.mapping.Instantiator contract lives in storm-foundation so that generated code compiles in model-only modules, mirroring the metamodel types. The ServiceLoader registry caches per class loader; the record mapper verifies the constructor against the record components before attaching an instantiator, so non-canonical constructors always construct reflectively. Only top-level, non-generic records and data classes get instantiators; the annotation processor registers the services file when processing is over, the KSP processor on finish. Closes #253
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.
Implements #253: the metamodel generators emit an
Instantiatorper record type that invokes the canonical constructor directly, and the row mapper dispatches to it instead ofConstructor.newInstance. This removes the last reflective call from the row mapping hot path. The motivation is portability, not performance: native images need no reflection configuration for entity records, and modular applications need noopensclauses for their model packages.Contract
st.orm.mapping.Instantiator<T>(storm-foundation):type()plusinstantiate(Object[] args). It lives in foundation for the same reason the metamodel types do — generated code must compile in model-only modules whose only Storm dependency is storm-foundation. It sits inst.orm.mappingrather than the root package because users never reference it in application code: the root package is user vocabulary,.mappingis machinery.Runtime
Instantiators(core spi) discovers implementations through theServiceLoader— automatically converted to build-time registrations by native-image — with the same dual class loader strategy asProviders, cached per class loader.ObjectMapperFactoryresolves the instantiator once per constructor into the cachedConstructorMetaand dispatches to it inconstruct(). The precomputed null diagnostics run in front of both paths, so error messages are unchanged.module-infodeclaresuses st.orm.mapping.Instantiator. Modular applications register their generated instantiators with aprovidesclause instead of opening the model package.Generators
<Name>Instantiatoralongside each metamodel for top-level, non-generic Java records and writesMETA-INF/services/st.orm.mapping.Instantiatorwhen processing is over.Ref<PetType>and nullable parameters), registering the services file infinish().Testing
InstantiatorIntegrationTestpins registration, equivalent construction, and the null fallback explicitly.Notes
Claiming native-image compatibility in the documentation should wait for an actual GraalVM build against a sample application; this PR delivers the mechanism. Fused typed decoding (generated code reading typed columns straight into constructor parameters) remains a non-goal, per the issue.
Closes #253