feat: add spring-boot-morphium as optional module - #299
Conversation
Adopt the spring-boot-morphium repository (branch move-to-morphium) as an optional extension module, following the same pattern as morphium-jakarta-data (M2) and quarkus-morphium (M4). Copied per the M5-T4 dry-run copy-list (Abschnitt 1): root pom.xml, the three publishable modules (morphium-spring-boot-autoconfigure/-starter/-test, each pom.xml + src/), README.md, CHANGELOG.md and docs-for-morphium/spring-boot.md. Repo-wide policy/CI files (.git, target/, MIGRATION-NOTES.md, LICENSE, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, .github/, .gitignore, .DS_Store) are intentionally not carried over. The module-local spring-boot.version property is removed from spring-boot-morphium/pom.xml since it now inherits from morphium-parent (see next commit); the spring-boot-dependencies BOM import stays in the module POM per invariant I4.
Register the spring-boot-morphium module in the extensions profile, after morphium-jakarta-data and quarkus-morphium (in that order), plus a spring-boot.version property (3.4.13), analogous to the existing quarkus.version pattern. The spring-boot-dependencies BOM import stays in spring-boot-morphium/pom.xml (invariant I4) -- only the version property moves here so a Spring Boot upgrade is a single-line change in morphium-parent.
- docs/spring-boot.md: copied from spring-boot-morphium/docs-for-morphium/spring-boot.md - mkdocs.yml: register the Spring Boot page in the Extensions nav section (Jakarta Data, Quarkus Extension, Spring Boot), replacing the M5 placeholder comment left by the quarkus-morphium wave - docs/index.md: add a Spring Boot entry to the Extensions (Optional Modules) section, in the style of the existing Jakarta Data / Quarkus Extension entries - CHANGELOG.md: entry for spring-boot-morphium under Unreleased/Added, after the quarkus-morphium entry, covering the three published artifacts, feature set, the two pre-integration naming corrections (module rename and property prefix rename), lockstep versioning with migration guidance, core independence via -DskipExtensions, no Docker requirement, and provenance from Bardioc1977/spring-boot-morphium README.md/README.de.md intentionally left unchanged: neither carries a module overview listing morphium-jakarta-data or quarkus-morphium either (verified against the M2/M4 commits), so there is no existing structure to extend.
Registers morphium-spring-boot-autoconfigure, morphium-spring-boot-starter, and morphium-spring-boot-test through the module registry (MODULE_DIRS/ MODULE_ARTIFACT_IDS/MODULE_EXTRA_CLASSIFIERS), and adds morphium-spring-boot-parent as its own POM-only special case at both the dry-run and real-release bundle-building sites, mirroring morphium-parent/quarkus-morphium-parent. Unlike quarkus-morphium/integration-tests, morphium-spring-boot-test IS meant for publication: it is a user-facing test-support helper (the @MorphiumTest composite annotation), analogous to poppydb as its own published artifact, not an internal test suite -- this is a deliberate decision, not an assumption carried over from the quarkus-morphium precedent. Also extends ALL_POM_FILES with spring-boot-morphium/pom.xml, same reasoning as the quarkus-morphium/pom.xml entry added in M4: mvn versions:set bumps every pom.xml in the reactor regardless of registry membership. Fixes a real packaging gap found while verifying: morphium-spring-boot-starter has no source files at all (by design -- an empty jar that only pulls in morphium-spring-boot-autoconfigure via a single Maven coordinate, following Spring Boot's own starter convention). With a completely empty src/main/java, maven-source-plugin and maven-javadoc-plugin silently produced no -sources.jar/-javadoc.jar at all -- confirmed real Spring Boot starters (e.g. spring-boot-starter-web) on Maven Central DO publish both, so this needed a fix, not acceptance. Added a minimal package-info.java so both plugins have a compilation unit to process; verified all three modules now produce jar+sources+javadoc after the fix, and the full reactor build/tests remain green.
…ation, not a plain @component A plain @component in this library's own package is only picked up by Spring Boot's component scan when the scan happens to cover that package -- for any real application depending on morphium-spring-boot-starter as an external jar, component scan starts in the application's own base package and never reaches here, so the aspect bean was never created and @MorphiumTransactional methods ran without startTransaction()/commit/abort at all, silently. Changed to @autoConfiguration and registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports alongside MorphiumAutoConfiguration and MorphiumHealthAutoConfiguration, so Spring Boot's auto-configuration import mechanism instantiates it regardless of the application's package structure. Adds MorphiumTransactionAspectTest verifying the aspect bean actually exists in the context and that @MorphiumTransactional methods commit on normal return / abort on exception. Found in code review on PR #18 (Bardioc1977/morphium).
…ived queries asynchronously Two related bugs in MorphiumRepositoryInvocationHandler: 1. buildConditionsSpec() (used by @Find/@delete methods) read @PARAM instead of Jakarta Data's @by for field binding. Without -parameters (the parent compiler config does not set it), the fallback to the reflected parameter name produced conditions like "arg0:0" instead of the intended field name; even with parameter names available, an explicit @by value was ignored entirely. FindMethodBridge then queried the wrong/non-existent field, so annotated @Find/@delete methods returned no matches or affected the wrong data. Now reads @by, matching quarkus-morphium's MorphiumDataProcessor. 2. Derived query methods declared with a CompletionStage return type (e.g. findByStatusAsync) were excluded from returnsSingle but never dispatched to QueryMethodBridge.executeQueryAsync -- they always ran the synchronous executeQuery, so the generated proxy tried to cast the raw result (List, Long, etc.) to CompletionStage and threw ClassCastException instead of running asynchronously. Also strips the "Async" method-name suffix before parsing (e.g. "findByStatusAsync" -> "findByStatus"), matching quarkus-morphium's MorphiumDataProcessor convention -- without stripping it, MethodNameParser misreads the suffix as part of the field name and the query matches nothing. Adds regression tests for both: an @find+@by method against real InMemDriver data, and a CompletionStage-returning derived query resolved from a real CompletableFuture. Found in code review on PR #18 (Bardioc1977/morphium).
The M5 branch predates two version bumps on develop (6.3.0 -> 6.3.2).
morphium-parent's version moved on without these four POMs, which
still pointed at 6.3.0-SNAPSHOT -- Maven resolved the stale parent
model, so ${spring-boot.version} (defined only in the current
morphium-parent) was never interpolated and the BOM import failed
with a literal ${spring-boot.version} in the coordinate.
Also drops the explicit relativePath on the top-level module POM,
matching quarkus-morphium and morphium-jakarta-data, which rely on
plain reactor resolution instead.
|
On the second open question — I'll answer that one myself rather than leave it hanging: |
There was a problem hiding this comment.
Pull request overview
This PR adds spring-boot-morphium, the third and final optional integration module in the modularization series (following #266 morphium-jakarta-data and #267 quarkus-morphium). It provides a Spring Boot auto-configuration for Morphium with Jakarta Data repository support, delegating all query logic to the shared morphium-jakarta-data runtime and adding only the Spring-specific wiring. The core remains untouched and the module builds only under the extensions profile.
Changes:
- Three new Maven modules (
morphium-spring-boot-autoconfigure,-starter,-test): auto-configuredMorphiumbean frommorphium.*properties, Jakarta Data repositories via JDK dynamic proxies,@MorphiumTransactionalAOP aspect (registered as an auto-configuration), an Actuator health indicator, and a@MorphiumTestcomposite annotation. - Reactor/registration wiring:
spring-boot.versionproperty + module entry in rootpom.xml,release.shmodule-registry additions, andmkdocs.ymlnav entry. - Documentation: module README,
docs/spring-boot.md,docs/index.mdpointer, and root/module changelog entries.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
.../MorphiumTransactionAspect.java |
AOP transaction advice registered as auto-configuration; missing explicit after= ordering vs its @ConditionalOnBean. |
.../MorphiumAutoConfiguration.java |
Builds/connects the Morphium bean with retry, SSL, and entity pre-scan. |
.../MorphiumHealthAutoConfiguration.java |
Actuator health indicator, correctly ordered after MorphiumAutoConfiguration. |
.../MorphiumRepositoryRegistrar.java / FactoryBean.java / InvocationHandler.java |
Repository scanning + JDK-proxy dispatch to jakarta-data bridges (signatures verified). |
.../MorphiumProperties.java |
@ConfigurationProperties for morphium.* keys. |
.../EnableMorphiumRepositories.java, MorphiumTransactional.java |
Public annotations for repository scanning and transactions. |
spring-boot-morphium/*/pom.xml, pom.xml |
Module POMs, BOM import, annotation-processor path, centralized spring-boot.version. |
release.sh |
Adds the three modules + spring-boot parent to the release bundle (arrays consistent). |
| Test sources | AutoConfig, proxy, and transaction regression tests using InMemDriver. |
README.md, docs/spring-boot.md, docs-for-morphium/spring-boot.md, docs/index.md, mkdocs.yml, changelogs |
Documentation; contain a stale 6.3.0-SNAPSHOT version and a broken README link. |
Suppressed comments (1)
docs/spring-boot.md:304
- This link points to
morphium-spring-boot-starter/README.md, but the module README actually lives atspring-boot-morphium/README.md(there is no README undermorphium-spring-boot-starter/), so this link is broken. Also useblobrather thantreefor a file path. The same broken link appears inspring-boot-morphium/docs-for-morphium/spring-boot.md:304.
[`morphium-spring-boot-starter/README.md`](https://github.com/sboesebeck/morphium/tree/develop/morphium-spring-boot-starter/README.md)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| * }</pre> | ||
| */ | ||
| @Aspect | ||
| @AutoConfiguration |
| <dependency> | ||
| <groupId>de.caluga</groupId> | ||
| <artifactId>morphium-spring-boot-starter</artifactId> | ||
| <version>6.3.0-SNAPSHOT</version> |
| </dependency> | ||
| ``` | ||
|
|
||
| In the Morphium reactor, `${project.version}` currently resolves to `6.3.0-SNAPSHOT`. |
| </dependency> | ||
| ``` | ||
|
|
||
| In the Morphium reactor, `${project.version}` currently resolves to `6.3.0-SNAPSHOT`. |
| `InMemDriver` is Morphium's in-memory MongoDB emulation — tests run against it with no | ||
| container and no external MongoDB, exactly like the core Morphium test suite. | ||
|
|
||
| ## Abgrenzung zu Spring Data MongoDB |
- MorphiumTransactionAspect: order after MorphiumAutoConfiguration, matching MorphiumHealthAutoConfiguration. @ConditionalOnBean is evaluated against beans registered so far, so without this it only worked by alphabetical-sort coincidence -- any rename could disable the aspect silently. - Sync stale 6.3.0-SNAPSHOT to the actual reactor version (6.3.2) in README.md, docs/spring-boot.md, docs-for-morphium/spring-boot.md. - Fix broken README cross-link (wrong module path, tree instead of blob) in the two doc copies. - Translate a German section heading in the two published (English) doc pages, third occurrence not flagged by review but same drift.
|
Copilot's four findings are all real, fixed in
Full reactor build green, 15/15 module tests still green after the change. |
sboesebeck
left a comment
There was a problem hiding this comment.
Reviewed at 4f3d117, built and tested locally. Conceptually this is well put together and the separation from core is clean — git diff --stat confirms your optionality claim, and the @AutoConfiguration-instead-of-@Component reasoning on MorphiumTransactionAspect is exactly right.
The problem is coverage: the 15 existing tests are green, but they only exercise the paths the proxy actually implements. I added six regression tests for features the module's own README advertises, and all six fail.
Test evidence
Module suite is now 21 tests — the 15 existing ones stay green, the six new ones are red:
Tests run: 21, Failures: 1, Errors: 5, Skipped: 0
derivedPageReturnUsesPageRequest ClassCastException: java.util.ArrayList cannot be cast to
jakarta.data.page.Page
derivedSortParameterIsApplied AssertionFailedError: expected: <2> but was: <1>
(Sort argument silently ignored, insertion order returned)
annotatedDeleteReturnsCount NullPointerException: Cannot invoke "java.lang.Long.longValue()"
because the return value of InvocationHandler.invoke(...) is null
annotatedFindCanReturnCompletionStage ClassCastException: TestEntity cannot be cast to
java.util.concurrent.CompletionStage
jdqlCanReturnCompletionStage ClassCastException: TestEntity cannot be cast to
java.util.concurrent.CompletionStage
repositoryDefaultMethodExecutes UnsupportedOperationException: Unsupported repository method:
TestEntityRepository.countActiveViaDefaultMethod
The common thread on 1–3: the shared morphium-jakarta-data module already has the bridge overloads these paths need (QueryMethodBridge.executeQuery with the four param indices, FindMethodBridge.executeAnnotatedDeleteCounted, executeFindAsync, JdqlMethodBridge.executeJdqlAsync). The Spring handler simply never calls them, while the Quarkus generator does. So these should be small fixes, not new functionality.
Findings
Blocking (details inline):
- Derived queries ignore dynamic pagination/sorting —
buildDerivedQueryHandlernever looks forPageRequest/Sort/Order/Limitparameters and calls the simple bridge overload.Page<T>returns fail withClassCastException,Sortis dropped. Contradicts the README feature table ("Pagination", "Sorting"). @Deletewith anint/longreturn is broken — always dispatches to thevoidbridge and returnsnull, so unboxing NPEs. Jakarta Data 1.0 explicitly allows the delete count here.- Async only works for derived queries —
@Findand@Querymethods returningCompletionStagerun synchronously and are then cast wrongly. defaultmethods on repository interfaces don't work — they reach the normal dispatcher and hitUnsupportedOperationException; the proxy needs amethod.isDefault()branch usingInvocationHandler.invokeDefault(...).
Non-blocking:
- The entity pre-scan almost certainly doubles ClassGraph work rather than saving it — see inline; I traced the core call sites and the second scan is unavoidable in any real app.
- The README version will rot again at the next release —
release.shonly bumps the two top-level READMEs. This is a pre-existing, generic problem (quarkus-morphium/README.mdis already stale at6.3.0-SNAPSHOT), so I'd rather fix it inrelease.shseparately than ask you to patch it here. - Nested
@MorphiumTransactionalsemantics need deciding — currently the inner call throws and the outer aspect aborts the whole transaction. At minimum document it; better would be REQUIRED-style reuse.
My take: good foundation, preview is the right status, and the answer to your doc question is that docs/spring-boot.md (MkDocs) is enough — no separate Antora tree. Points 1–4 should be fixed and covered by tests before merge; 5–7 can follow.
The tests
Take these over as-is if they're useful. Two files, both under morphium-spring-boot-autoconfigure/src/test/java/de/caluga/morphium/spring/autoconfigure/.
TestEntityRepository.java — added methods (rest of the file unchanged)
Page<TestEntity> findByStatus(String status, PageRequest pageRequest);
List<TestEntity> findByStatus(String status, Sort<TestEntity> sort);
@Delete
long removeByStatus(@By("status") String status);
@Find
CompletionStage<List<TestEntity>> byStatusAsync(@By("status") String status);
@Query("WHERE status = :status")
CompletionStage<List<TestEntity>> queryByStatusAsync(@Param("status") String status);
default long countActiveViaDefaultMethod() {
return countByStatus("active");
}New imports: jakarta.data.Sort, jakarta.data.page.Page, jakarta.data.page.PageRequest,
jakarta.data.repository.Delete, jakarta.data.repository.Param, jakarta.data.repository.Query,
java.util.concurrent.CompletionStage.
MorphiumRepositoryProxyMissingCasesTest.java — new file
package de.caluga.morphium.spring.autoconfigure;
import de.caluga.morphium.Morphium;
import jakarta.data.Sort;
import jakarta.data.page.PageRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Regression coverage for repository-proxy features advertised by the Spring Boot module.
* These tests intentionally expose the currently missing dispatch paths in PR #299.
*/
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("test")
class MorphiumRepositoryProxyMissingCasesTest {
@Autowired
TestEntityRepository repository;
@Autowired
Morphium morphium;
@BeforeEach
void cleanUp() {
morphium.clearCollection(TestEntity.class);
}
@Test
void derivedPageReturnUsesPageRequest() {
repository.save(new TestEntity("a", "active", 1));
repository.save(new TestEntity("b", "active", 2));
assertEquals(1,
repository.findByStatus("active", PageRequest.ofSize(1)).content().size());
}
@Test
void derivedSortParameterIsApplied() {
repository.save(new TestEntity("a", "active", 1));
repository.save(new TestEntity("b", "active", 2));
List<TestEntity> result = repository.findByStatus("active", Sort.desc("priority"));
assertEquals(2, result.getFirst().getPriority());
}
@Test
void annotatedDeleteReturnsCount() {
repository.save(new TestEntity("a", "active", 1));
repository.save(new TestEntity("b", "active", 2));
assertEquals(2, repository.removeByStatus("active"));
}
@Test
void annotatedFindCanReturnCompletionStage() throws Exception {
repository.save(new TestEntity("a", "active", 1));
assertEquals(1,
repository.byStatusAsync("active").toCompletableFuture().get().size());
}
@Test
void jdqlCanReturnCompletionStage() throws Exception {
repository.save(new TestEntity("a", "active", 1));
assertEquals(1,
repository.queryByStatusAsync("active").toCompletableFuture().get().size());
}
@Test
void repositoryDefaultMethodExecutes() {
repository.save(new TestEntity("a", "active", 1));
assertEquals(1, repository.countActiveViaDefaultMethod());
}
}CI
Worth flagging separately: the only status on this head is security/snyk: success — there are no check runs, so no test suite has actually run against this PR on GitHub. That's the M6 CI-workflow item from your plan, but it means the green checkmark currently says nothing about the code.
| delegate, parseableName, args != null ? args : new Object[0], | ||
| returnsSingle, returnsOptional, returnsBoolean, returnsStream, orderBySpec); | ||
| } | ||
| return args -> QueryMethodBridge.executeQuery( |
There was a problem hiding this comment.
Blocking (1/4): derived queries ignore dynamic PageRequest/Sort/Order/Limit.
Unlike buildFindHandler and buildJdqlHandler right below, this method never calls findParamIndex(...) and always dispatches to the simple executeQuery overload. Two consequences, both covered by the new tests:
- a
Page<T>-returning method gets the plainListresult back and blows up in the proxy:
ClassCastException: java.util.ArrayList cannot be cast to jakarta.data.page.Page - a
Sort<T>argument is silently dropped —findByStatus("active", Sort.desc("priority"))returns insertion order, no error
Both are listed as supported in this module's README feature table ("Pagination: Page<T>, PageRequest, CursoredPage<T>" and "Sorting: Sort<T>, Order<T> as method parameters").
The fix should be mechanical: QueryMethodBridge.executeQuery already has the overload taking sortParamIndex/orderParamIndex/pageRequestParamIndex/limitParamIndex (QueryMethodBridge.java:234) — it is what the Quarkus generator calls, and it short-circuits back to the simple overload itself when all four indices are -1, so you can dispatch to it unconditionally. Note that isSpecialParam already exists here for exactly this parameter set.
| private MethodHandler buildDeleteHandler(Method method) { | ||
| String conditionsSpec = buildConditionsSpec(method); | ||
| return args -> { | ||
| FindMethodBridge.executeAnnotatedDelete( |
There was a problem hiding this comment.
Blocking (2/4): @Delete with an int/long return type is broken.
This always calls the void bridge and returns null, so a long removeByStatus(...) method fails at the proxy boundary with
NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of
"java.lang.reflect.InvocationHandler.invoke(Object, java.lang.reflect.Method, Object[])" is null
Jakarta Data 1.0 allows void, int and long for a @Delete method, where the numeric variants return the number of deleted entities. FindMethodBridge.executeAnnotatedDeleteCounted (FindMethodBridge.java:301) already provides that count and is currently unused from here — so this is a return-type switch on method.getReturnType(), picking the counted bridge for int/long/Integer/Long and keeping the current call for void.
| boolean returnsStream = Stream.class.isAssignableFrom(method.getReturnType()); | ||
| String orderBySpec = getOrderBySpec(method); | ||
|
|
||
| return args -> JdqlMethodBridge.executeJdql( |
There was a problem hiding this comment.
Blocking (3/4a): @Query methods returning CompletionStage execute synchronously and are then cast wrongly.
buildJdqlHandler has no CompletionStage branch, and isSingleReturn doesn't exclude it either — so a CompletionStage<List<TestEntity>> method is analysed as a single-entity query, executed on the calling thread, and the resulting entity is handed to the proxy as if it were the stage:
ClassCastException: TestEntity cannot be cast to java.util.concurrent.CompletionStage
JdqlMethodBridge.executeJdqlAsync (JdqlMethodBridge.java:848) exists for this and is never called from the Spring module.
Same issue in buildFindHandler below — see the separate comment there. Only buildDerivedQueryHandler handles async today, which makes the asymmetry easy to miss: the README documents "Async: CompletionStage<T> return type" without restricting it to derived queries.
| boolean returnsCursoredPage = CursoredPage.class.isAssignableFrom(method.getReturnType()); | ||
| boolean returnsStream = Stream.class.isAssignableFrom(method.getReturnType()); | ||
|
|
||
| return args -> FindMethodBridge.executeFind( |
There was a problem hiding this comment.
Blocking (3/4b): same async gap for @Find.
Mirror of the buildJdqlHandler comment above: no CompletionStage branch, isSingleReturn doesn't exclude it, so @Find CompletionStage<List<TestEntity>> byStatusAsync(...) runs synchronously and then fails with
ClassCastException: TestEntity cannot be cast to java.util.concurrent.CompletionStage
FindMethodBridge.executeFindAsync (FindMethodBridge.java:347) is the counterpart that should be called here.
Worth fixing both in one pass, and while you're in here: isSingleReturn returning true for any unrecognised wrapper type is what turns each of these gaps into a confusing ClassCastException at the call site rather than a clear error. An explicit unsupported-return-type check would make the next such gap self-diagnosing.
| return buildDerivedQueryHandler(method); | ||
| } | ||
|
|
||
| throw new UnsupportedOperationException( |
There was a problem hiding this comment.
Blocking (4/4): default methods on repository interfaces end up here.
A default method is a perfectly ordinary part of a Jakarta Data repository interface (the usual way to compose derived queries into a small piece of domain logic), but it matches none of the branches above and therefore fails with
UnsupportedOperationException: Unsupported repository method: TestEntityRepository.countActiveViaDefaultMethod
analyzeMethod needs a method.isDefault() branch before the derived-query check, dispatching via InvocationHandler.invokeDefault(proxy, method, args) (JDK 16+, so fine on the Java 21 baseline). Note that invokeDefault needs the proxy instance, which analyzeMethod's MethodHandler signature doesn't currently carry — either pass it through or special-case default methods directly in invoke before the handlers.computeIfAbsent(...) lookup.
| } | ||
| if (!typeIds.isEmpty()) { | ||
| AnnotationAndReflectionHelper.registerTypeIds(typeIds); | ||
| log.info("Pre-registered {} entity type IDs, Morphium will skip ClassGraph scan", typeIds.size()); |
There was a problem hiding this comment.
Non-blocking: this log line is not accurate, and the pre-scan most likely costs more than it saves.
AnnotationAndReflectionHelper.registerTypeIds(...) only short-circuits the type-ID initialisation in AnnotationAndReflectionHelper.init(). Everything else in core still goes through ClassGraphCache, which builds its own full ScanResult on first use:
| call site | annotation / query |
|---|---|
Morphium.java:264 |
@Messaging implementations |
Morphium.java:314 |
@Driver implementations |
Morphium.java:3358 |
@Capped |
Morphium.java:3404 |
@Entity (startup index check) |
ObjectMapperImpl.java:163 |
@Entity |
InMemoryDriver.java:4411 |
MongoCommand subclasses |
The @Driver lookup in particular is unconditional for this module: MorphiumProperties.driverName defaults to "PooledDriver" and buildConfig always sets it (:159), so Morphium's constructor always takes the driverName != null branch and triggers the cached scan. That scan would have covered the type IDs too.
So the net effect in a real application is two classpath scans instead of one — this method's own uncached new ClassGraph().enableAnnotationInfo().scan(), plus core's cached one — and the "Morphium will skip ClassGraph scan" message is wrong.
Simplest fix is to drop preRegisterEntities() entirely and let the cached scan serve everyone. If you'd rather keep a pre-registration hook (for a future Spring AOT/native story), the right one is ClassGraphCache.preRegisterClassesWithAnnotation(...), which the Quarkus module uses — but note its own javadoc: it only covers the name-based lookup path, not getClassInfoWithAnnotation/getSubclassesOf, so it would not make this scan-free either.
| @Around("@annotation(de.caluga.morphium.spring.autoconfigure.MorphiumTransactional) || " + | ||
| "@within(de.caluga.morphium.spring.autoconfigure.MorphiumTransactional)") | ||
| public Object aroundTransactional(ProceedingJoinPoint pjp) throws Throwable { | ||
| morphium.startTransaction(); |
There was a problem hiding this comment.
Non-blocking, but please decide and document: nested @MorphiumTransactional calls.
startTransaction() is called unconditionally, and all three drivers reject a second one while a transaction is open — DriverBase.java:626 (PooledDriver/SingleMongoConnectDriver) and InMemoryDriver.java:5069 both throw IllegalArgumentException.
So one @MorphiumTransactional service method calling another currently means: the inner advice throws, the exception propagates into the outer advice's catch, which aborts the outer transaction and rethrows. All work of the outer method is lost, and the message the developer sees (transaction in progress) doesn't point at the cause. Since Spring developers arrive with @Transactional's REQUIRED default in mind, this shape will be encountered.
Options, in rough order of preference:
- REQUIRED-style reuse — track nesting depth (thread-local counter, matching the drivers' thread-local transaction state) and let inner invocations join the outer transaction: commit only when the outermost returns, abort marks the whole thing rollback-only.
- Explicit propagation attribute on
@MorphiumTransactional, defaulting to REQUIRED. - At minimum: document that nesting is unsupported, and detect it here with a clear error instead of letting the driver's
IllegalArgumentExceptiontake out the outer transaction.
Also worth a doc line while you're here: the aspect has no rollback-rules concept, so unlike Spring's @Transactional (which by default rolls back on unchecked exceptions only) it aborts on any Throwable. That's a defensible choice, just a surprising difference.
| <dependency> | ||
| <groupId>de.caluga</groupId> | ||
| <artifactId>morphium-spring-boot-starter</artifactId> | ||
| <version>6.3.2-SNAPSHOT</version> |
There was a problem hiding this comment.
Non-blocking, and not really yours to fix — flagging so it doesn't get lost.
6.3.2-SNAPSHOT is hardcoded here and again at line 282, but release.sh only bumps the version snippets in the two top-level READMEs (README.md, README.de.md — see release.sh:280). So this will silently go stale at the next release.
That's not hypothetical: quarkus-morphium/README.md already says 6.3.0-SNAPSHOT in the same two places, one release cycle after #267.
The right fix is in release.sh — extend the bump loop over the extension modules' READMEs, ideally driven by the existing MODULE_DIRS registry so a fourth module is a registry entry rather than another hardcoded path. I'll handle that separately; no change needed in this PR.
|
Answers to your open questions — including the two from #266 and #267 that I left hanging, sorry about that. Docker in CI (#267)Rather not. The reason is a general one rather than anything about your PR: almost everywhere in this project we assume that PoppyDB stands in when a test would otherwise need a real MongoDB. That is what it exists for — same wire protocol, its own replica-set election, embeddable directly ( The good news is that your code is already built that way. If you want that class covered rather than skipped, PoppyDB is a real option and not just in theory: To be clear about what this does not mean: keep Dev Services as a feature. Auto-starting a MongoDB container is exactly right for a developer's laptop, and the Dev UI card and the container-startup path are worth having. This is only about what CI is allowed to depend on. CI workflow (#266) — yes, as its own PRYes please, as a separate follow-up, and I would now put it higher than "nice to have". Some context on why. Full pre-merge runs for this project happen on a dedicated machine here rather than on GitHub Actions: a five-phase matrix (in-memory, MongoDB replica set, PoppyDB replica set, MongoDB single, PoppyDB single) that takes about 2.5 hours. I looked closely at what it actually covers while reviewing this PR, and the answer is uncomfortable: it builds the whole reactor with The consequence is that the tests in I will widen the local gate to cover the extension modules regardless, since that is a one-line change on my side and does not need to wait for anything. But a GitHub-side workflow is the part that gives you a signal on a PR before I ever look at it, and right now the only status on this PR is Snyk. Given the Docker answer above, that workflow can be plain
|
…ries Three defects in the repository proxy's method dispatch, all of them features the module's README already advertises. Derived queries never looked up dynamic Sort/Order/PageRequest/Limit parameters and always dispatched to the simple bridge overload. A Page<T> method therefore got a plain List back (ClassCastException at the proxy boundary) and a Sort<T> argument was silently dropped, giving insertion order with no error at all. Both branches now resolve the four parameter indices the same way buildJdqlHandler already did and call the overload that takes them; it short-circuits back to the simple overload itself when no dynamic parameter is present. @delete always called the void bridge and returned null, so the int/long return types Jakarta Data permits failed unboxing null at the proxy. Numeric return types now use executeAnnotatedDeleteCounted and report the number of deleted entities. @query and @find had no CompletionStage branch, and isSingleReturn did not exclude CompletionStage either, so an async method was analysed as a single-entity query, ran on the caller's thread, and handed the entity itself back where a stage was expected. Both now dispatch to the async bridges, and isSingleReturn excludes CompletionStage so the stage completes with the list the signature promises rather than one element. Also dispatch default methods through InvocationHandler.invokeDefault. A default method carries its own implementation and matched none of the analysis branches, so it ended in "Unsupported repository method". Handled in invoke() rather than analyzeMethod() because invokeDefault needs the proxy instance, which the MethodHandler interface cannot carry, and checked before the derived-query branch since a default method is free to be named findBy*.
…cond REQUIRED propagation, same semantics as quarkus-morphium's MorphiumTransactionalInterceptor: when a transaction is already active on this thread, the advice participates in it rather than opening its own, and leaves commit/abort to the outermost advised call. Without this, one @MorphiumTransactional method calling another lost everything the outer method had done: all drivers reject a second startTransaction() with IllegalArgumentException, that exception propagated into the outer advice's catch, and the outer transaction was aborted. Spring developers arrive with @transactional's REQUIRED default in mind, so this shape is reached easily. No nesting counter is needed - Morphium already tracks the active transaction per thread, which is why the Quarkus interceptor tests getTransaction() rather than counting depth. Retry, write-buffer and CosmosDB handling from that interceptor are deliberately not copied here; they are separate concerns. Documented on the advice: the REQUIRED semantics, and that this aspect has no rollback-rules concept and therefore aborts on any Throwable, unlike Spring's @transactional which by default rolls back on unchecked exceptions only.
preRegisterEntities() ran its own uncached ClassGraph scan and called AnnotationAndReflectionHelper.registerTypeIds, logging that Morphium would skip its own scan. That claim was wrong: registerTypeIds only short-circuits type-ID initialisation, while every other lookup still goes through ClassGraphCache, which builds a full ScanResult on first use. The @driver lookup in particular is unconditional for this module, because driverName always has a value and buildConfig always sets it. Net effect in a real application was two classpath scans instead of one, plus a misleading log line. Removing the method lets the cached scan serve everyone, which it already did for type IDs too. Verified the method had a single call site and no test asserted on it.
One regression test per finding, each verified to fail without its fix. They assert the resulting value, not merely the absence of an exception, because three of these defects had a silent-wrong-result variant: - dynamic Sort: asserts the actual ordering of three differently prioritised entities, since dropping the argument produced insertion order without any error - Page return type: asserts a Page comes back at all - counted @delete: asserts the returned count AND that the rows are gone - CompletionStage on @query and @find: asserts the stage completes with a List rather than a single entity - the shape that a half-fix (async branch without excluding CompletionStage from isSingleReturn) would still have got wrong - default method: deliberately named countBy* so it also proves the isDefault() check wins over derived-query parsing - nested transactions: the inner call goes through a second proxied bean rather than a self-invocation, so the aspect really runs twice; asserts both documents survive, and that an inner failure rolls back both 23 tests total, up from 15.
|
All four blocking findings plus both non-blocking ones are fixed in The four blocking ones1. Dynamic 2. 3. 4. The two non-blocking ones
Nested The nesting test routes the inner call through a second proxied bean rather than a self-invocation — a self-call would bypass the aspect entirely and prove nothing. Verification
One I did not doThe explicit unsupported-return-type check. Every legal Jakarta Data return type for Also: agreed on the CI workflow, and thanks for digging into what the five-phase matrix actually covers — 63 extension test classes running in nobody's pipeline explains this batch of findings better than anything else could. I will open that as its own PR (plain |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (4)
spring-boot-morphium/morphium-spring-boot-autoconfigure/pom.xml:51
- The comment justifies the
classgraphdependency as an "Entity-Pre-Scan in MorphiumAutoConfiguration", butMorphiumAutoConfiguration(and the rest of this module's main sources) performs no such pre-scan — it only buildsMorphiumConfig, connects with retry, and forces the replica-set flag. As written,classgraphappears to be an unused direct dependency and the justification is inaccurate. Please either remove the dependency (and the docs/CHANGELOG claims that reference the pre-scan), or restore the pre-scan implementation if it was intended.
<!-- classgraph: Dependency-Deklaration bleibt bestehen (Entity-Pre-Scan in
MorphiumAutoConfiguration), die Versionsverwaltung übernimmt künftig
morphium-parent's dependencyManagement (siehe morphium/pom.xml) statt
einer eigenen Version hier. -->
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
</dependency>
docs/spring-boot.md:22
- This claims
MorphiumAutoConfigurationperforms "a best-effort classpath pre-scan for@Entity/@Embeddedclasses so Morphium can skip its own internal scan at startup", but no such pre-scan exists in the module's code (MorphiumAutoConfigurationonly builds config, connects with retry, and forces the replica-set flag). The same inaccurate claim appears in the identicalspring-boot-morphium/docs-for-morphium/spring-boot.mdcopy and in the rootCHANGELOG.md. Please drop this clause (or implement the pre-scan).
transient failures (linear backoff) and a best-effort classpath pre-scan for
`@Entity`/`@Embedded` classes so Morphium can skip its own internal scan at startup.
spring-boot-morphium/docs-for-morphium/spring-boot.md:22
- Same inaccurate claim as in
docs/spring-boot.md:MorphiumAutoConfigurationdoes not implement a classpath pre-scan for@Entity/@Embeddedclasses. Please drop this clause (or implement the pre-scan) to keep the docs consistent with the code.
transient failures (linear backoff) and a best-effort classpath pre-scan for
`@Entity`/`@Embedded` classes so Morphium can skip its own internal scan at startup.
CHANGELOG.md:509
- This changelog entry describes "a best-effort classpath pre-scan for
@Entity/@Embeddedclasses", but the module implements no such pre-scan (MorphiumAutoConfigurationonly builds config, connects with retry, and adjusts the replica-set flag). Remove this claim so the changelog matches the shipped behavior.
IDE autocompletion), connection retry with linear backoff on transient failures, and a
best-effort classpath pre-scan for `@Entity`/`@Embedded` classes. Jakarta Data `@Repository`
Copilot's second review pass caught what removing preRegisterEntities() in 767041f missed: an unused classgraph dependency declaration still justified by that method, and three prose copies (docs/spring-boot.md, its docs-for-morphium duplicate, CHANGELOG.md) still describing the pre-scan as a shipped feature. classgraph itself is not needed here - it already arrives transitively through morphium (morphium-core declares it, morphium-parent centralises the version), same as before the explicit declaration was added for the now-removed method.
|
Copilot's second pass caught what removing |
sboesebeck
left a comment
There was a problem hiding this comment.
All six findings verified fixed. Merging as preview.
Correction on my side first: you are right about the bridge overload — the one taking the four indices is QueryMethodBridge.java:86, and :234 is the one without them. I checked; my review had it backwards. Thanks for not just following the pointer.
Your catch on finding 3 is the more valuable half of that fix, and I had missed it: adding only the async branch removes the ClassCastException but leaves returnsSingle true, so the stage completes with one entity instead of the list the signature promises. A green build and a silently wrong result. Asserting the shape rather than the absence of an exception is the right call.
What I checked, beyond taking the descriptions at face value:
- Ran your 23 tests against the current
developcore in a separate worktree — 23/23,BUILD SUCCESS. Your branch predates yesterday's commits there, so this confirms no incompatibility with today's core. - Footprint outside the module is only what an optional module needs:
pom.xml,release.sh,docs/spring-boot.md,mkdocs.yml.morphium-coreandpoppydbuntouched — the one-way dependency holds. - Every
de.calugaimport in the module's main sources is public API (Morphium,MorphiumConfig,config.CollectionCheckSettings,annotations.Id). No driver internals, nothing fromdriver.inmem. - Config uses the sub-object style throughout (
driverSettings(),connectionSettings(),clusterSettings(),authSettings(),cacheSettings(),collectionCheckSettings()). The three remaining flat setters —setUseSSL,setSslContext,setAutoIndexAndCappedCreationOnWrite— are not deprecated and have no sub-object equivalent, so that is correct as written. - REQUIRED propagation goes through the public
morphium.getTransaction()rather than reaching into driver state. Right call. - Dispatch delegates to the shared bridges everywhere now instead of reimplementing query logic, which was the substance of the original findings.
- Your
release.shregistry change merges cleanly with myMODULE_README_FILEScommit — verified withgit merge-tree, no conflict.
One documentation note, not a blocker. The REQUIRED propagation differs from Spring's in one respect worth a sentence in the docs: if an outer @MorphiumTransactional method catches an exception from an inner one, this aspect still commits, whereas Spring would have marked the transaction rollback-only. Deliberate given you did not copy the interceptor's other machinery, and fine for preview — just worth stating so nobody discovers it in production.
Reminder for the follow-up you already offered: add spring-boot-morphium/README.md to MODULE_README_FILES in release.sh.
On the CI workflow — one concrete finding from wiring the extension modules into our own test matrix last night, which will save you a debugging session. The first run that ever executed them here failed all 37 quarkus-morphium/integration-tests classes with QuarkusBindException: Port already bound: 8081. Not your code: 8081 is Quarkus's default test port and our dashboard's API happens to sit on it. I fixed it on our side with -Dquarkus.http.test-port=0. A GitHub runner will not have that specific collision, but the ITs binding a fixed well-known port is worth knowing about when you write the workflow. The other three modules were green: jakarta-data 82 tests, quarkus runtime 63, quarkus deployment 27.
#305) Follow-up Stephan flagged when merging #299: spring-boot-morphium/README.md was missing from release.sh's MODULE_README_FILES, so it would have kept pinning a stale SNAPSHOT version on every future release cycle -- exactly what this array exists to prevent for the other two extension modules. While verifying the fix actually bumps correctly (not just registering the file and assuming it works), found two things the existing sed patterns in bump_module_readme_snapshots() would not have matched even now: - The "In the Morphium reactor, ... currently resolves to `X-SNAPSHOT`." sentence has "resolves to" between "currently" and the version, but the pattern is `currently [0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT` -- no wildcard for words in between. Reworded to "... is currently X-SNAPSHOT." (matching the working phrasing already used elsewhere) so the pattern actually fires. - The prerequisites table's Morphium row still said "6.2.2" -- stale from before this module tracked the reactor version in lockstep, and in a format the "| Morphium | X-SNAPSHOT" pattern doesn't match either. Updated to the same "X-SNAPSHOT (built in lockstep as part of the reactor)" phrasing quarkus-morphium/README.md already uses successfully. Verified end to end: copied the file, ran the exact three sed substitutions bump_module_readme_snapshots() uses with a fake target version, confirmed all four SNAPSHOT-bearing lines updated correctly, restored the original (byte-identical, confirmed via diff) before committing the real fix. Co-authored-by: Heiko Kopp <extern.heiko.kopp1@porsche.de>
There was no automated build/test workflow for PRs -- only doc deployment and wiki sync. Every review so far (including sboesebeck#299/sboesebeck#303-305) ran entirely on manual, ad hoc verification. This adds one: full reactor build/test on every push to develop/master and every PR, using the standard `mvn verify` (no -DskipExtensions, since this is the only place the three optional extension modules -- morphium-jakarta-data, quarkus-morphium, spring-boot-morphium -- get exercised automatically at all). -Dquarkus.http.test-port=0 per your note on sboesebeck#299: quarkus-morphium's integration-tests module binds Quarkus's fixed default test port (8081) in its @QuarkusTest runs. 0 tells Quarkus to pick a free ephemeral port instead, so a GitHub-hosted runner (or any other environment with something else already on 8081) can't collide with it. Harmless for the other modules -- only Quarkus reads the property. Also fixed a stale doc comment in the parent pom.xml's -DskipExtensions explanation: it still said "BOTH extension modules ... morphium-jakarta-data AND quarkus-morphium", predating spring-boot-morphium's addition in sboesebeck#299. Updated to name all three. This PR's first run surfaced 3 messaging test failures that turned out to be a test-base config bug (MultiDriverTestBase carrying inMemorySharedDatabases =false along its InMemoryDriver fallback), not a morphium-core bug and not something wrong with this workflow -- fixed separately in sboesebeck#300 (already on develop) and confirmed green on rebase. See the PR discussion on sboesebeck#307 for the full trail (two-environment reproduction, bisect, then root cause).
There was no automated build/test workflow for PRs -- only doc deployment and wiki sync. Every review so far (including #299/#303-305) ran entirely on manual, ad hoc verification. This adds one: full reactor build/test on every push to develop/master and every PR, using the standard `mvn verify` (no -DskipExtensions, since this is the only place the three optional extension modules -- morphium-jakarta-data, quarkus-morphium, spring-boot-morphium -- get exercised automatically at all). -Dquarkus.http.test-port=0 per your note on #299: quarkus-morphium's integration-tests module binds Quarkus's fixed default test port (8081) in its @QuarkusTest runs. 0 tells Quarkus to pick a free ephemeral port instead, so a GitHub-hosted runner (or any other environment with something else already on 8081) can't collide with it. Harmless for the other modules -- only Quarkus reads the property. Also fixed a stale doc comment in the parent pom.xml's -DskipExtensions explanation: it still said "BOTH extension modules ... morphium-jakarta-data AND quarkus-morphium", predating spring-boot-morphium's addition in #299. Updated to name all three. This PR's first run surfaced 3 messaging test failures that turned out to be a test-base config bug (MultiDriverTestBase carrying inMemorySharedDatabases =false along its InMemoryDriver fallback), not a morphium-core bug and not something wrong with this workflow -- fixed separately in #300 (already on develop) and confirmed green on rebase. See the PR discussion on #307 for the full trail (two-environment reproduction, bisect, then root cause). Co-authored-by: Heiko Kopp <extern.heiko.kopp1@porsche.de>
Hi Stephan,
Third and final building block of the modularization, following #266 and #267:
spring-boot-morphium, a Spring Boot auto-configuration for Morphium with the same Jakarta Data repository support as the Quarkus extension.What this PR does
Three modules (
morphium-spring-boot-autoconfigure,morphium-spring-boot-starter,morphium-spring-boot-test):Morphiumbean built frommorphium.*properties (MorphiumProperties,MorphiumAutoConfiguration)MorphiumRepositoryFactoryBean/MorphiumRepositoryRegistrar/MorphiumRepositoryInvocationHandler) — query derivation (findBy*/countBy*/existsBy*/deleteBy*with And/Or, Between, In, Like, ...), JDQL (@Query("WHERE status = :s ORDER BY name")), and@Find/@Deletewith explicit@Byfield binding@MorphiumTransactionalas a registered auto-configuration (AOP-based commit/rollback viaMorphiumTransactionAspect), not a plain@Component— that distinction mattered, see "Community review" belowMorphiumHealthAutoConfiguration) surfacing connection status under/actuator/health@MorphiumTestcomposite annotation (morphium-spring-boot-test) wiringInMemDriverfor tests with no MongoDB neededMorphiumRepositoryescape hatch fordistinct()/query()/directmorphium()access when the derived-query surface isn't enoughOptionality
git diff --statagainstdevelopshows onlyspring-boot-morphium, doc, and reactor-registration changes —morphium-core,poppydb,morphium-jakarta-data, andquarkus-morphiumare untouched. Same pattern as #267: thespring-boot-dependenciesBOM import stays inspring-boot-morphium/pom.xml, only the version property (spring-boot.version) lives in the parent, so a Spring Boot upgrade is a one-line change without every core build resolving ~250 Spring-managed coordinates.Community review before this PR
Same process as #266/#267: a review PR against my own fork first (CodeRabbit auto-disabled on the non-default base branch, GitHub Copilot review triggered manually), which found three real bugs, all fixed and covered by regression tests before this PR:
MorphiumTransactionAspectwas a plain@Component, not a registered auto-configuration — meant it silently never activated unless a consuming application happened to component-scan the auto-configure package itself, defeating the point of an auto-configuration starter@By-bound parameters on@Find/@Deletemethods were ignored — the binding info was parsed but never applied when building the queryCompletionStage-returning derived query methods dispatched synchronously on the calling thread instead of the configured async executorVerification
-Pextensions install):BUILD SUCCESSacross all 13 modulesWhat's next
With all three optional modules (
morphium-jakarta-data,quarkus-morphium,spring-boot-morphium) now proposed, M6 (consolidation — CI workflow,release.shcross-module check, doc pass) is the remaining piece of the original modularization plan.Open questions for you
docs/spring-boot.md(MkDocs) enough?previeworstable, same as the Quarkus one?