Repository navigation
Document critter runtime generation's class loader requirement - #20
Open
evanchooly wants to merge 1 commit into
Open
evanchooly wants to merge 1 commit into
evanchooly wants to merge 1 commit into
Conversation
Runtime generation can't access entities outside Morphia's module or class loader; recommend critter-maven for app-server, shared-lib and JPMS layouts. Refs MorphiaOrg/morphia#4357
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The documentation accurately captures the runtime constraint and provides clear deployment guidance.
Review effort: Balanced
Findings: None
What changed in this PR
Documents runtime critter generation’s class-loader and JPMS limitations, addressing MorphiaOrg/morphia#4357.
Changes:
- Explains when runtime generation falls back to reflection.
- Recommends AOT generation for isolated class-loader or module layouts.
| File | Description |
|---|---|
content/morphia/3.0/maven-plugin.md |
Documents class-loader/module requirements and AOT behavior. |
content/morphia/3.0/configuration.md |
Links mapper configuration guidance to the new section. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Oct 5, 2026
Merged
Merged
Merged
Merged
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 5, 2026
The `JdkTests` job's matrix lists `java: [ 17, 21, 25 ]`, but it passed
a hard-coded `java: 17` to the shared workflow. So all three legs (times
both mappers) ran on JDK 17, and 21 and 25 were never tested. It now
passes `${{ matrix.java }}`.
The legs still use the build compiled on JDK 17 (`reuseBuild: true`),
which is what we want: JDK 17 bytecode running on newer JVMs.
Expect this PR's CI to be the first real run on 21 and 25, so any
failures there are newly visible, not newly introduced.
Found while working on #4360.
## Merge order
This PR is one of a batch that came out of the critter review issues
(#4352–#4359). Suggested merge order:
1. #4361, #4360 (CI only, independent)
2. #4362, then #4366 (#4366 depends on #4362)
3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367
(independent, any order)
4. #4368 last (conflicts with #4363 and #4365 in
`TestCritterMapper.java`; rebase after they merge)
**This PR:** no dependencies. Merge any time.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 5, 2026
…4360) Refs #4353 ## What - **`pom.xml`**: new `final-field-mutation` profile. It activates only when **both** the JDK is 26+ (`<jdk>[26,)</jdk>`) **and** `-Dfinal.field.mutation=<mode>` is set. It sets the `argLine` property to `--illegal-final-field-mutation=<mode>`, so surefire passes it to the forked test JVM. On older JDKs the profile stays off, because those JVMs reject the option with `Unrecognized option` and won't start. It uses the `argLine` property instead of surefire `<configuration>`, so it still works with jacoco's `prepare-agent` in the `coverage` profile. - **`.github/workflows/build.yml`**: new `FinalFieldMutationTests` job. It runs `reflection` and `critter` on JDK 26 with `-Dfinal.field.mutation=deny`, using `reuseBuild` like the other legs. It is **`optional: true`** (`continue-on-error`) and is not in `Release.needs`. Option and values come from [JEP 500](https://openjdk.org/jeps/500): `--illegal-final-field-mutation=allow|warn|debug|deny`. The JDK 26 default is `warn`. With `deny`, `Field::set` on a final field throws `IllegalAccessException`. ## Why non-blocking This leg is **expected to fail** for now. Morphia still mutates final fields in the reflection mapper's `FieldAccessor`, critter's runtime nestmate accessors, critter's AOT `__writeXxx` for final fields, and `ConstructorCreator`'s re-set after construction (#4352). The leg's job is to list every remaining dependency and catch new ones. Once those paths are removed or put behind an explicit opt-in, drop `optional: true` so it blocks. This PR uses `Refs` rather than `Fixes` because #4353 also asks for a decision on entities whose finals can only be set reflectively. ## Reproduce locally (JDK 26+) ``` ./mvnw install -DskipTests cd core && ../mvnw surefire:test -Dmorphia.mapper=critter -Dfinal.field.mutation=deny ``` ## Notes - I had no JDK 26 locally, so I haven't yet seen which tests fail under `deny`. The first CI run of this leg will be the first list. - Separate issue, not changed here: the existing `JdkTests` matrix defines `java: [17, 21, 25]` but passes `java: 17` to the reusable workflow, so every JDK leg currently runs on 17. ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies. The new JDK 26 leg is non-blocking and expected to fail until the reflective final-field writes are removed (#4362 removes the most common one).
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 5, 2026
) Fixes #4352 ## Change `ConstructorCreator.set()` used to queue every decoded property into `pendingModels`/`pendingValues`, and `getInstance()` re-set all of them after construction, including the ones the constructor had just received. For an immutable, constructor-mapped entity that meant a redundant reflective `Field.set` on each `final` field for every decoded document. JDK 26 warns about that under [JEP 500](https://openjdk.org/jeps/500). Now only properties with no matching constructor parameter are queued. Properties the chosen constructor doesn't take, like the extra fields when `bestConstructor` picks a partial constructor, are still set after construction, as before. ## Behavior change If a constructor transforms its argument (normalizes, copies, wraps it in an unmodifiable collection, etc.), the constructor's result is kept. Before, the raw decoded value silently overwrote it. This is almost certainly what users expect, but it is a visible change for any entity that relied on the overwrite. ## Tests - New `ConstructorCreatorTest#constructorArgumentsAreNotResetAfterConstruction`: an immutable entity whose constructor lowercases/trims `name` and sorts `tags` into an unmodifiable list, plus a non-constructor field `note`. It checks that the normalized values survive the decode and that `note` is still set. With the fix reverted it fails under both mappers (`expected: <mixed case> but was: < MiXeD Case >`). - Full `morphia-core` suite: - critter: 1300 run, 0 failures, 0 errors, 16 skipped - reflection: 1300 run, 0 failures, 0 errors, 16 skipped ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies. **#4366 depends on it**; merge this first.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
…models (#4363) `CritterMapper(CritterMapper other, MorphiaConfig config)` reused `other.runtimeModels` even when `config` changed settings that runtime generation bakes into bytecode (collection/property naming, discriminator function/key, property discovery, annotation providers). Entities first mapped through the copy got classes generated under the original's settings. The copy now keeps the original's runtime models when the config is the same instance or both configs have equal generation keys. Otherwise it uses `RuntimeModels.forConfig(config, classLoader)`, which means unshared models when the key is null (custom strategies) or the parent is a `CritterClassLoader`. Entities already mapped are still cloned from the original as before. The common copy paths (`copy()`, the datastore copy, and copies that change only the database name) still share generated classes. Tests: - `testCopyWithDifferentNamingGeneratesItsOwnModels`: fails without the fix (`expected: <Name> but was: <name>`) - `testCopyWithDifferentDatabaseSharesRuntimeModels`: a copy that changes only the database still reuses the generated class Core suite: 1301 tests, 0 failures, 16 skipped, with both critter and reflection mappers. Fixes #4356 ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies. **#4368 conflicts with it** in `TestCritterMapper.java`, so merge this before #4368.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
…or (#4364) Fixes #4355 The `__writeXxx` method that `AddFieldAccessorMethods#writeFinalField` weaves for final fields called `Field.set` without handling its checked `IllegalAccessException`, so a failure surfaced as an undeclared checked exception. The `Field.set` call now sits in a `trying(...)` block that catches `Exception` and rethrows `new RuntimeException("Failed to set final field '<name>'", e)`. This is the same type, message and catch scope as the runtime path in `NestmateAccessorGenerator`, so both tiers fail the same way. The catch covers all `Exception`s, not only `IllegalAccessException`, to match the runtime tier, and every failure names the field. The Field lookup and caching stay outside the try. **Test:** `TestGeneration#testGeneratorFinalFieldWriteFailureIsWrapped` puts a Field that was never made accessible into the woven class's `__fieldCode` cache. That makes `Field.set` throw `IllegalAccessException` through the AOT accessor. The test then checks the exception type, message and cause. With the fix reverted, the test fails because the raw `IllegalAccessException` reaches the caller. **Results:** core 1300 tests (16 skipped), 0 failures under both critter and reflection. critter-maven verify passes (6 unit tests and 3 invoker tests). ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies; merges cleanly with the rest of the batch.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
…#4365) Fixes #4357 Runtime critter generation defines property accessors as hidden nestmates (`privateLookupIn(...).defineHiddenClass(..., NESTMATE)`), which needs full privilege access to the entity, i.e. the entity must be in Morphia's module (on the classpath: the same class loader). In app-server shared-lib, isolated-loader, or JPMS layouts this failed with a raw `IllegalAccessException` message in the fallback warning. - `CritterGenerator.defineNestmate` wraps an `IllegalAccessException` from `privateLookupIn`/`defineHiddenClass` (only those calls) in a new internal `NestmateAccessException`. - `CritterMapper.tryRuntimeGeneration` looks for it in the cause chain and logs a clear warning: the entity isn't in Morphia's module/class loader, falling back to reflection, pre-generate with critter-maven. Other failures are logged as before. - Tests: an entity compiled at test time and loaded by its own `URLClassLoader` falls back to a reflective `EntityModel` and logs the new message once; a unit test covers the cause-chain classification. - Updated `.claude/skills/diagnose/SKILL.md`. Docs: MorphiaOrg/morphia-docs#20 Core suite: critter 1301 run, 0 failures/errors, 16 skipped; reflection 1301 run, 0 failures/errors, 16 skipped. ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies. **#4368 conflicts with it** in `TestCritterMapper.java`, so merge this before #4368.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
Adds round-trip tests (save, then find through the Datastore) for Java records mapped as entities. Covers #4354. **Depends on #4352.** These tests fail under every mapper and tier until `ConstructorCreator` stops re-setting properties it already passed to the constructor. ## What's covered `dev.morphia.test.records.TestRecordEntities`: - a record `@Entity` with an `@Id`, a `String`, an `int` renamed with `@Property("years")`, and a `List<String>`. The test checks the stored document's shape, then finds by `_id` and by the renamed component. - a record embedded in a regular class entity, both as a single field and in a list. Each scenario runs against two sets of fixtures: | Fixtures | Package | Critter tier | |---|---|---| | `AotRecordPerson` / `AotRecordAddress` / `AotRecordHolder` | `dev.morphia.test.models.records` (under `morphia.packages`) | AOT, generated by critter-maven `generate-test-models` | | `RuntimeRecordPerson` / `RuntimeRecordAddress` / `RuntimeRecordHolder` | `dev.morphia.test.records` (outside `morphia.packages`) | runtime generation | Every test asserts which tier built the model: a reflection `EntityModel` under `reflection`, and under `critter` a `CritterEntityModel` whose class loader is or isn't a `CritterClassLoader`. If critter quietly falls back to another tier, the test fails rather than passing against the wrong tier. ## Current failure (all 4 tests, both mappers) Saving works. Decoding fails in `ConstructorCreator.getInstance()`. The canonical constructor builds the record correctly, then the `pendingModels` loop calls `PropertyModel.setValue` on every component again. `Field.set` always throws for record fields, even after `setAccessible(true)`: - reflection: `FieldAccessor.set` -> `IllegalAccessException: Can not set final ... AotRecordPerson.id` - critter AOT: the generated `IdAccessor.set` -> the injected `AotRecordPerson.__writeId` -> `Field.set` -> same exception - critter runtime: `Failed to set final field 'id'` I checked this locally. Queuing only the properties that have no constructor parameter (the #4352 fix) makes all 4 tests pass under both `critter` and `reflection`, with the tier assertions passing too. That change is not in this PR. Critter generation needed no fixes: AOT and runtime both generate models for the records without complaint. ## Full core suite (on this branch, without the #4352 fix) - critter: 1303 run, 0 failures, 4 errors (the new tests), 16 skipped - reflection: 1303 run, 0 failures, 4 errors (the new tests), 16 skipped Fixes #4354 ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** #4362 is merged and this branch includes it, so it's ready to merge. ### CI note Pull-request CI runs `install` without critter-maven's `generate-test-models`, so there are no pre-generated models there and the `Aot*` fixtures get runtime-generated models. The tier check expects AOT exactly when the pre-generated model is on the classpath (push builds and local runs that generate them) and runtime generation otherwise. With the check forced to expect runtime, the AOT tests fail when models are present, so it still catches a silent tier fallback. Follow-up for the CI gap: #4369.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
…cs (#4367) Fixes #4358 - **`CritterClassLoader.findClass`**: the name is now added to `definedTypes` only after `defineClass` succeeds, so a `ClassFormatError`/`VerifyError` from bad bytes no longer leaves `getResource` hiding the parent's `.class` resource. The removed bytes are deliberately not restored: a retry would define the same bytes and fail the same way. - **`CritterMapper`**: the class-level and `tryRuntimeGeneration` javadocs now say runtime generation uses hidden nestmate accessors, not VarHandles. - **`CritterClassLoader.shouldRegister`**: the comment now explains that `NestmateAccessorRegistry`'s map, though keyed per `CritterClassLoader`, is a static field, so the generator (which registers through the parent's copy of the class) and the generated models (which look accessors up) must see the same `Class`. New test `CritterClassLoaderTest.failedDefinitionsDoNotHideResources` registers invalid bytes for `dev.morphia.critter.Critter`, expects `ClassFormatError` on load, then asserts the `.class` resource is visible again. It fails without the fix. Core suite: 1300 tests, 0 failures, 16 skipped with both `-Dmorphia.mapper=critter` and `reflection`. ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** no dependencies; merges cleanly with the rest of the batch.
evanchooly
added a commit
to MorphiaOrg/morphia
that referenced
this pull request
Oct 6, 2026
…se (#4368) Fixes #4359 Test-only changes in `TestCritterMapper`: - **Tier assertions.** New `assertRuntimeGenerated(EntityModel)` helper checks the model is a `CritterEntityModel` whose class was loaded by a `CritterClassLoader`. It's used in every test meant to exercise runtime generation, so they fail loudly instead of quietly testing AOT models if `dev.morphia.mapping` is ever added to the test `morphia.packages`. It isn't used in the reflection-fallback, imported-model (`register`), or null/non-entity tests. - **Weak-cache release.** `testRuntimeClassLoaderReleasedWithItsMappers` maps an entity through two sharing mappers (plus a copy) inside a helper that returns only a `WeakReference` to the generated loader, then runs `System.gc()` in a bounded loop (up to 10s) and asserts the reference clears. The config uses a discriminator key no other test uses, so no other mapper in the JVM can share and pin that `RuntimeModels`. This covers both `CritterMapper.RuntimeModels.SHARED` and `NestmateAccessorRegistry`. No leak found: the loader is collected. I checked the test can fail by temporarily holding the mapper in a static field, and the test then failed as expected. - **Concurrency.** `testConcurrentMappingAcrossSharingMappers`: 8 threads, each with its own `CritterMapper` using the same config (a fresh key, so generation actually races), released together by a `CountDownLatch`. All succeed, all models share one generated class, each mapper registers its own model instance. Full core suite: 1301 tests, 0 failures, 0 errors, 16 skipped, with both `-Dmorphia.mapper=critter` and `-Dmorphia.mapper=reflection`. ## Merge order This PR is one of a batch that came out of the critter review issues (#4352–#4359). Suggested merge order: 1. #4361, #4360 (CI only, independent) 2. #4362, then #4366 (#4366 depends on #4362) 3. #4364, #4363, #4365 (+ MorphiaOrg/morphia-docs#20), #4367 (independent, any order) 4. #4368 last (conflicts with #4363 and #4365 in `TestCritterMapper.java`; rebase after they merge) **This PR:** **merge last.** It conflicts with #4363 and #4365 in `TestCritterMapper.java` (all three add tests there); rebase onto master after they merge and re-run the suite.
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.
Adds a "Class loaders and modules" section to the Maven plugin page and a pointer from the configuration page: runtime critter generation defines hidden nestmate accessors, which needs the entity to be in Morphia's module (on the classpath, the same class loader). In app-server shared-lib, isolated class loader, or separate-JPMS-module layouts it falls back to reflection; critter-maven AOT models avoid this.
Refs MorphiaOrg/morphia#4357
Merge order
Documents the behavior added in MorphiaOrg/morphia#4365; merge alongside or after it. No other dependencies.