Repository navigation
[WP1] Starter silently overrides every repository base class (data-loss risk) #173
Copy link
Copy link
Closed as not planned
Closed as not planned
Copy link
Labels
status: pending-design-work動工前需要更多的設計或研究動工前需要更多的設計或研究type: bugSomething isn't workingSomething isn't working
Description
此內容由 AI 產生(specification-mapper 全庫審查 2026-07-16)。
Work package WP1 · findings: COR-01 · worst severity: high
From the 2026-07-16 whole-codebase review (branch jakarta, modules mapper/ + starter/). Each finding below is CONFIRMED by independent adversarial verification. File paths and line numbers reflect the review snapshot and may drift — treat them as starting points, not exact coordinates.
COR-01 — repositoryBaseClassCustomizer silently overrides the repository base class of every JPA repository, clobbering @EnableJpaRepositories(repositoryBaseClass=...)
- Category / severity: correctness / high · effort M · confidence high
- Location:
starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java:131 - Evidence: repositoryBaseClassCustomizer calls factory.setRepositoryBaseClass(properties.getRepositoryBaseClass()) unconditionally, and JpaRepositoryFactoryBeanPostProcessor injects it into every JpaRepositoryFactoryBean in the context. In spring-data-commons 3.5.9, RepositoryFactoryBeanSupport.afterPropertiesSet applies the user's repositoryBaseClass (bytecode offsets 127-144: repositoryBaseClass.ifPresent(factory::setRepositoryBaseClass)) BEFORE running repositoryFactoryCustomizers (offsets 147-157), so the starter's default DefaultQueryBySpecExecutor always wins over a base class the user configured via @EnableJpaRepositories, with no log above DEBUG and no error.
- Failure scenario: An application uses @EnableJpaRepositories(repositoryBaseClass = SoftDeleteRepositoryImpl.class) where SoftDeleteRepositoryImpl overrides delete() to set a deleted flag. Adding specification-mapper-starter silently swaps the base class of ALL repositories to DefaultQueryBySpecExecutor (SimpleJpaRepository); delete() becomes a hard DELETE and production data is physically removed. Alternatively, custom base-class methods declared on repository interfaces fail at startup with QueryCreationException.
- Suggested fix: Do not apply the base-class customizer when the user has not opted in: make spec.mapper.repository-base-class have no default and only register the customizer when the property is set, or detect a pre-configured non-default base class and skip (log at WARN when overriding). At minimum document the clobbering behavior prominently.
- Verification: [CONFIRMED] Traced the full path. (1) SpecMapperAutoConfiguration.java:123-132: repositoryBaseClassCustomizer unconditionally calls factory.setRepositoryBaseClass(properties.getRepositoryBaseClass()); SpecMapperProperties.java:48 defaults the property to DefaultQueryBySpecExecutor.class, and the bean carries no opt-in condition beyond starter-wide spec.mapper.enabled (default true). (2) JpaRepositoryFactoryBeanPostProcessor.java:43-48 injects the customizer into every JpaRepositoryFactoryBean in the context. (3) Verified the ordering in the actual runtime version — Boot 3.5.15 → spring-data-bom 2025.0.12 → spring-data-commons 3.5.12 (finding cited 3.5.9; identical): disassembled RepositoryFa…
Full report: see the codebase-review-2026-07-16 branch (linked on the parent tracking issue).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
status: pending-design-work動工前需要更多的設計或研究動工前需要更多的設計或研究type: bugSomething isn't workingSomething isn't working
Activity
shihyuho commentedon Jul 16, 2026
Triage Notes - needs human
Category: bug (correctness / data-loss risk)
Summary: The starter's repository-base-class customizer unconditionally replaces the base class of every JPA repository, clobbering any base class the application configured via
@EnableJpaRepositories(repositoryBaseClass = ...).Current behavior:
SpecMapperProperties.getRepositoryBaseClass()defaults toDefaultQueryBySpecExecutor.class.SpecMapperAutoConfiguration.RepositoryFactoryCustomizerAutoConfiguration.repositoryBaseClassCustomizer(...)returns aRepositoryFactoryCustomizerthat callsfactory.setRepositoryBaseClass(properties.getRepositoryBaseClass())unconditionally. Its only gate is the starter-widespec.mapper.enabledproperty (defaulttrue); there is no opt-in for the base-class override and no detection of a user-configured base class.JpaRepositoryFactoryBeanPostProcessor.postProcessBeforeInitialization(...)injects this customizer into everyJpaRepositoryFactoryBeanin the context.RepositoryFactoryBeanSupport.afterPropertiesSet()(spring-data-commons, verified against the 3.5.x line in the runtime version), the user'srepositoryBaseClass.ifPresent(factory::setRepositoryBaseClass)is applied first, thenrepositoryFactoryCustomizers.forEach(...)runs — so the starter's customizer overwrites the user's base class withDefaultQueryBySpecExecutor(aSimpleJpaRepository). This happens with no logging above DEBUG and no error.Failure scenario: An application configures
@EnableJpaRepositories(repositoryBaseClass = SoftDeleteRepositoryImpl.class)whereSoftDeleteRepositoryImploverridesdelete()to set a soft-delete flag. Addingspecification-mapper-startersilently swaps every repository's base class toDefaultQueryBySpecExecutor, sodelete()becomes a hard SQLDELETEand rows are physically removed. Alternatively, if repository interfaces declare methods that only the user's custom base class implements, startup fails withQueryCreationException.Desired behavior (target contract — exact policy is the human decision): Applying
specification-mapper-startermust not silently discard an application-configured repository base class. TheQueryBySpecExecutorcapability the starter provides must be reconciled with a user's own base class rather than overwriting it, OR the override must be made explicit/opt-in with a visible warning. The chosen behavior must be documented.Key interfaces:
tw.com.softleader.data.jpa.spec.starter.autoconfigure.SpecMapperAutoConfiguration.RepositoryFactoryCustomizerAutoConfiguration#repositoryBaseClassCustomizer(ObjectProvider<SpecMapperProperties>)tw.com.softleader.data.jpa.spec.starter.autoconfigure.SpecMapperProperties#getRepositoryBaseClass()(propertyspec.mapper.repository-base-class, defaultDefaultQueryBySpecExecutor.class)tw.com.softleader.data.jpa.spec.starter.repository.support.JpaRepositoryFactoryBeanPostProcessortw.com.softleader.data.jpa.spec.starter.repository.support.DefaultQueryBySpecExecutor(extendsSimpleJpaRepository, implementsQueryBySpecExecutorAdapter)org.springframework.data.repository.core.support.RepositoryFactoryCustomizer/RepositoryFactoryBeanSupport#afterPropertiesSet()Acceptance criteria (once the policy is decided):
@EnableJpaRepositories(repositoryBaseClass = X.class)no longer hasXsilently replaced byDefaultQueryBySpecExecutor— the resolved base class matches the decided policy.queryBySpeccapability behaves as specified by that policy (either still available, or clearly documented as the user's responsibility).QueryBySpecExecutor; (b) user configures a custom base class — the decided policy holds and no silent clobbering occurs.Out of scope:
mapper/module core (SpecMapper, resolvers).specMapperCustomizerproxy post-processor path (injection ofSpecMapperinto the adapter) except where the base-class policy affects it.Needs human because: The fix requires a product/API policy decision that an autonomous agent cannot safely make. Installing
DefaultQueryBySpecExecutoras the default base class is the starter's core mechanism for giving every repository theQueryBySpecExecutormethods; the three candidate remedies each carry a different, human-owned trade-off:spec.mapper.repository-base-classhave no default / register the customizer only when explicitly set — this is a breaking change that removes automaticqueryBySpecsupport for every existing user who relies on it.queryBySpec, changing the starter's contract.QueryBySpecExecutorcapability — a public API shape decision about how the two base classes combine.Choosing which trade-off is acceptable (default on/off, acceptable breaking change, public API shape) is exactly the kind of judgment the ready-for-agent bar excludes. Once a maintainer picks the policy, the remaining implementation and tests are straightforward and this can move to ready-for-agent.
shihyuho commentedon Jul 20, 2026
Superseded by #191 in the execution map #204, which carries the same finding (COR-01). This 2026-07-16 static plan is being retired in favour of that map.