Skip to content

[WP6] Starter auto-configuration robustness (fluent null-spec, BPP early-init, ordering, API parity) #178

Description

@shihyuho

此內容由 AI 產生(specification-mapper 全庫審查 2026-07-16)。

Work package WP6 · findings: COR-13, COR-12, MAINT-02, MAINT-05 · worst severity: medium

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-13 — Fluent findBySpec throws IllegalArgumentException for null or empty criteria, unlike every sibling method

  • Category / severity: correctness / medium · effort S · confidence high
  • Location: starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/QueryBySpecExecutorAdapter.java:116
  • Evidence: QueryBySpecExecutorAdapter.findBySpec(Object, Function) declares the spec parameter @nullable and delegates findBy(mapper.toSpec(spec, domainClass), queryFunction). SpecMapper.toSpec (mapper/src/main/java/tw/com/softleader/data/jpa/spec/SpecMapper.java:104-114) returns null both when rootObject is null and when no field produces a spec (all-empty criteria). SimpleJpaRepository.findBy in Spring Data JPA 3.5.12 (the version resolved by Boot BOM 3.5.15) executes Assert.notNull(spec, "Specification must not be null"), whereas findAll/findOne/count/exists tolerate a null Specification — the project's own QueryBySpecExecutorTest.findByEmptySpec proves empty criteria are a supported use case for the list variant.
  • Failure scenario: A user submits an empty search form; the service calls repository.findBySpec(emptyCriteria, q -> q.sortBy(sort).page(page)) to use projections/paging via the fluent API. toSpec returns null and SimpleJpaRepository.findBy throws IllegalArgumentException("Specification must not be null") — a 500 — while the identical criteria passed to findBySpec(criteria) or findBySpec(criteria, pageable) returns all rows.
  • Suggested fix: In the fluent findBySpec default method, substitute an unrestricted specification when mapping yields null, e.g. var s = mapper.trySpec(spec, domainClass).orElse((root, query, cb) -> null); return findBy(s, queryFunction);, and add a test mirroring findByEmptySpec for the fluent variant.
  • Verification: [CONFIRMED] Traced all links. QueryBySpecExecutorAdapter.java:116 passes mapper.toSpec(spec, domainClass) into findBy(); SpecMapper.java:104-113 returns null for null rootObject and for empty/all-null criteria (specs.isEmpty()). In spring-data-jpa 3.5.x (SimpleJpaRepository, verified from local clone tag 3.5.10), findBy(Specification, queryFunction) and its doFindBy both call Assert.notNull(spec, "Specification must not be null"), so a null spec throws IllegalArgumentException. Sibling methods tolerate null: findAll(spec)/findAll(spec,sort) route through getQuery(@nullable Specification, ...), findOne through getQuery too, count/exists through @nullable getCountQuery/applySpecificationToCrit…

COR-12 — BeanPostProcessor declared as non-static @bean forces early instantiation of all RepositoryFactoryCustomizer beans and their dependencies

  • Category / severity: correctness / medium · effort S · confidence medium
  • Location: starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java:116
  • Evidence: jpaRepositoryFactoryBeanPostProcessor is a non-static @bean method whose parameter List<RepositoryFactoryCustomizer> customizers makes the container instantiate every RepositoryFactoryCustomizer bean — including user-defined ones — during the BeanPostProcessor registration phase, before other BeanPostProcessors are in place. The starter's own two customizers defer their dependencies via ObjectProvider, but user-contributed customizer beans (which SpecMapperAutoConfiguration explicitly supports) receive no such protection, and the non-static method additionally forces early creation of the enclosing configuration class.
  • Failure scenario: A user defines a RepositoryFactoryCustomizer @bean that injects a service annotated @transactional (or wrapped by any AOP aspect / @ConfigurationProperties post-processing). Because the customizer is created while BeanPostProcessors are still being registered, that service bean is instantiated without its proxy — Spring logs 'is not eligible for getting processed by all BeanPostProcessors' — and its transactions/aspects are silently absent at runtime.
  • Suggested fix: Make the @bean method static and inject ObjectProvider<RepositoryFactoryCustomizer> (resolved lazily inside postProcessBeforeInitialization) instead of a List parameter, so customizer beans are created on first repository factory initialization rather than at BPP registration.
  • Verification: [CONFIRMED] Traced both files. JpaRepositoryFactoryBeanPostProcessor (repository/support/JpaRepositoryFactoryBeanPostProcessor.java) implements org.springframework.beans.factory.config.BeanPostProcessor. In SpecMapperAutoConfiguration.java line 116-119, its @bean factory method is non-static and injects List. Spring must instantiate a @Bean-returned BeanPostProcessor during the registerBeanPostProcessors phase; resolving the List parameter eagerly creates every RepositoryFactoryCustomizer bean (and, being non-static, the enclosing config class) at that early point, before all regular BPPs are registered. The starter's own customizers (lines 123, 137) inject only …

MAINT-02 — Auto-configuration lacks @AutoConfiguration/@AutoConfigureAfter; @ConditionalOnBean(JpaRepositoryFactoryBean) correctness rests on alphabetical ordering

  • Category / severity: maintainability / medium · effort S · confidence medium
  • Location: starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java:111
  • Evidence: SpecMapperAutoConfiguration is registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports but is annotated plain @configuration(proxyBeanMethods = false) with no @autoConfiguration or @AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class). The nested RepositoryFactoryCustomizerAutoConfiguration gates on @ConditionalOnBean(JpaRepositoryFactoryBean.class), which per Boot's documentation is only reliable when evaluation order relative to the configuration that registers those beans is guaranteed. Today it works only because 'org.springframework...JpaRepositoriesAutoConfiguration' sorts alphabetically before 'tw.com.softleader...SpecMapperAutoConfiguration'.
  • Failure scenario: An application enables JPA repositories from its own auto-configuration (e.g. a company platform starter whose class name sorts after tw.com.softleader, or bootstrapRepositories deferred mode changing registration timing). The @ConditionalOnBean evaluates before any JpaRepositoryFactoryBean definition exists, the customizers are never registered, and every repository extending QueryBySpecExecutor fails at startup with QueryCreationException ('Could not create query for method findBySpec') — or worse, starts without SpecMapper injection.
  • Suggested fix: Annotate the class with @autoConfiguration(after = JpaRepositoriesAutoConfiguration.class) (spring-boot-autoconfigure is already a dependency) so condition evaluation order is contractual rather than accidental.
  • Verification: [CONFIRMED] Traced SpecMapperAutoConfiguration.java: line 60 confirms plain @configuration(proxyBeanMethods=false) with no @AutoConfiguration/@AutoConfigureAfter; the class is registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (verified as the sole entry), so Spring Boot treats it as an auto-configuration and orders it via AutoConfigurationSorter, whose initial pass is an alphabetical FQCN sort. Line 111 gates the nested RepositoryFactoryCustomizerAutoConfiguration — which registers the JpaRepositoryFactoryBeanPostProcessor (a BeanPostProcessor) that actually injects the RepositoryFactoryCustomizers/SpecMapper — behind @ConditionalOnBean(JpaReposit…

MAINT-05 — QueryBySpecExecutor lacks counterparts for JpaSpecificationExecutor's findAll(spec, countSpec, pageable) and delete(spec)

  • Category / severity: maintainability / low · effort S · confidence high
  • Location: starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/QueryBySpecExecutor.java:44
  • Evidence: JpaSpecificationExecutor in the resolved spring-data-jpa 3.5.12 declares findAll(Specification, Specification countSpec, Pageable) and delete(Specification) (verified via javap). QueryBySpecExecutor exposes BySpec counterparts for findOne/findAll x3/count/exists/findBy but neither a findBySpec(Object spec, Object countSpec, Pageable) nor a deleteBySpec(Object). delete(spec) predates the removed parity guard so its absence may be deliberate, but the countSpec overload postdates it and is plain drift.
  • Failure scenario: A consumer needs a paged spec query with a cheaper count query (the reason the countSpec overload exists) or wants criteria-driven bulk deletion; the BySpec API offers no path, forcing them to call mapper.toSpec manually and cast the repository to JpaSpecificationExecutor, defeating the starter's abstraction and spreading SpecMapper plumbing through application code.
  • Suggested fix: Add default methods findBySpec(Object spec, Object countSpec, Pageable) and (if deletion is intended to be supported) deleteBySpec(Object) to QueryBySpecExecutor/QueryBySpecExecutorAdapter; otherwise record the deliberate exclusions where the parity guard can assert them.
  • Verification: [CONFIRMED] Facts fully verified. javap on the resolved spring-data-jpa 3.5.9 (jakarta branch) shows JpaSpecificationExecutor declares findAll(Specification, Specification countSpec, Pageable) and delete(Specification). QueryBySpecExecutor.java (line 44 area) exposes findOneBySpec, findBySpec x4 (List/Page/Sort/FluentQuery), countBySpec, existsBySpec — with no deleteBySpec and no findBySpec(spec, countSpec, pageable) counterpart. QueryBySpecExecutorAdapter mirrors the same seven and nothing more; no parity-guard test exists (grep of starter/src/test found none). So the described drift is genuine. Downgrading severity to low: QueryBySpecExecutorAdapter already extends JpaSpecificationExecutor…

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.

    Agent Brief

    Category: bug

    Summary: Harden the starter auto-configuration and fluent query API: make the fluent findBySpec tolerate empty/null criteria like its siblings (COR-13), stop the BeanPostProcessor @Bean from force-instantiating all RepositoryFactoryCustomizer beans during BPP registration (COR-12), and make auto-configuration ordering contractual rather than reliant on alphabetical FQCN sorting (MAINT-02).

    Current behavior

    • COR-13 (fluent null-spec). QueryBySpecExecutorAdapter.findBySpec(Object, Function) (the FluentQuery variant) delegates straight to JpaSpecificationExecutor.findBy(mapper.toSpec(spec, domainClass), queryFunction). SpecMapper.toSpec returns null both when the criteria object is null and when no field yields a spec (all-empty criteria). Spring Data JPA's SimpleJpaRepository.findBy (and its internal doFindBy) assert the specification is non-null, so an empty/null criteria object throws IllegalArgumentException("Specification must not be null"). Every sibling method — findBySpec(spec) (List), findBySpec(spec, Sort), findBySpec(spec, Pageable), countBySpec, existsBySpec, findOneBySpec — routes through findAll/count/exists/findOne, which tolerate a null specification and return all rows. The existing test QueryBySpecExecutorTest.findByEmptySpec proves empty criteria are a supported use case for the List variant, but there is no equivalent test for the fluent variant.
    • COR-12 (BPP early-init). In SpecMapperAutoConfiguration.RepositoryFactoryCustomizerAutoConfiguration, the @Bean factory method that produces JpaRepositoryFactoryBeanPostProcessor is a non-static instance method whose parameter is List<RepositoryFactoryCustomizer> customizers. Because the returned bean is itself a BeanPostProcessor, Spring must instantiate it during the registerBeanPostProcessors phase; resolving the List parameter eagerly instantiates every RepositoryFactoryCustomizer bean (including user-contributed ones) at that early point, before all regular BeanPostProcessors are registered. Being non-static, the method also forces early creation of the enclosing configuration class. User-defined customizers that inject AOP-proxied / @Transactional collaborators can therefore receive un-proxied instances.
    • MAINT-02 (ordering). SpecMapperAutoConfiguration is the sole entry in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, so Spring Boot treats it as an auto-configuration, yet it is annotated plain @Configuration(proxyBeanMethods = false) with no @AutoConfiguration and no ordering relative to JpaRepositoriesAutoConfiguration. Its nested RepositoryFactoryCustomizerAutoConfiguration gates on @ConditionalOnBean(JpaRepositoryFactoryBean.class), whose reliability depends on being evaluated after the config that registers those beans. It works today only because the FQCN sorts alphabetically after org.springframework...JpaRepositoriesAutoConfiguration.

    Desired behavior

    • COR-13. The fluent findBySpec(Object, Function) returns results (all rows, subject to the fluent query) for empty/null criteria instead of throwing, matching the behavior and contract of every sibling *BySpec method. When mapping yields no specification, substitute an unrestricted specification (a predicate that adds no restriction) before calling findBy.
    • COR-12. Registering the starter's BeanPostProcessor no longer forces eager instantiation of RepositoryFactoryCustomizer beans or the enclosing configuration class during the BPP registration phase. Customizer beans are resolved lazily — at the point a repository factory is actually post-processed — so user-defined customizers and their AOP-proxied dependencies are created after all BeanPostProcessors are in place.
    • MAINT-02. Auto-configuration ordering is contractual: SpecMapperAutoConfiguration is declared as a proper auto-configuration that is ordered after JpaRepositoriesAutoConfiguration, so the @ConditionalOnBean(JpaRepositoryFactoryBean.class) gate is evaluated after JPA repository factory beans are registered regardless of FQCN alphabetical order.

    Key interfaces

    • tw.com.softleader.data.jpa.spec.starter.repository.support.QueryBySpecExecutorAdapter#findBySpec(Object, Function) — the fluent variant to fix.
    • tw.com.softleader.data.jpa.spec.SpecCodec#trySpec(Object, Class) / SpecMapper.toSpec(...) — mapping that may yield null.
    • org.springframework.data.jpa.repository.JpaSpecificationExecutor#findBy(Specification, Function) — the delegate that rejects null specs.
    • tw.com.softleader.data.jpa.spec.starter.autoconfigure.SpecMapperAutoConfiguration and its nested RepositoryFactoryCustomizerAutoConfiguration.
    • tw.com.softleader.data.jpa.spec.starter.repository.support.JpaRepositoryFactoryBeanPostProcessor — constructor currently takes List<RepositoryFactoryCustomizer>.
    • org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration — the auto-configuration to order after.

    Acceptance criteria

    • Calling the fluent findBySpec(Object, Function) with an empty criteria object (all fields null) returns the fluent result over all rows rather than throwing IllegalArgumentException, and with null criteria behaves identically to the other *BySpec methods given null.
    • A new test mirroring QueryBySpecExecutorTest.findByEmptySpec covers the fluent variant (e.g. findBy(q -> q.all()) or a projection) with empty criteria and asserts all rows are returned.
    • The @Bean method producing JpaRepositoryFactoryBeanPostProcessor no longer eagerly resolves a List<RepositoryFactoryCustomizer>; customizers are supplied lazily (e.g. via ObjectProvider<RepositoryFactoryCustomizer> resolved inside postProcessBeforeInitialization), and the method is declared static so the enclosing configuration class is not force-created during BPP registration.
    • JpaRepositoryFactoryBeanPostProcessor still applies every registered RepositoryFactoryCustomizer to each JpaRepositoryFactoryBean it post-processes (existing customizer application behavior is preserved).
    • SpecMapperAutoConfiguration is declared as a proper auto-configuration ordered after JpaRepositoriesAutoConfiguration (e.g. @AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)), so the nested @ConditionalOnBean(JpaRepositoryFactoryBean.class) gate no longer depends on alphabetical FQCN ordering.
    • The existing starter test suite (repository wiring, SpecMapper injection, findBySpec/countBySpec/existsBySpec behavior, customizer/resolver customization tests) continues to pass unchanged.

    Out of scope

    • MAINT-05 (API parity — findBySpec(spec, countSpec, Pageable) and deleteBySpec(spec)). Deliberately excluded from this agent brief. Adding methods to the public QueryBySpecExecutor interface is a public-API-surface decision, and the issue itself flags that the absence of delete(spec) "may be deliberate." Whether to expose criteria-driven bulk deletion and a cheaper-count paged overload — and their exact signatures — needs a maintainer decision. Track separately.
    • No change to the mapping semantics of SpecMapper.toSpec (it may continue to return null); the fix lives at the adapter/delegation layer.
    • No change to the public method signatures of existing QueryBySpecExecutor methods.
  3. shihyuho commented on Jul 20, 2026

    @shihyuho
    MemberAuthor

    Superseded by #195 in the execution map #204, which carries the same findings (COR-12, COR-13, MAINT-02, MAINT-05). PR #188 attempted this package and was closed unmerged; that work can be picked up again from #195. 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

    status: ready-for-agentFully specified and ready for an autonomous AFK agent to implementtype: bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions