Provide JpaRepositoryFragmentsContributor in JPA Repository Factory and Repository Factory Bean.

Closes #3874
This commit is contained in:
Mark Paluch
2025-05-07 17:11:38 +02:00
parent eb4ad0923d
commit 65214fb0b1
13 changed files with 457 additions and 174 deletions

View File

@@ -43,13 +43,10 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.aot.AotContext;
@@ -63,14 +60,10 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.config.ImplementationDetectionConfiguration;
import org.springframework.data.repository.config.ImplementationLookupConfiguration;
import org.springframework.data.repository.config.RepositoryConfiguration;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
import org.springframework.data.util.Streamable;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -105,6 +98,11 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
return "JPA";
}
@Override
public String getRepositoryBaseClassName() {
return SimpleJpaRepository.class.getName();
}
@Override
public String getRepositoryFactoryBeanClassName() {
return JpaRepositoryFactoryBean.class.getName();
@@ -342,123 +340,5 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
return emf != null ? new JpaRepositoryContributor(repositoryContext, emf)
: new JpaRepositoryContributor(repositoryContext);
}
@Nullable
@Override
@SuppressWarnings("NullAway")
protected RepositoryConfiguration<?> getRepositoryMetadata(RegisteredBean bean) {
RepositoryConfiguration<?> configuration = super.getRepositoryMetadata(bean);
if (configuration != null && configuration.getRepositoryBaseClassName().isPresent()) {
return configuration;
}
return new Meh<>(configuration);
}
}
/**
* I'm just a dirty hack so we can refine the {@link #getRepositoryBaseClassName()} method as we cannot instantiate
* the bean safely to extract it form the repository factory in data commons. So we either have a configurable
* {@link RepositoryConfiguration} return from
* {@link RepositoryRegistrationAotProcessor#getRepositoryMetadata(RegisteredBean)} or change the arrangement and
* maybe move the type out of the factoy.
*
* @param <T>
*/
static class Meh<T extends RepositoryConfigurationSource> implements RepositoryConfiguration<T> {
private RepositoryConfiguration<?> configuration;
public Meh(RepositoryConfiguration<?> configuration) {
this.configuration = configuration;
}
@Nullable
@Override
public Object getSource() {
return configuration.getSource();
}
@Override
public T getConfigurationSource() {
return (T) configuration.getConfigurationSource();
}
@Override
public boolean isLazyInit() {
return configuration.isLazyInit();
}
@Override
public boolean isPrimary() {
return configuration.isPrimary();
}
@Override
public Streamable<String> getBasePackages() {
return configuration.getBasePackages();
}
@Override
public Streamable<String> getImplementationBasePackages() {
return configuration.getImplementationBasePackages();
}
@Override
public String getRepositoryInterface() {
return configuration.getRepositoryInterface();
}
@Override
public Optional<Object> getQueryLookupStrategyKey() {
return Optional.ofNullable(configuration.getQueryLookupStrategyKey());
}
@Override
public Optional<String> getNamedQueriesLocation() {
return configuration.getNamedQueriesLocation();
}
@Override
public Optional<String> getRepositoryBaseClassName() {
String name = SimpleJpaRepository.class.getName();
return Optional.of(name);
}
@Override
public String getRepositoryFactoryBeanClassName() {
return configuration.getRepositoryFactoryBeanClassName();
}
@Override
public String getImplementationBeanName() {
return configuration.getImplementationBeanName();
}
@Override
public String getRepositoryBeanName() {
return configuration.getRepositoryBeanName();
}
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return configuration.getExcludeFilters();
}
@Override
public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) {
return configuration.toImplementationDetectionConfiguration(factory);
}
@Override
public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) {
return configuration.toLookupConfiguration(factory);
}
@Nullable
@Override
public String getResourceDescription() {
return configuration.getResourceDescription();
}
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.util.Assert;
public abstract class JpaEntityInformationSupport<T, ID> extends AbstractEntityInformation<T, ID>
implements JpaEntityInformation<T, ID> {
private JpaEntityMetadata<T> metadata;
private final JpaEntityMetadata<T> metadata;
/**
* Creates a new {@link JpaEntityInformationSupport} with the given domain class.

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.data.jpa.repository.support;
import static org.springframework.data.querydsl.QuerydslUtils.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Tuple;
@@ -32,7 +30,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -40,7 +37,6 @@ import org.springframework.data.jpa.repository.query.*;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -79,6 +75,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
private EntityPathResolver entityPathResolver;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
private JpaRepositoryFragmentsContributor fragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT;
private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR;
private JpaQueryMethodFactory queryMethodFactory;
private QueryRewriterProvider queryRewriterProvider;
@@ -159,6 +156,17 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
this.escapeCharacter = escapeCharacter;
}
/**
* Configures the {@link JpaRepositoryFragmentsContributor} to be used. Defaults to
* {@link JpaRepositoryFragmentsContributor#DEFAULT}.
*
* @param fragmentsContributor
* @since 4.0
*/
public void setFragmentsContributor(JpaRepositoryFragmentsContributor fragmentsContributor) {
this.fragmentsContributor = fragmentsContributor;
}
/**
* Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link DefaultJpaQueryMethodFactory}.
*
@@ -259,51 +267,39 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
@Override
@SuppressWarnings("unchecked")
public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
}
@Override
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
return getRepositoryFragments(metadata, entityManager, entityPathResolver, this.crudMethodMetadata);
}
/**
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically,
* adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl.
* <p>
* Can be overridden by subclasses to customize {@link RepositoryFragments}.
* Built-in fragment contribution can be customized by configuring {@link JpaRepositoryFragmentsContributor}.
*
* @param metadata repository metadata.
* @param entityManager the entity manager.
* @param resolver resolver to translate a plain domain class into a {@link EntityPath}.
* @param crudMethodMetadata metadata about the invoked CRUD methods.
* @return
* @return {@link RepositoryFragments} to be added to the repository.
* @since 2.5.1
*/
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, EntityManager entityManager,
EntityPathResolver resolver, CrudMethodMetadata crudMethodMetadata) {
boolean isQueryDslRepository = QUERY_DSL_PRESENT
&& QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
RepositoryFragments fragments = this.fragmentsContributor.contribute(metadata,
getEntityInformation(metadata.getDomainType()), entityManager, resolver);
if (isQueryDslRepository) {
if (metadata.isReactiveRepository()) {
throw new InvalidDataAccessApiUsageException(
"Cannot combine Querydsl and reactive repository support in a single interface");
}
QuerydslJpaPredicateExecutor<?> querydslJpaPredicateExecutor = new QuerydslJpaPredicateExecutor<>(
getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata);
invokeAwareMethods(querydslJpaPredicateExecutor);
return RepositoryFragments
.of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor));
for (RepositoryFragment<?> fragment : fragments) {
fragment.getImplementation().filter(JpaRepositoryConfigurationAware.class::isInstance)
.ifPresent(it -> invokeAwareMethods((JpaRepositoryConfigurationAware) it));
}
return RepositoryFragments.empty();
return fragments;
}
private void invokeAwareMethods(JpaRepositoryConfigurationAware repository) {

View File

@@ -55,9 +55,10 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
private @Nullable BeanFactory beanFactory;
private @Nullable EntityManager entityManager;
private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE;
private JpaRepositoryFragmentsContributor repositoryFragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
private @Nullable JpaQueryMethodFactory queryMethodFactory;
private @Nullable Function<BeanFactory, QueryEnhancerSelector> queryEnhancerSelectorSource;
private @Nullable Function<@Nullable BeanFactory, QueryEnhancerSelector> queryEnhancerSelectorSource;
/**
* Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface.
@@ -100,20 +101,24 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
this.entityPathResolver = resolver.getIfAvailable(() -> SimpleEntityPathResolver.INSTANCE);
}
/**
* Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will
* fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is
* available.
*
* @param resolver may be {@literal null}.
*/
@Autowired
public void setQueryMethodFactory(ObjectProvider<JpaQueryMethodFactory> resolver) { // TODO: nullable insteand of ObjectProvider
@Override
public JpaRepositoryFragmentsContributor getRepositoryFragmentsContributor() {
return repositoryFragmentsContributor;
}
JpaQueryMethodFactory factory = resolver.getIfAvailable();
if (factory != null) {
this.queryMethodFactory = factory;
}
/**
* Configures the {@link JpaRepositoryFragmentsContributor} to contribute built-in fragment functionality to the
* repository.
*
* @param repositoryFragmentsContributor must not be {@literal null}.
* @since 4.0
*/
public void setRepositoryFragmentsContributor(JpaRepositoryFragmentsContributor repositoryFragmentsContributor) {
this.repositoryFragmentsContributor = repositoryFragmentsContributor;
}
public void setEscapeCharacter(char escapeCharacter) {
this.escapeCharacter = EscapeCharacter.of(escapeCharacter);
}
/**
@@ -153,6 +158,23 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
};
}
/**
* Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will
* fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is
* available.
*
* @param resolver may be {@literal null}.
*/
@Autowired
public void setQueryMethodFactory(ObjectProvider<JpaQueryMethodFactory> resolver) { // TODO: nullable insteand of
// ObjectProvider
JpaQueryMethodFactory factory = resolver.getIfAvailable();
if (factory != null) {
this.queryMethodFactory = factory;
}
}
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
@@ -169,6 +191,7 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager);
factory.setEntityPathResolver(entityPathResolver);
factory.setEscapeCharacter(escapeCharacter);
factory.setFragmentsContributor(getRepositoryFragmentsContributor());
if (queryMethodFactory != null) {
factory.setQueryMethodFactory(queryMethodFactory);
@@ -189,8 +212,4 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
super.afterPropertiesSet();
}
public void setEscapeCharacter(char escapeCharacter) {
this.escapeCharacter = EscapeCharacter.of(escapeCharacter);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import jakarta.persistence.EntityManager;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
import org.springframework.util.Assert;
import com.querydsl.core.types.EntityPath;
/**
* JPA-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository.
* <p>
* Implementations must define a no-args constructor.
* <p>
* Contributed fragments may implement the {@link JpaRepositoryConfigurationAware} interface to access configuration
* settings.
*
* @author Mark Paluch
* @since 4.0
*/
public interface JpaRepositoryFragmentsContributor extends RepositoryFragmentsContributor {
JpaRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE;
/**
* Returns a composed {@code JpaRepositoryFragmentsContributor} that first applies this contributor to its inputs, and
* then applies the {@code after} contributor concatenating effectively both results. If evaluation of either
* contributors throws an exception, it is relayed to the caller of the composed contributor.
*
* @param after the contributor to apply after this contributor is applied.
* @return a composed contributor that first applies this contributor and then applies the {@code after} contributor.
*/
default JpaRepositoryFragmentsContributor andThen(JpaRepositoryFragmentsContributor after) {
Assert.notNull(after, "JpaRepositoryFragmentsContributor must not be null");
return new JpaRepositoryFragmentsContributor() {
@Override
public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
JpaEntityInformation<?, ?> entityInformation, EntityManager entityManager, EntityPathResolver resolver) {
return JpaRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, entityManager, resolver)
.append(after.contribute(metadata, entityInformation, entityManager, resolver));
}
@Override
public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
return JpaRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata));
}
};
}
/**
* Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific
* extensions. Typically, adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl.
*
* @param metadata repository metadata.
* @param entityInformation must not be {@literal null}.
* @param entityManager the entity manager.
* @param resolver resolver to translate a plain domain class into a {@link EntityPath}.
* @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository.
*/
RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
JpaEntityInformation<?, ?> entityInformation, EntityManager entityManager, EntityPathResolver resolver);
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import static org.springframework.data.querydsl.QuerydslUtils.*;
import jakarta.persistence.EntityManager;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.RepositoryFragmentsContributor;
/**
* JPA-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements
* {@link QuerydslPredicateExecutor}.
*
* @author Mark Paluch
* @since 4.0
* @see QuerydslJpaPredicateExecutor
*/
enum QuerydslContributor implements JpaRepositoryFragmentsContributor {
INSTANCE;
@Override
public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
JpaEntityInformation<?, ?> entityInformation, EntityManager entityManager, EntityPathResolver resolver) {
if (isQuerydslRepository(metadata)) {
if (metadata.isReactiveRepository()) {
throw new InvalidDataAccessApiUsageException(
"Cannot combine Querydsl and reactive repository support in a single interface");
}
QuerydslJpaPredicateExecutor<?> executor = new QuerydslJpaPredicateExecutor<>(entityInformation, entityManager,
resolver, null);
return RepositoryComposition.RepositoryFragments
.of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor));
}
return RepositoryComposition.RepositoryFragments.empty();
}
@Override
public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
if (isQuerydslRepository(metadata)) {
return RepositoryComposition.RepositoryFragments
.of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslJpaPredicateExecutor.class));
}
return RepositoryComposition.RepositoryFragments.empty();
}
private static boolean isQuerydslRepository(RepositoryMetadata metadata) {
return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface());
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedFiles;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.data.aot.AotContext;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.config.InfrastructureConfig;
import org.springframework.mock.env.MockPropertySource;
/**
* Integration tests for AOT processing.
*
* @author Mark Paluch
*/
class AotContributionIntegrationTests {
@EnableJpaRepositories(considerNestedRepositories = true, includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) })
static class AotConfiguration extends InfrastructureConfig {
}
@Test // GH-3830
void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException {
TestGenerationContext generationContext = generate(AotConfiguration.class);
InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE,
QuerydslUserRepository.class.getName().replace('.', '/') + ".json");
InputStreamResource isr = new InputStreamResource(metadata);
String json = isr.getContentAsString(StandardCharsets.UTF_8);
assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject()
.containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor")
.containsEntry("fragment", "org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor");
assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject()
.containsEntry("fragment", "org.springframework.data.jpa.repository.support.SimpleJpaRepository");
}
private static TestGenerationContext generate(Class<?>... configurationClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvironment().getPropertySources()
.addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true"));
context.register(configurationClasses);
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(context, generationContext);
return generationContext;
}
}

View File

@@ -61,7 +61,7 @@ class JpaRepositoryMetadataIntegrationTests {
assertThatJson(json).isObject() //
.containsEntry("name", UserRepository.class.getName()) //
.containsEntry("module", "") // TODO: JPA should be here
.containsEntry("module", "JPA") //
.containsEntry("type", "IMPERATIVE");
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot;
import java.util.List;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
interface QuerydslUserRepository extends CrudRepository<User, Integer>, QuerydslPredicateExecutor<User> {
List<User> findUserNoArgumentsBy();
}

View File

@@ -80,6 +80,11 @@ public class TestJpaAotRepositoryContext<T> implements AotRepositoryContext {
return "dummyRepository";
}
@Override
public String getModuleName() {
return "JPA";
}
@Override
public Set<String> getBasePackages() {
return Set.of("org.springframework.data.dummy.repository.aot");

View File

@@ -88,6 +88,11 @@ class JpaRepositoryRegistrationAotProcessorUnitTests {
return "jpaRepository";
}
@Override
public String getModuleName() {
return "JPA";
}
@Override
public Set<String> getBasePackages() {
return Collections.singleton(this.getClass().getPackageName());

View File

@@ -15,13 +15,15 @@
*/
package org.springframework.data.jpa.repository.support;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.metamodel.IdentifiableType;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import java.io.IOException;
@@ -35,6 +37,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.aop.framework.Advised;
import org.springframework.core.OverridingClassLoader;
import org.springframework.data.jpa.domain.sample.User;
@@ -62,6 +65,7 @@ class JpaRepositoryFactoryUnitTests {
private JpaRepositoryFactory factory;
@Mock EntityManager entityManager;
@Mock PersistenceUnitUtil persistenceUnitUtil;
@Mock Metamodel metamodel;
@Mock
@SuppressWarnings("rawtypes") JpaEntityInformation entityInformation;
@@ -74,6 +78,7 @@ class JpaRepositoryFactoryUnitTests {
when(entityManager.getEntityManagerFactory()).thenReturn(emf);
when(entityManager.getDelegate()).thenReturn(entityManager);
when(emf.createEntityManager()).thenReturn(entityManager);
when(emf.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil);
// Setup standard factory configuration
factory = new JpaRepositoryFactory(entityManager) {
@@ -140,6 +145,9 @@ class JpaRepositoryFactoryUnitTests {
@Test
void createsProxyWithCustomBaseClass() {
when(metamodel.managedType(any()))
.thenReturn(mock(ManagedType.class, withSettings().extraInterfaces(IdentifiableType.class)));
JpaRepositoryFactory factory = new CustomGenericJpaRepositoryFactory(entityManager);
factory.setQueryLookupStrategyKey(Key.CREATE_IF_NOT_FOUND);
UserCustomExtendedRepository repository = factory.getRepository(UserCustomExtendedRepository.class);

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import jakarta.persistence.EntityManager;
import java.util.Iterator;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.domain.sample.QCustomer;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import com.querydsl.core.types.EntityPath;
/**
* Unit tests for {@link JpaRepositoryFragmentsContributor}.
*
* @author Mark Paluch
*/
class JpaRepositoryFragmentsContributorUnitTests {
@Test // GH-3279
void composedContributorShouldCreateFragments() {
JpaRepositoryFragmentsContributor contributor = JpaRepositoryFragmentsContributor.DEFAULT
.andThen(MyJpaRepositoryFragmentsContributor.INSTANCE);
EntityPathResolver entityPathResolver = mock(EntityPathResolver.class);
when(entityPathResolver.createPath(any())).thenReturn((EntityPath) QCustomer.customer);
EntityManager entityManager = mock(EntityManager.class);
when(entityManager.getDelegate()).thenReturn(entityManager);
RepositoryComposition.RepositoryFragments fragments = contributor.contribute(
AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class),
new JpaEntityInformationSupportUnitTests.DummyJpaEntityInformation<>(QuerydslUserRepository.class),
entityManager, entityPathResolver);
assertThat(fragments).hasSize(2);
Iterator<RepositoryFragment<?>> iterator = fragments.iterator();
RepositoryFragment<?> querydsl = iterator.next();
assertThat(querydsl.getImplementationClass()).contains(QuerydslJpaPredicateExecutor.class);
RepositoryFragment<?> additional = iterator.next();
assertThat(additional.getImplementationClass()).contains(MyFragment.class);
}
enum MyJpaRepositoryFragmentsContributor implements JpaRepositoryFragmentsContributor {
INSTANCE;
@Override
public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata,
JpaEntityInformation<?, ?> entityInformation, EntityManager entityManager, EntityPathResolver resolver) {
return RepositoryComposition.RepositoryFragments.just(new MyFragment());
}
@Override
public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) {
return RepositoryComposition.RepositoryFragments.just(new MyFragment());
}
}
static class MyFragment {
}
interface QuerydslUserRepository extends Repository<User, Long>, QuerydslPredicateExecutor<User> {}
}