Skip to content

[WP1] Starter silently overrides every repository base class (data-loss risk) #173

Closed as not planned

Description

@shihyuho

此內容由 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).

Activity

  1. added theissue type on Jul 16, 2026
  2. shihyuho commented on Jul 16, 2026

    @shihyuho
    MemberAuthor

    This was generated by AI during triage.

    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 to DefaultQueryBySpecExecutor.class.
    • SpecMapperAutoConfiguration.RepositoryFactoryCustomizerAutoConfiguration.repositoryBaseClassCustomizer(...) returns a RepositoryFactoryCustomizer that calls factory.setRepositoryBaseClass(properties.getRepositoryBaseClass()) unconditionally. Its only gate is the starter-wide spec.mapper.enabled property (default true); 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 every JpaRepositoryFactoryBean in the context.
    • In RepositoryFactoryBeanSupport.afterPropertiesSet() (spring-data-commons, verified against the 3.5.x line in the runtime version), the user's repositoryBaseClass.ifPresent(factory::setRepositoryBaseClass) is applied first, then repositoryFactoryCustomizers.forEach(...) runs — so the starter's customizer overwrites the user's base class with DefaultQueryBySpecExecutor (a SimpleJpaRepository). This happens with no logging above DEBUG and no error.

    Failure scenario: An application configures @EnableJpaRepositories(repositoryBaseClass = SoftDeleteRepositoryImpl.class) where SoftDeleteRepositoryImpl overrides delete() to set a soft-delete flag. Adding specification-mapper-starter silently swaps every repository's base class to DefaultQueryBySpecExecutor, so delete() becomes a hard SQL DELETE and rows are physically removed. Alternatively, if repository interfaces declare methods that only the user's custom base class implements, startup fails with QueryCreationException.

    Desired behavior (target contract — exact policy is the human decision): Applying specification-mapper-starter must not silently discard an application-configured repository base class. The QueryBySpecExecutor capability 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() (property spec.mapper.repository-base-class, default DefaultQueryBySpecExecutor.class)
    • tw.com.softleader.data.jpa.spec.starter.repository.support.JpaRepositoryFactoryBeanPostProcessor
    • tw.com.softleader.data.jpa.spec.starter.repository.support.DefaultQueryBySpecExecutor (extends SimpleJpaRepository, implements QueryBySpecExecutorAdapter)
    • org.springframework.data.repository.core.support.RepositoryFactoryCustomizer / RepositoryFactoryBeanSupport#afterPropertiesSet()

    Acceptance criteria (once the policy is decided):

    • An application that sets @EnableJpaRepositories(repositoryBaseClass = X.class) no longer has X silently replaced by DefaultQueryBySpecExecutor — the resolved base class matches the decided policy.
    • Whatever reconciliation policy is chosen, the queryBySpec capability behaves as specified by that policy (either still available, or clearly documented as the user's responsibility).
    • Any override of a user-configured base class is surfaced at WARN (not DEBUG), or is gated behind explicit opt-in.
    • The behavior is documented (starter docs / property Javadoc).
    • Integration tests cover: (a) no user base class configured — starter still installs QueryBySpecExecutor; (b) user configures a custom base class — the decided policy holds and no silent clobbering occurs.

    Out of scope:

    • Changes to the mapper/ module core (SpecMapper, resolvers).
    • The specMapperCustomizer proxy post-processor path (injection of SpecMapper into 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 DefaultQueryBySpecExecutor as the default base class is the starter's core mechanism for giving every repository the QueryBySpecExecutor methods; the three candidate remedies each carry a different, human-owned trade-off:

    1. Make spec.mapper.repository-base-class have no default / register the customizer only when explicitly set — this is a breaking change that removes automatic queryBySpec support for every existing user who relies on it.
    2. Detect a user-configured non-default base class and skip the override (WARN) — repositories using that base class then lose queryBySpec, changing the starter's contract.
    3. Require/compose the user's base class with QueryBySpecExecutor capability — 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.
  3. shihyuho commented on Jul 20, 2026

    @shihyuho
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions