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()); + } +}