diff --git a/pom.xml b/pom.xml
index 13bf0377b4..d884acefee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-commons
- 3.1.0-SNAPSHOT
+ 3.1.x-GH-2721-SNAPSHOT
Spring Data Core
Core Spring concepts underpinning every Spring Data module.
diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
index c1a0ae9983..c4cb5642ba 100644
--- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
+++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
@@ -31,6 +31,7 @@
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.ManagedTypes;
+import org.springframework.data.util.QTypeContributor;
import org.springframework.data.util.TypeContributor;
import org.springframework.data.util.TypeUtils;
import org.springframework.lang.Nullable;
@@ -134,9 +135,11 @@ protected void contributeType(ResolvableType type, GenerationContext generationC
Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE);
- TypeContributor.contribute(type.toClass(), annotationNamespaces, generationContext);
+ Class> resolvedType = type.toClass();
+ TypeContributor.contribute(resolvedType, annotationNamespaces, generationContext);
+ QTypeContributor.contributeEntityPath(resolvedType, generationContext, resolvedType.getClassLoader());
- TypeUtils.resolveUsedAnnotations(type.toClass()).forEach(
+ TypeUtils.resolveUsedAnnotations(resolvedType).forEach(
annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext));
}
diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java
index 2c72ddc73b..62f51b2365 100644
--- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java
+++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java
@@ -42,14 +42,15 @@
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.aot.AotContext;
-import org.springframework.data.util.TypeContributor;
-import org.springframework.data.util.TypeUtils;
import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.TargetAware;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.util.QTypeContributor;
+import org.springframework.data.util.TypeContributor;
+import org.springframework.data.util.TypeUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -263,10 +264,12 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge
contribution.getRuntimeHints().reflection()
.registerType(repositoryInformation.getRepositoryInterface(),
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS))
- .registerType(repositoryInformation.getRepositoryBaseClass(),
- hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS));
+ .registerType(repositoryInformation.getRepositoryBaseClass(), hint -> hint
+ .withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS));
TypeContributor.contribute(repositoryInformation.getDomainType(), contribution);
+ QTypeContributor.contributeEntityPath(repositoryInformation.getDomainType(), contribution,
+ repositoryContext.getClassLoader());
// Repository Fragments
for (RepositoryFragment> fragment : getRepositoryInformation().getFragments()) {
diff --git a/src/main/java/org/springframework/data/util/QTypeContributor.java b/src/main/java/org/springframework/data/util/QTypeContributor.java
new file mode 100644
index 0000000000..12e4d9171c
--- /dev/null
+++ b/src/main/java/org/springframework/data/util/QTypeContributor.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022 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.util;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Function;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 4.1
+ */
+public class QTypeContributor {
+
+ private final static Log logger = LogFactory.getLog(QTypeContributor.class);
+ private static Function> entityPathType = cacheOf(QTypeContributor::getEntityPathType);
+
+ public static void contributeEntityPath(Class> type, GenerationContext context, @Nullable ClassLoader classLoader) {
+
+ Class> entityPathType = QTypeContributor.entityPathType.apply(classLoader);
+ if (entityPathType == null) {
+ return;
+ }
+
+ String queryClassName = getQueryClassName(type);
+ if (ClassUtils.isPresent(queryClassName, classLoader)) {
+ try {
+ if (ClassUtils.isAssignable(entityPathType, ClassUtils.forName(queryClassName, classLoader))) {
+
+ logger.debug("Registering Q type %s for %s.");
+ context.getRuntimeHints().reflection().registerType(TypeReference.of(queryClassName),
+ MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS);
+ } else {
+ logger.debug("Skipping Q type %s. Not an EntityPath.");
+ }
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("%s could not be loaded".formatted(queryClassName), e);
+ }
+ }
+ }
+
+ @Nullable
+ private static Class> getEntityPathType(ClassLoader classLoader) {
+
+ if (!ClassUtils.isPresent("com.querydsl.core.types.EntityPath", classLoader)) {
+ return null;
+ }
+
+ try {
+ return ClassUtils.forName("com.querydsl.core.types.EntityPath", classLoader);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Function> cacheOf(Function> inFunction) {
+ Map> cache = new WeakHashMap<>();
+ return in -> cache.computeIfAbsent(in, inFunction::apply);
+ }
+
+ /**
+ * Returns the name of the query class for the given domain class.
+ *
+ * @param domainClass
+ * @return
+ */
+ private static String getQueryClassName(Class> domainClass) {
+
+ String simpleClassName = ClassUtils.getShortName(domainClass);
+ String pkgName = domainClass.getPackage().getName();
+
+ return String.format("%s.Q%s%s", pkgName, getClassBase(simpleClassName), domainClass.getSimpleName());
+ }
+
+ /**
+ * Analyzes the short class name and potentially returns the outer class.
+ *
+ * @param shortName
+ * @return
+ */
+ private static String getClassBase(String shortName) {
+
+ String[] parts = shortName.split("\\.");
+
+ return parts.length < 2 ? "" : parts[0] + "_";
+ }
+}
diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithQuerydslPredicateExecutor.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithQuerydslPredicateExecutor.java
new file mode 100644
index 0000000000..2b2d999252
--- /dev/null
+++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithQuerydslPredicateExecutor.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 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.aot.sample;
+
+import javax.annotation.processing.Generated;
+
+import com.querydsl.core.types.dsl.BeanPath;
+import com.querydsl.core.types.dsl.EntityPathBase;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.MyRepo;
+import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.config.EnableRepositories;
+
+/**
+ * @author Christoph Strobl
+ */
+@EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyRepo.class) },
+ basePackageClasses = ConfigWithQuerydslPredicateExecutor.class, considerNestedRepositories = true)
+public class ConfigWithQuerydslPredicateExecutor {
+
+ public interface MyRepo extends CrudRepository, QuerydslPredicateExecutor {
+
+ }
+
+ public static class Person {
+
+ Address address;
+
+ }
+
+ public static class Address {
+ String street;
+ }
+}
diff --git a/src/test/java/org/springframework/data/aot/sample/QConfigWithQuerydslPredicateExecutor_Person.java b/src/test/java/org/springframework/data/aot/sample/QConfigWithQuerydslPredicateExecutor_Person.java
new file mode 100644
index 0000000000..8cc2d59c7b
--- /dev/null
+++ b/src/test/java/org/springframework/data/aot/sample/QConfigWithQuerydslPredicateExecutor_Person.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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.aot.sample;
+
+import com.querydsl.core.types.dsl.EntityPathBase;
+import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.Person;
+
+public class QConfigWithQuerydslPredicateExecutor_Person extends EntityPathBase {
+
+ public QConfigWithQuerydslPredicateExecutor_Person(Class type, String variable) {
+ super(type, variable);
+ }
+}
diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java
index 89464c71b6..df96f63484 100644
--- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java
+++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java
@@ -34,9 +34,12 @@
import org.springframework.data.aot.sample.ConfigWithFragments;
import org.springframework.data.aot.sample.ConfigWithQueryMethods;
import org.springframework.data.aot.sample.ConfigWithQueryMethods.ProjectionInterface;
+import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor;
+import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.Person;
import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository;
import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresent;
import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository;
+import org.springframework.data.aot.sample.QConfigWithQuerydslPredicateExecutor_Person;
import org.springframework.data.aot.sample.ReactiveConfig;
import org.springframework.data.domain.Page;
import org.springframework.data.repository.PagingAndSortingRepository;
@@ -275,6 +278,20 @@ void doesNotCareAboutNonDataAnnotations() {
});
}
+ @Test // GH-2721
+ void registersQTypeIfPresent() {
+
+ RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(
+ ConfigWithQuerydslPredicateExecutor.class).forRepository(ConfigWithQuerydslPredicateExecutor.MyRepo.class);
+
+ assertThatContribution(repositoryBeanContribution) //
+ .codeContributionSatisfies(contribution -> {
+ contribution.contributesReflectionFor(Person.class);
+ contribution.contributesReflectionFor(
+ QConfigWithQuerydslPredicateExecutor_Person.class);
+ });
+ }
+
RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class> configuration) {
return computeAotConfiguration(configuration, new AnnotationConfigApplicationContext());
}
diff --git a/src/test/java/org/springframework/data/util/QTypeContributorUnitTests.java b/src/test/java/org/springframework/data/util/QTypeContributorUnitTests.java
new file mode 100644
index 0000000000..932865042b
--- /dev/null
+++ b/src/test/java/org/springframework/data/util/QTypeContributorUnitTests.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 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.util;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.aot.generate.ClassNameGenerator;
+import org.springframework.aot.generate.DefaultGenerationContext;
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.generate.InMemoryGeneratedFiles;
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
+import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.Person;
+import org.springframework.data.aot.sample.QConfigWithQuerydslPredicateExecutor_Person;
+import org.springframework.data.classloadersupport.HidingClassLoader;
+import org.springframework.javapoet.ClassName;
+
+import com.querydsl.core.types.EntityPath;
+
+class QTypeContributorUnitTests {
+
+ @Test // GH-2721
+ void addsQTypeHintIfPresent() {
+
+ GenerationContext generationContext = new DefaultGenerationContext(
+ new ClassNameGenerator(ClassName.get(this.getClass())), new InMemoryGeneratedFiles());
+
+ QTypeContributor.contributeEntityPath(Person.class, generationContext, null);
+
+ assertThat(generationContext.getRuntimeHints())
+ .matches(RuntimeHintsPredicates.reflection().onType(QConfigWithQuerydslPredicateExecutor_Person.class));
+ }
+
+ @Test // GH-2721
+ void doesNotAddQTypeHintIfTypeNotPresent() {
+
+ GenerationContext generationContext = new DefaultGenerationContext(
+ new ClassNameGenerator(ClassName.get(this.getClass())), new InMemoryGeneratedFiles());
+
+ QTypeContributor.contributeEntityPath(Person.class, generationContext,
+ HidingClassLoader.hideTypes(QConfigWithQuerydslPredicateExecutor_Person.class));
+
+ assertThat(generationContext.getRuntimeHints()).matches(
+ RuntimeHintsPredicates.reflection().onType(QConfigWithQuerydslPredicateExecutor_Person.class).negate());
+ }
+
+ @Test // GH-2721
+ void doesNotAddQTypeHintIfQuerydslNotPresent() {
+
+ GenerationContext generationContext = new DefaultGenerationContext(
+ new ClassNameGenerator(ClassName.get(this.getClass())), new InMemoryGeneratedFiles());
+
+ QTypeContributor.contributeEntityPath(Person.class, generationContext, HidingClassLoader.hide(EntityPath.class));
+
+ assertThat(generationContext.getRuntimeHints()).matches(
+ RuntimeHintsPredicates.reflection().onType(QConfigWithQuerydslPredicateExecutor_Person.class).negate());
+ }
+}