From d19a533216e1108f09418e9663058044248401f2 Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Sun, 8 Mar 2026 23:53:38 +0100 Subject: [PATCH] Optimize memory footprint. (#76) --- .../orm/core/repository/EntityRepository.java | 15 ++++--- .../repository/impl/EntityRepositoryImpl.java | 14 ++++--- .../java/st/orm/core/spi/RefFactoryImpl.java | 6 +-- .../main/java/st/orm/core/spi/RefImpl.java | 21 ++++------ .../orm/core/template/impl/LazySupplier.java | 36 ++++++++++------- .../core/template/impl/MetamodelFactory.java | 7 ++-- .../core/template/impl/LazySupplierTest.java | 33 +++++---------- .../src/main/java/st/orm/DetachedRef.java | 15 ++----- .../src/main/java/st/orm/Ref.java | 15 +++---- .../st/orm/repository/EntityRepository.java | 15 ++++--- .../st/orm/repository/EntityRepository.kt | 15 ++++--- .../st/orm/metamodel/MetamodelProcessor.kt | 19 ++++++++- .../st/orm/metamodel/MetamodelProcessor.java | 40 +++++++++++++++---- 13 files changed, 136 insertions(+), 115 deletions(-) diff --git a/storm-core/src/main/java/st/orm/core/repository/EntityRepository.java b/storm-core/src/main/java/st/orm/core/repository/EntityRepository.java index 196201082..4d453fd3a 100644 --- a/storm-core/src/main/java/st/orm/core/repository/EntityRepository.java +++ b/storm-core/src/main/java/st/orm/core/repository/EntityRepository.java @@ -221,14 +221,17 @@ public interface EntityRepository, ID> extends Repository { /** * Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key. * - *

This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual - * record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will - * trigger a new database call. This approach is particularly useful when you need to minimize memory usage while - * keeping the option to re-fetch the complete record later.

+ *

This method discards the full entity data and returns an attached ref that encapsulates just the primary key. + * The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, + * which will trigger a new database call. This approach is particularly useful when you need to minimize memory + * usage while keeping the option to re-fetch the complete record later.

+ * + *

Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can + * re-fetch the entity from the database.

* * @param entity the entity to unload into a lightweight ref. - * @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when - * needed. + * @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched + * again when needed. * @since 1.3 */ Ref unload(@Nonnull E entity); diff --git a/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java b/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java index f2c1df04d..61711f6b5 100644 --- a/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java +++ b/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java @@ -506,14 +506,16 @@ public Ref ref(@Nonnull E entity) { /** * Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key. * - *

This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual - * record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will - * trigger a new database call. This approach is particularly useful when you need to minimize memory usage while - * keeping the option to re-fetch the complete record later.

+ *

This method discards the full entity data and returns an attached ref that encapsulates just the primary key. + * The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, + * which will trigger a new database call.

+ * + *

Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can + * re-fetch the entity from the database.

* * @param entity the entity to unload into a lightweight ref. - * @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when - * needed. + * @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched + * again when needed. * @since 1.3 */ @Override diff --git a/storm-core/src/main/java/st/orm/core/spi/RefFactoryImpl.java b/storm-core/src/main/java/st/orm/core/spi/RefFactoryImpl.java index d56159545..47ae4cd0d 100644 --- a/storm-core/src/main/java/st/orm/core/spi/RefFactoryImpl.java +++ b/storm-core/src/main/java/st/orm/core/spi/RefFactoryImpl.java @@ -100,11 +100,7 @@ public Ref create(@Nonnull Class type, @Nonnull ID pk @Override public Ref create(@Nonnull T record, @Nonnull ID pk) { var type = (Class) record.getClass(); - var supplier = new LazySupplier<>(() -> - ((QueryBuilder) template - .selectFrom(type)) - .where(pk) - .getSingleResult(), record); + var supplier = new LazySupplier<>(record); return create(supplier, type, pk); } diff --git a/storm-core/src/main/java/st/orm/core/spi/RefImpl.java b/storm-core/src/main/java/st/orm/core/spi/RefImpl.java index ba9b899ae..b9a9c383d 100644 --- a/storm-core/src/main/java/st/orm/core/spi/RefImpl.java +++ b/storm-core/src/main/java/st/orm/core/spi/RefImpl.java @@ -20,7 +20,9 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import st.orm.Data; +import st.orm.Entity; import st.orm.Ref; +import st.orm.core.repository.EntityRepository; import st.orm.core.template.impl.LazySupplier; /** @@ -107,23 +109,14 @@ public boolean isFetchable() { } /** - * Returns a ref with the same identity but without data. + * Returns a detached ref with the same identity but without data. The returned ref is not attached to a database + * context. To obtain an attached ref that can re-fetch the record, use + * {@link EntityRepository#unload(Entity) EntityRepository.unload()} instead. * - *

For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database. - * The same ref instance may be returned since the data can be recovered on demand.

- * - *

For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()} - * on the returned ref will fail since there is no database connection to recover the data. Use this with caution - * when working with detached refs, as the original data cannot be retrieved.

- * - *

For detached refs that are already unloaded, this method returns the same instance.

- * - * @return a ref with the same type and primary key but without cached data; may return {@code this} if no new - * instance is required. + * @return a detached ref with the same type and primary key but without cached data. */ @Override public Ref unload() { - supplier.remove(); - return this; + return Ref.of(type, pk); } } diff --git a/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java b/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java index b576314d9..ceae6d47a 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java @@ -24,7 +24,8 @@ import java.util.function.Supplier; /** - * Returns a lazily initialized value. + * Returns a lazily initialized value. Once the value has been resolved, the supplier is released for garbage + * collection. * * @param the type of results supplied by this supplier. */ @@ -41,27 +42,41 @@ public static Supplier lazy(@Nonnull Supplier supplier) { return new LazySupplier<>(supplier); } - private final Supplier supplier; + private volatile Supplier supplier; private final AtomicReference reference; + /** + * Creates a lazy supplier that will invoke the given supplier on the first call to {@link #get()}. + * + * @param supplier the supplier that provides the value. + */ public LazySupplier(@Nonnull Supplier supplier) { this.supplier = requireNonNull(supplier); this.reference = new AtomicReference<>(); } - public LazySupplier(@Nonnull Supplier supplier, @Nonnull T initialValue) { - this.supplier = requireNonNull(supplier); + /** + * Creates a lazy supplier with a pre-loaded initial value. The supplier is not retained and {@link #get()} will + * return the initial value without invoking any supplier. + * + * @param initialValue the pre-loaded value. + */ + public LazySupplier(@Nonnull T initialValue) { + this.supplier = null; this.reference = new AtomicReference<>(requireNonNull(initialValue)); } /** - * Gets a result. + * Gets a result. On the first invocation, the supplier is called to produce the value. The supplier is then + * released for garbage collection. * - * @return a result + * @return a result. */ @Override public T get() { - return reference.updateAndGet(value -> requireNonNullElseGet(value, supplier)); + T result = reference.updateAndGet(value -> requireNonNullElseGet(value, supplier)); + supplier = null; // Release the supplier (and its captured context) for GC. + return result; } /** @@ -73,13 +88,6 @@ public Optional value() { return ofNullable(reference.get()); } - /** - * Removes the lazily loaded value. This will not remove the supplier. - */ - public void remove() { - reference.set(null); - } - /** * Returns the first argument if it is non-{@code null} and otherwise returns the non-{@code null} value of * {@code supplier.get()}. diff --git a/storm-core/src/main/java/st/orm/core/template/impl/MetamodelFactory.java b/storm-core/src/main/java/st/orm/core/template/impl/MetamodelFactory.java index d6021bc75..9bfbd66b6 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/MetamodelFactory.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/MetamodelFactory.java @@ -274,9 +274,10 @@ private static Metamodel getModel(@Nonnull Class ro @Nonnull String path ) { try { - Metamodel current = (Metamodel) Class.forName(rootTable.getName() + "Metamodel", true, rootTable.getClassLoader()) - .getConstructor() - .newInstance(); + Metamodel current = (Metamodel) Class.forName( + rootTable.getName() + "Metamodel", true, rootTable.getClassLoader()) + .getMethod("instance") + .invoke(null); for (String segment : path.split("\\.")) { current = (Metamodel) readSegment(current, segment); } diff --git a/storm-core/src/test/java/st/orm/core/template/impl/LazySupplierTest.java b/storm-core/src/test/java/st/orm/core/template/impl/LazySupplierTest.java index cac9098e1..3adc7dc72 100644 --- a/storm-core/src/test/java/st/orm/core/template/impl/LazySupplierTest.java +++ b/storm-core/src/test/java/st/orm/core/template/impl/LazySupplierTest.java @@ -43,14 +43,11 @@ public void testLazyStaticFactoryMethod() { @Test public void testConstructorWithInitialValue() { - AtomicInteger callCount = new AtomicInteger(0); - LazySupplier lazy = new LazySupplier<>(() -> { - callCount.incrementAndGet(); - return "fallback"; - }, "initial"); + LazySupplier lazy = new LazySupplier<>("initial"); assertEquals("initial", lazy.get()); - assertEquals(0, callCount.get(), "Supplier should not be called when initial value is provided"); + assertTrue(lazy.value().isPresent()); + assertEquals("initial", lazy.value().get()); } @Test @@ -69,35 +66,25 @@ public void testValueReturnsPresentAfterGet() { @Test public void testValueReturnsPresentWithInitialValue() { - LazySupplier lazy = new LazySupplier<>(() -> "fallback", "initial"); + LazySupplier lazy = new LazySupplier<>("initial"); assertTrue(lazy.value().isPresent(), "Value should be present when initial value is provided"); assertEquals("initial", lazy.value().get()); } @Test - public void testRemoveClearsLazyValue() { - LazySupplier lazy = new LazySupplier<>(() -> "hello"); - lazy.get(); - assertTrue(lazy.value().isPresent()); - - lazy.remove(); - assertTrue(lazy.value().isEmpty(), "Value should be empty after remove()"); - } - - @Test - public void testGetAfterRemoveReinvokesSupplier() { + public void testSupplierReleasedAfterGet() { AtomicInteger callCount = new AtomicInteger(0); LazySupplier lazy = new LazySupplier<>(() -> { callCount.incrementAndGet(); return "hello"; }); - lazy.get(); - assertEquals(1, callCount.get()); + assertEquals("hello", lazy.get()); + assertEquals(1, callCount.get(), "Supplier should be called exactly once"); - lazy.remove(); - lazy.get(); - assertEquals(2, callCount.get(), "After remove, get should invoke supplier again"); + // After get(), the supplier should be released. Calling get() again returns the cached value. + assertEquals("hello", lazy.get()); + assertEquals(1, callCount.get(), "Supplier should not be called again after first get()"); } @Test diff --git a/storm-foundation/src/main/java/st/orm/DetachedRef.java b/storm-foundation/src/main/java/st/orm/DetachedRef.java index d2e34138a..845e548ef 100644 --- a/storm-foundation/src/main/java/st/orm/DetachedRef.java +++ b/storm-foundation/src/main/java/st/orm/DetachedRef.java @@ -101,19 +101,10 @@ public boolean isFetchable() { } /** - * Returns a ref with the same identity but without data. + * Returns a detached ref with the same identity but without data. Since this ref is already detached and unloaded, + * the same instance is returned. * - *

For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database. - * The same ref instance may be returned since the data can be recovered on demand.

- * - *

For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()} - * on the returned ref will fail since there is no database connection to recover the data. Use this with caution - * when working with detached refs, as the original data cannot be retrieved.

- * - *

For detached refs that are already unloaded, this method returns the same instance.

- * - * @return a ref with the same type and primary key but without cached data; may return {@code this} if no new - * instance is required. + * @return this instance. */ @Override public Ref unload() { diff --git a/storm-foundation/src/main/java/st/orm/Ref.java b/storm-foundation/src/main/java/st/orm/Ref.java index a0c1451be..103fb2ade 100644 --- a/storm-foundation/src/main/java/st/orm/Ref.java +++ b/storm-foundation/src/main/java/st/orm/Ref.java @@ -269,19 +269,14 @@ default boolean isLoaded() { } /** - * Returns a ref with the same identity but without data. + * Returns a detached ref with the same identity but without data. * - *

For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database. - * The same ref instance may be returned since the data can be recovered on demand.

+ *

The returned ref is not attached to a database context. Calling {@link #fetch()} on the returned ref will + * fail since there is no database connection to recover the data.

* - *

For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()} - * on the returned ref will fail since there is no database connection to recover the data. Use this with caution - * when working with detached refs, as the original data cannot be retrieved.

+ *

For refs that are already detached and unloaded, this method may return the same instance.

* - *

For detached refs that are already unloaded, this method returns the same instance.

- * - * @return a ref with the same type and primary key but without cached data; may return {@code this} if no new - * instance is required. + * @return a detached ref with the same type and primary key but without cached data. */ Ref unload(); } diff --git a/storm-java21/src/main/java/st/orm/repository/EntityRepository.java b/storm-java21/src/main/java/st/orm/repository/EntityRepository.java index a1d60fd07..7759485aa 100644 --- a/storm-java21/src/main/java/st/orm/repository/EntityRepository.java +++ b/storm-java21/src/main/java/st/orm/repository/EntityRepository.java @@ -204,14 +204,17 @@ public interface EntityRepository, ID> extends Repository { /** * Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key. * - *

This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual - * record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will - * trigger a new database call. This approach is particularly useful when you need to minimize memory usage while - * keeping the option to re-fetch the complete record later.

+ *

This method discards the full entity data and returns an attached ref that encapsulates just the primary key. + * The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, + * which will trigger a new database call. This approach is particularly useful when you need to minimize memory + * usage while keeping the option to re-fetch the complete record later.

+ * + *

Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can + * re-fetch the entity from the database.

* * @param entity the entity to unload into a lightweight ref. - * @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when - * needed. + * @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched + * again when needed. * @since 1.3 */ Ref unload(@Nonnull E entity); diff --git a/storm-kotlin/src/main/kotlin/st/orm/repository/EntityRepository.kt b/storm-kotlin/src/main/kotlin/st/orm/repository/EntityRepository.kt index 3cf60556b..ee3ae0e4c 100644 --- a/storm-kotlin/src/main/kotlin/st/orm/repository/EntityRepository.kt +++ b/storm-kotlin/src/main/kotlin/st/orm/repository/EntityRepository.kt @@ -202,14 +202,17 @@ interface EntityRepository : Repository where E : Entity { * Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key. * * - * This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual - * record is not retained in memory, but can be retrieved on demand by calling [Ref.fetch], which will - * trigger a new database call. This approach is particularly useful when you need to minimize memory usage while - * keeping the option to re-fetch the complete record later. + * This method discards the full entity data and returns an attached ref that encapsulates just the primary key. + * The actual record is not retained in memory, but can be retrieved on demand by calling [Ref.fetch], + * which will trigger a new database call. This approach is particularly useful when you need to minimize memory + * usage while keeping the option to re-fetch the complete record later. + * + * Unlike [Ref.unload], which returns a detached ref, this method returns an attached ref that can + * re-fetch the entity from the database. * * @param entity the entity to unload into a lightweight ref. - * @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when - * needed. + * @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched + * again when needed. * @since 1.3 */ fun unload(entity: E): Ref diff --git a/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt b/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt index 29c0f7d2d..9e4d6136e 100644 --- a/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt +++ b/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt @@ -646,6 +646,7 @@ class MetamodelProcessor( ): String { val builder = StringBuilder() val className = classDeclaration.simpleName.asString() + val modelRef = "${className}Metamodel.instance<$className>()" getModelProperties(classDeclaration).forEach { prop -> val fieldName = prop.simpleName.asString() val typeRef = prop.type @@ -662,7 +663,7 @@ class MetamodelProcessor( builder.append(" /** Represents the $className.$fieldName record. */\n") builder.append( " val $fieldName: $childMetaType = " + - "${className}Metamodel<$className>().$fieldName\n", + "$modelRef.$fieldName\n", ) } else { val kotlinTypeName = getKotlinTypeName(typeRef, packageName) // E (unwrap Ref) @@ -672,7 +673,7 @@ class MetamodelProcessor( builder.append(" /** Represents the $className.$fieldName field. */\n") builder.append( " val $fieldName: $baseClass<$className, $kotlinTypeName, $valueKotlinTypeName> = " + - "${className}Metamodel<$className>().$fieldName\n", + "$modelRef.$fieldName\n", ) } } @@ -1093,6 +1094,20 @@ class MetamodelProcessor( |${initClassFields(classDeclaration, packageName, metaClassName, forceNullableChain)} | } |$convenienceCtors + |${ + if (isDataRoot && !forceNullableChain) { + """ + | companion object { + | private val INSTANCE: $metaClassName<*> = $metaClassName() + | + | @Suppress("UNCHECKED_CAST") + | @JvmStatic + | fun instance(): $metaClassName = INSTANCE as $metaClassName + | }""" + } else { + "" + } + } |} """.trimMargin(), ) diff --git a/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java b/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java index 815023b7e..0373b3440 100644 --- a/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java +++ b/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java @@ -744,6 +744,7 @@ private static String identicalComparisonExpr(@Nonnull String left, @Nonnull Str private String buildInterfaceFields(@Nonnull Element recordElement, @Nonnull String packageName) { StringBuilder builder = new StringBuilder(); String recordName = recordElement.getSimpleName().toString(); + String modelRef = recordName + "Metamodel.<" + recordName + ">instance()"; for (Element enclosed : recordElement.getEnclosedElements()) { TypeMirror recordComponentType = getRecordComponentType(enclosed).orElse(null); if (recordComponentType == null) continue; @@ -768,8 +769,8 @@ private String buildInterfaceFields(@Nonnull Element recordElement, @Nonnull Str .append(inline ? "record." : "foreign key.") .append(" */\n"); builder.append(" ").append(fieldTypeName).append("Metamodel<").append(recordName).append("> ") - .append(fieldName).append(" = new ").append(recordName).append("Metamodel<") - .append(recordName).append(">().").append(fieldName).append(";\n"); + .append(fieldName).append(" = ").append(modelRef).append(".") + .append(fieldName).append(";\n"); } else { String valueTypeName = getValueTypeName(fieldType, packageName); boolean unique = isUniqueField(recordElement, fieldName); @@ -779,8 +780,8 @@ private String buildInterfaceFields(@Nonnull Element recordElement, @Nonnull Str .append("} field. */\n"); builder.append(" ").append(baseClass).append("<").append(recordName).append(", ").append(fieldTypeName) .append(", ").append(valueTypeName).append("> ") - .append(fieldName).append(" = new ").append(recordName).append("Metamodel<") - .append(recordName).append(">().").append(fieldName).append(";\n"); + .append(fieldName).append(" = ").append(modelRef).append(".") + .append(fieldName).append(";\n"); } } if (!builder.isEmpty()) { @@ -1187,12 +1188,23 @@ private void generateMetamodelClass(@Nonnull Element recordElement) { " this(path, field, inline, parent, getter, false);\n" + " }\n"; } + String staticInstance = ""; + if (isData) { + staticInstance = + "\n @SuppressWarnings(\"rawtypes\")\n" + + " private static final " + metaClassName + " INSTANCE = new " + metaClassName + "();\n\n" + + " @SuppressWarnings(\"unchecked\")\n" + + " public static " + metaClassName + " instance() {\n" + + " return INSTANCE;\n" + + " }\n"; + } String footer = "}\n"; try (Writer writer = fileObject.openWriter()) { writer.write(header); writer.write(body); writer.write(constructors); writer.write(fullCtor); + writer.write(staticInstance); writer.write(footer); } } catch (Exception e) { @@ -1269,6 +1281,7 @@ private void generateSealedMetamodelInterface(@Nonnull TypeElement sealedInterfa String metaInterfaceName = typeName + "_"; StringBuilder fields = new StringBuilder(); + String modelRef = typeName + "Metamodel.<" + typeName + ">instance()"; for (ExecutableElement getter : declaredGetters) { String fieldName = getter.getSimpleName().toString(); TypeMirror fieldType = getter.getReturnType(); @@ -1283,8 +1296,8 @@ private void generateSealedMetamodelInterface(@Nonnull TypeElement sealedInterfa fields.append(" /** Represents the {@link ").append(typeName).append("#").append(fieldName) .append("()} record. */\n"); fields.append(" ").append(fieldTypeName).append("Metamodel<").append(typeName).append("> ") - .append(fieldName).append(" = new ").append(typeName).append("Metamodel<") - .append(typeName).append(">().").append(fieldName).append(";\n"); + .append(fieldName).append(" = ").append(modelRef).append(".") + .append(fieldName).append(";\n"); } else { String valueTypeName = getValueTypeName(fieldType, packageName); boolean unique = isUniqueFieldOnSubclass(sealedInterface, fieldName); @@ -1293,8 +1306,8 @@ private void generateSealedMetamodelInterface(@Nonnull TypeElement sealedInterfa .append("()} field. */\n"); fields.append(" ").append(baseClass).append("<").append(typeName).append(", ").append(fieldTypeName) .append(", ").append(valueTypeName).append("> ") - .append(fieldName).append(" = new ").append(typeName).append("Metamodel<") - .append(typeName).append(">().").append(fieldName).append(";\n"); + .append(fieldName).append(" = ").append(modelRef).append(".") + .append(fieldName).append(";\n"); } } if (!fields.isEmpty()) { @@ -1616,12 +1629,23 @@ private void generateSealedMetamodelClass(@Nonnull TypeElement sealedInterface, " this(path, field, inline, parent, getter, false);\n" + " }\n"; } + String staticInstance = ""; + if (isData) { + staticInstance = + "\n @SuppressWarnings(\"rawtypes\")\n" + + " private static final " + metaClassName + " INSTANCE = new " + metaClassName + "();\n\n" + + " @SuppressWarnings(\"unchecked\")\n" + + " public static " + metaClassName + " instance() {\n" + + " return INSTANCE;\n" + + " }\n"; + } String footer = "}\n"; try (Writer writer = fileObject.openWriter()) { writer.write(header); writer.write(body); writer.write(constructors); writer.write(fullCtor); + writer.write(staticInstance); writer.write(footer); } } catch (Exception e) {