From dfdf4c7f2527e15a49a35edb3d21863d3d348959 Mon Sep 17 00:00:00 2001 From: Shihyu Ho Date: Thu, 16 Jul 2026 14:15:48 +0800 Subject: [PATCH] fix(starter): harden auto-configuration and fluent findBySpec robustness COR-13: fluent findBySpec(Object, Function) now substitutes an unrestricted specification when mapping yields null, so empty/null criteria return all rows (via the fluent query) instead of throwing IllegalArgumentException, matching every sibling *BySpec method. COR-12: the JpaRepositoryFactoryBeanPostProcessor @Bean is now static and injects ObjectProvider, resolved lazily inside postProcessBeforeInitialization, so registering the BeanPostProcessor no longer force-instantiates customizer beans (or the enclosing config) during BPP registration. MAINT-02: SpecMapperAutoConfiguration is now @AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class), making the nested @ConditionalOnBean(JpaRepositoryFactoryBean) gate ordering contractual rather than dependent on alphabetical FQCN sorting. Refs #178 Co-authored-by: Claude Opus 4.8 (1M context) --- .../autoconfigure/SpecMapperAutoConfiguration.java | 10 +++++----- .../JpaRepositoryFactoryBeanPostProcessor.java | 6 +++--- .../support/QueryBySpecExecutorAdapter.java | 3 ++- .../starter/repository/QueryBySpecExecutorTest.java | 11 +++++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java index 2f6d568..dd658d9 100644 --- a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java +++ b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/autoconfigure/SpecMapperAutoConfiguration.java @@ -27,19 +27,19 @@ import static tw.com.softleader.data.jpa.spec.ASTWriterFactory.impersonation; import static tw.com.softleader.data.jpa.spec.starter.autoconfigure.SpecMapperProperties.PREFIX_SPEC_MAPPER; -import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; @@ -57,7 +57,7 @@ */ @Slf4j @RequiredArgsConstructor -@Configuration(proxyBeanMethods = false) +@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class) @EnableConfigurationProperties(SpecMapperProperties.class) @ConditionalOnProperty(prefix = PREFIX_SPEC_MAPPER, value = "enabled", matchIfMissing = true) public class SpecMapperAutoConfiguration { @@ -113,8 +113,8 @@ static class RepositoryFactoryCustomizerAutoConfiguration { @Bean @ConditionalOnMissingBean - JpaRepositoryFactoryBeanPostProcessor jpaRepositoryFactoryBeanPostProcessor( - List customizers) { + static JpaRepositoryFactoryBeanPostProcessor jpaRepositoryFactoryBeanPostProcessor( + ObjectProvider customizers) { return new JpaRepositoryFactoryBeanPostProcessor(customizers); } diff --git a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/JpaRepositoryFactoryBeanPostProcessor.java b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/JpaRepositoryFactoryBeanPostProcessor.java index 23ec714..cf20e1c 100644 --- a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/JpaRepositoryFactoryBeanPostProcessor.java +++ b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/JpaRepositoryFactoryBeanPostProcessor.java @@ -20,10 +20,10 @@ */ package tw.com.softleader.data.jpa.spec.starter.repository.support; -import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; @@ -37,13 +37,13 @@ @RequiredArgsConstructor public class JpaRepositoryFactoryBeanPostProcessor implements BeanPostProcessor { - final List customizers; + final ObjectProvider customizers; @Override public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { if (bean instanceof JpaRepositoryFactoryBean factoryBean) { - customizers.forEach(factoryBean::addRepositoryFactoryCustomizer); + customizers.orderedStream().forEach(factoryBean::addRepositoryFactoryCustomizer); } return bean; } diff --git a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/QueryBySpecExecutorAdapter.java b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/QueryBySpecExecutorAdapter.java index f36c389..5dd741b 100644 --- a/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/QueryBySpecExecutorAdapter.java +++ b/starter/src/main/java/tw/com/softleader/data/jpa/spec/starter/repository/support/QueryBySpecExecutorAdapter.java @@ -113,7 +113,8 @@ default R findBySpec( var domainClass = getDomainClass(); notNull(mapper, "getSpecMapper() must not returns null"); notNull(domainClass, "getDomainClass() must not returns null"); - return findBy(mapper.toSpec(spec, domainClass), queryFunction); + var specification = mapper.toSpec(spec, domainClass); + return findBy(specification != null ? specification : (root, query, cb) -> null, queryFunction); } SpecMapper getSpecMapper(); diff --git a/starter/src/test/java/tw/com/softleader/data/jpa/spec/starter/repository/QueryBySpecExecutorTest.java b/starter/src/test/java/tw/com/softleader/data/jpa/spec/starter/repository/QueryBySpecExecutorTest.java index 73cdd6c..f247170 100644 --- a/starter/src/test/java/tw/com/softleader/data/jpa/spec/starter/repository/QueryBySpecExecutorTest.java +++ b/starter/src/test/java/tw/com/softleader/data/jpa/spec/starter/repository/QueryBySpecExecutorTest.java @@ -147,6 +147,17 @@ void findBySpecAndQuery() { assertThat(actual).hasSize(1).contains(matt); } + @Test + void findByEmptySpecAndQuery() { + var matt = repository.save(Customer.builder().name("matt").build()); + var bob = repository.save(Customer.builder().name("bob").build()); + var mary = repository.save(Customer.builder().name("mary").build()); + + var criteria = MyCriteria.builder().build(); + var actual = repository.findBySpec(criteria, FluentQuery.FetchableFluentQuery::all); + assertThat(actual).hasSize(3).contains(matt, bob, mary); + } + @Builder @Data static class MyCriteria {