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).

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