diff --git a/src/main/java/org/springframework/data/ManagedTypes.java b/src/main/java/org/springframework/data/ManagedTypes.java index 33edb47ba..447d2d458 100644 --- a/src/main/java/org/springframework/data/ManagedTypes.java +++ b/src/main/java/org/springframework/data/ManagedTypes.java @@ -16,49 +16,137 @@ package org.springframework.data; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; /** - * Types managed by a Spring Data implementation. Used to predefine a set of know entities that might need processing - * during container/repository initialization phase. + * Types managed by a Spring Data implementation. + * + * Used to predefine a set of know entities that might need processing during the Spring container, + * Spring Data Repository initialization phase. * * @author Christoph Strobl + * @author John Blum + * @see java.lang.FunctionalInterface * @since 3.0 */ +// TODO: Does this need to be in org.springframework.data? Maybe move to org.springframework.data.domain? +@FunctionalInterface public interface ManagedTypes { - void forEach(Consumer> action); - - default List> toList() { - - List> tmp = new ArrayList<>(100); - forEach(tmp::add); - return tmp; + /** + * Factory method used to construct a new instance of {@link ManagedTypes} containing no {@link Class types}. + * + * @return an empty {@link ManagedTypes} instance. + * @see java.util.Collections#emptySet() + * @see #of(Iterable) + */ + @NonNull + static ManagedTypes empty() { + return of(Collections.emptySet()); } - static ManagedTypes of(Iterable> types) { + /** + * Factory method used to return a {@literal null-safe} instance of {@link ManagedTypes}. + * + * @param types {@link ManagedTypes} to evaluate. + * @return the given {@link ManagedTypes} if not {@literal null} + * or an {@link #empty()} {@link ManagedTypes} instance. + * @see #empty() + */ + @NonNull + static ManagedTypes nullSafeManagedTypes(@Nullable ManagedTypes types) { + return types != null ? types : empty(); + } + + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Iterable} + * of {@link Class types}. + * + * @param types {@link Iterable} of {@link Class types} used to initialize the {@link ManagedTypes}; + * must not be {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Iterable} + * of {@link Class types}. + * @see java.lang.Iterable + * @see #of(Stream) + * @see #of(Supplier) + */ + @NonNull + static ManagedTypes of(@NonNull Iterable> types) { return types::forEach; } - static ManagedTypes of(Stream> types) { + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Stream} + * of {@link Class types}. + * + * @param types {@link Stream} of {@link Class types} used to initialize the {@link ManagedTypes}; + * must not be {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Stream} + * of {@link Class types}. + * @see java.util.stream.Stream + * @see #of(Iterable) + * @see #of(Supplier) + */ + @NonNull + static ManagedTypes of(@NonNull Stream> types) { return types::forEach; } - static ManagedTypes o(Supplier>> dataProvider) { + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Supplier} of + * an {@link Iterable} of {@link Class types}. + * + * @param dataProvider {@link Supplier} of an {@link Iterable} of {@link Class types} used to lazily initialize + * the {@link ManagedTypes}; must not be {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Supplier} of + * an {@link Iterable} of {@link Class types}. + * @see java.util.function.Supplier + * @see java.lang.Iterable + * @see #of(Iterable) + * @see #of(Stream) + */ + @NonNull + static ManagedTypes of(@NonNull Supplier>> dataProvider) { return new ManagedTypes() { - Lazy>> lazyProvider = Lazy.of(dataProvider); + final Lazy>> lazyProvider = Lazy.of(dataProvider); @Override - public void forEach(Consumer> action) { + public void forEach(@NonNull Consumer> action) { lazyProvider.get().forEach(action); } }; } + + /** + * Applies the given {@link Consumer action} to each of the {@link Class types} contained in + * this {@link ManagedTypes} instance. + * + * @param action {@link Consumer} defining the action to perform on the {@link Class types} + * contained in this {@link ManagedTypes} instance; must not be {@literal null}. + * @see java.util.function.Consumer + */ + void forEach(Consumer> action); + + /** + * Returns all the {@link ManagedTypes} in a {@link List}. + * + * @return these {@link ManagedTypes} in a {@link List}; never {@literal null}. + * @see java.util.List + */ + default List> toList() { + + List> list = new ArrayList<>(100); + forEach(list::add); + return list; + } } diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index e94e9f0d8..55fafea07 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -28,30 +28,37 @@ import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** - * The context in which the AOT processing happens. Grants access to the {@link ConfigurableListableBeanFactory - * beanFactory} and {@link ClassLoader}. Holds a few convenience methods to check if a type - * {@link #isTypePresent(String) is present} and allows resolution of them. WARNING: Unstable internal - * API! + * The context in which the AOT processing happens. + * + * Grants access to the {@link ConfigurableListableBeanFactory beanFactory} and {@link ClassLoader}. Holds a few + * convenience methods to check if a type {@link #isTypePresent(String) is present} and allows resolution of them. + * + * WARNING: Unstable internal API! * * @author Christoph Strobl + * @author John Blum + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory + * @since 3.0 */ public interface AotContext { /** * Create an {@link AotContext} backed by the given {@link BeanFactory}. * - * @param beanFactory must not be {@literal null}. - * @return new instance of {@link AotContext}. + * @param beanFactory reference to the {@link BeanFactory}; must not be {@literal null}. + * @return a new instance of {@link AotContext}. + * @see BeanFactory */ - static AotContext context(BeanFactory beanFactory) { + static AotContext from(@NonNull BeanFactory beanFactory) { - Assert.notNull(beanFactory, "BeanFactory must not be null!"); + Assert.notNull(beanFactory, "BeanFactory must not be null"); return new AotContext() { @@ -59,6 +66,7 @@ public interface AotContext { ? (ConfigurableListableBeanFactory) beanFactory : new DefaultListableBeanFactory(beanFactory); + @NonNull @Override public ConfigurableListableBeanFactory getBeanFactory() { return bf; @@ -66,75 +74,203 @@ public interface AotContext { }; } + /** + * Returns a reference to the {@link ConfigurableListableBeanFactory} backing this {@link AotContext}. + * + * @return a reference to the {@link ConfigurableListableBeanFactory} backing this {@link AotContext}. + * @see ConfigurableListableBeanFactory + */ ConfigurableListableBeanFactory getBeanFactory(); + /** + * Returns the {@link ClassLoader} used by this {@link AotContext} to resolve {@link Class types}. + * + * By default, this is the same {@link ClassLoader} used by the {@link BeanFactory} to resolve {@link Class types} + * declared in bean definitions. + * + * @return the {@link ClassLoader} used by this {@link AotContext} to resolve {@link Class types}. + * @see ConfigurableListableBeanFactory#getBeanClassLoader() + */ + @Nullable default ClassLoader getClassLoader() { return getBeanFactory().getBeanClassLoader(); } - default boolean isTypePresent(String typeName) { - return ClassUtils.isPresent(typeName, getBeanFactory().getBeanClassLoader()); + /** + * Determines whether the given {@link String named} {@link Class type} is present on the application classpath. + * + * @param typeName {@link String name} of the {@link Class type} to evaluate; must not be {@literal null}. + * @return {@literal true} if the given {@link String named} {@link Class type} is present + * on the application classpath. + * @see #getClassLoader() + */ + default boolean isTypePresent(@NonNull String typeName) { + return ClassUtils.isPresent(typeName, getClassLoader()); } + /** + * Returns a new {@link TypeScanner} used to scan for {@link Class types} that will be contributed to the AOT + * processing infrastructure. + * + * @return a {@link TypeScanner} used to scan for {@link Class types} that will be contributed to the AOT + * processing infrastructure. + * @see TypeScanner + */ + @NonNull default TypeScanner getTypeScanner() { return new TypeScanner(getClassLoader()); } - default Set> scanPackageForTypes(Collection> identifyingAnnotations, + /** + * Scans for {@link Class types} in the given {@link String named packages} annotated with the store-specific + * {@link Annotation identifying annotations}. + * + * @param identifyingAnnotations {@link Collection} of {@link Annotation Annotations} identifying store-specific + * model {@link Class types}; must not be {@literal null}. + * @param packageNames {@link Collection} of {@link String package names} to scan. + * @return a {@link Set} of {@link Class types} found during the scan. + * @see TypeScanner#scanForTypesAnnotatedWith(Class[]) + * @see TypeScanner.Scanner#inPackages(Collection) + * @see #getTypeScanner() + */ + default Set> scanPackageForTypes(@NonNull Collection> identifyingAnnotations, Collection packageNames) { + return getTypeScanner().scanForTypesAnnotatedWith(identifyingAnnotations).inPackages(packageNames); } - default Optional> resolveType(String typeName) { + /** + * Resolves the required {@link String named} {@link Class type}. + * + * @param typeName {@link String} containing the {@literal fully-qualified class name} of the {@link Class type} + * to resolve; must not be {@literal null}. + * @return a resolved {@link Class type} for the given, required {@link String name}. + * @throws TypeNotPresentException if the {@link String named} {@link Class type} cannot be found. + */ + @NonNull + default Class resolveRequiredType(@NonNull String typeName) throws TypeNotPresentException { - if (!isTypePresent(typeName)) { - return Optional.empty(); - } - return Optional.of(resolveRequiredType(typeName)); - } - - default Class resolveRequiredType(String typeName) throws TypeNotPresentException { try { return ClassUtils.forName(typeName, getClassLoader()); - } catch (ClassNotFoundException e) { - throw new TypeNotPresentException(typeName, e); + } catch (ClassNotFoundException cause) { + throw new TypeNotPresentException(typeName, cause); } } + /** + * Resolves the given {@link String named} {@link Class type} if present. + * + * @param typeName {@link String} containing the {@literal fully-qualified class name} of the {@link Class type} + * to resolve; must not be {@literal null}. + * @return an {@link Optional} value containing the {@link Class type} + * if the {@link String fully-qualified class name} is present on the application classpath. + * @see #isTypePresent(String) + * @see #resolveRequiredType(String) + * @see java.util.Optional + */ + default Optional> resolveType(@NonNull String typeName) { + + return isTypePresent(typeName) + ? Optional.of(resolveRequiredType(typeName)) + : Optional.empty(); + } + + /** + * Resolves the {@link BeanDefinition bean's} defined {@link Class type}. + * + * @param beanReference {@link BeanReference} to the managed bean. + * @return the {@link Class type} of the {@link BeanReference referenced bean} if defined; may be {@literal null}. + * @see BeanReference + */ @Nullable - default Class resolveType(BeanReference beanReference) { + default Class resolveType(@NonNull BeanReference beanReference) { return getBeanFactory().getType(beanReference.getBeanName(), false); } - default BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + /** + * Gets the {@link BeanDefinition} for the given, required {@link String named bean}. + * + * @param beanName {@link String} containing the {@literal name} of the bean; must not be {@literal null}. + * @return the {@link BeanDefinition} for the given, required {@link String named bean}. + * @throws NoSuchBeanDefinitionException if a {@link BeanDefinition} cannot be found for + * the {@link String named bean}. + * @see BeanDefinition + */ + @NonNull + default BeanDefinition getBeanDefinition(@NonNull String beanName) throws NoSuchBeanDefinitionException { return getBeanFactory().getBeanDefinition(beanName); } - default RootBeanDefinition getRootBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + /** + * Gets the {@link RootBeanDefinition} for the given, required {@link String bean name}. + * + * @param beanName {@link String} containing the {@literal name} of the bean. + * @return the {@link RootBeanDefinition} for the given, required {@link String bean name}. + * @throws NoSuchBeanDefinitionException if a {@link BeanDefinition} cannot be found for + * the {@link String named bean}. + * @throws IllegalStateException if the bean is not a {@link RootBeanDefinition root bean}. + * @see RootBeanDefinition + */ + @NonNull + default RootBeanDefinition getRootBeanDefinition(@NonNull String beanName) throws NoSuchBeanDefinitionException { - BeanDefinition val = getBeanFactory().getBeanDefinition(beanName); - if (!(val instanceof RootBeanDefinition)) { - throw new IllegalStateException(String.format("%s is not a root bean", beanName)); + BeanDefinition beanDefinition = getBeanDefinition(beanName); + + if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { + return rootBeanDefinition; } - return RootBeanDefinition.class.cast(val); + + throw new IllegalStateException(String.format("%s is not a root bean", beanName)); } - default boolean isFactoryBean(String beanName) { + /** + * Determines whether a bean identified by the given, required {@link String name} is a + * {@link org.springframework.beans.factory.FactoryBean}. + * + * @param beanName {@link String} containing the {@literal name} of the bean to evaluate; + * must not be {@literal null}. + * @return {@literal true} if the bean identified by the given, required {@link String name} is a + * {@link org.springframework.beans.factory.FactoryBean}. + */ + default boolean isFactoryBean(@NonNull String beanName) { return getBeanFactory().isFactoryBean(beanName); } + /** + * Determines whether a Spring {@link org.springframework.transaction.TransactionManager} is present. + * + * @return {@literal true} if a Spring {@link org.springframework.transaction.TransactionManager} is present. + */ default boolean isTransactionManagerPresent() { - return resolveType("org.springframework.transaction.TransactionManager") // - .map(it -> !ObjectUtils.isEmpty(getBeanFactory().getBeanNamesForType(it))) // - .orElse(false); + return resolveType("org.springframework.transaction.TransactionManager") + .filter(it -> !ObjectUtils.isEmpty(getBeanFactory().getBeanNamesForType(it))) + .isPresent(); } - default void ifTypePresent(String typeName, Consumer> action) { + /** + * Determines whether the given, required {@link String type name} is declared on the application classpath + * and performs the given, required {@link Consumer action} if present. + * + * @param typeName {@link String name} of the {@link Class type} to process; must not be {@literal null}. + * @param action {@link Consumer} defining the action to perform on the resolved {@link Class type}; + * must not be {@literal null}. + * @see java.util.function.Consumer + * @see #resolveType(String) + */ + default void ifTypePresent(@NonNull String typeName, @NonNull Consumer> action) { resolveType(typeName).ifPresent(action); } - default void ifTransactionManagerPresent(Consumer beanNamesConsumer) { + /** + * Runs the given {@link Consumer action} on any {@link org.springframework.transaction.TransactionManager} beans + * defined in the application context. + * + * @param beanNamesConsumer {@link Consumer} defining the action to perform on + * the {@link org.springframework.transaction.TransactionManager} beans if present; must not be {@literal null}. + * @see java.util.function.Consumer + */ + default void ifTransactionManagerPresent(@NonNull Consumer beanNamesConsumer) { ifTypePresent("org.springframework.transaction.TransactionManager", txMgrType -> { String[] txMgrBeanNames = getBeanFactory().getBeanNamesForType(txMgrType); diff --git a/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java b/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java deleted file mode 100644 index e8ec1a266..000000000 --- a/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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; - -import java.lang.annotation.Annotation; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.aot.generator.CodeContribution; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.generator.AotContributingBeanPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.data.repository.config.RepositoryConfigurationExtension; -import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; -import org.springframework.data.repository.config.RepositoryMetadata; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; - -/** - * {@link AotContributingBeanPostProcessor} taking care of data repositories. - *

- * Post processes {@link RepositoryFactoryBeanSupport repository factory beans} to provide generic type information to - * AOT tooling to allow deriving target type from the {@link org.springframework.beans.factory.config.BeanDefinition - * bean definition}. If generic types to not match, due to customization of the factory bean by the user, at least the - * target repository type is provided via the {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE}. - *

- *

- * Via {@link #contribute(AotRepositoryContext, CodeContribution)} stores can provide custom logic for contributing - * additional (eg. reflection) configuration. By default reflection configuration will be added for types reachable from - * the repository declaration and query methods as well as all used {@link Annotation annotations} from the - * {@literal org.springframework.data} namespace. - *

- * The post processor is typically configured via {@link RepositoryConfigurationExtension#getAotPostProcessor()} and - * gets added by the {@link org.springframework.data.repository.config.RepositoryConfigurationDelegate}. - * - * @author Christoph Strobl - * @since 3.0 - */ -public class AotContributingRepositoryBeanPostProcessor implements AotContributingBeanPostProcessor, BeanFactoryAware { - - private static final Log logger = LogFactory.getLog(AotContributingBeanPostProcessor.class); - - private ConfigurableListableBeanFactory beanFactory; - private Map> configMap; - - @Nullable - @Override - public RepositoryBeanContribution contribute(RootBeanDefinition beanDefinition, Class beanType, String beanName) { - - if (ObjectUtils.isEmpty(configMap) || !configMap.containsKey(beanName)) { - return null; - } - - RepositoryMetadata metadata = configMap.get(beanName); - - Set> identifyingAnnotations = Collections.emptySet(); - if (metadata.getConfigurationSource() instanceof RepositoryConfigurationExtensionSupport ces) { - identifyingAnnotations = new LinkedHashSet<>(ces.getIdentifyingAnnotations()); - } - - RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.readRepositoryInformation(metadata, - beanFactory); - - DefaultRepositoryContext ctx = new DefaultRepositoryContext(); - ctx.setAotContext(() -> beanFactory); - ctx.setBeanName(beanName); - ctx.setBasePackages(metadata.getBasePackages().toSet()); - ctx.setRepositoryInformation(repositoryInformation); - ctx.setIdentifyingAnnotations(identifyingAnnotations); - - /* - * Help the AOT processing render the FactoryBean type correctly that is used to tell the outcome of the FB. - * We just need to set the target repo type of the RepositoryFactoryBeanSupport while keeping the actual ID and DomainType set to object. - * If the generics do not match we do not try to resolve and remap them, but rather set the ObjectType attribute. - */ - if (logger.isDebugEnabled()) { - logger.debug(String.format("Enhancing repository factory bean definition %s.", beanName)); - } - - ResolvableType resolvedFactoryBean = ResolvableType.forClass( - ctx.resolveType(metadata.getRepositoryFactoryBeanClassName()).orElse(RepositoryFactoryBeanSupport.class)); - if (resolvedFactoryBean.getGenerics().length == 3) { - beanDefinition.setTargetType(ResolvableType.forClassWithGenerics( - ctx.resolveType(metadata.getRepositoryFactoryBeanClassName()).orElse(RepositoryFactoryBeanSupport.class), - repositoryInformation.getRepositoryInterface(), Object.class, Object.class)); - } else { - beanDefinition.setTargetType(resolvedFactoryBean); - beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, repositoryInformation.getRepositoryInterface()); - } - - return new RepositoryBeanContribution(ctx).setModuleContribution(this::contribute); - } - - protected void contribute(AotRepositoryContext ctx, CodeContribution contribution) { - - ctx.getResolvedTypes() // - .stream() // - .filter(it -> !isJavaOrPrimitiveType(it)) // - .forEach(it -> contributeType(it, contribution)); - - ctx.getResolvedAnnotations().stream() // - .filter(AotContributingRepositoryBeanPostProcessor::isSpringDataManagedAnnotation) // - .map(MergedAnnotation::getType) // - .forEach(it -> contributeType(it, contribution)); - } - - protected static boolean isSpringDataManagedAnnotation(MergedAnnotation annotation) { - - if (isInDataNamespace(annotation.getType())) { - return true; - } - - return annotation.getMetaTypes().stream().anyMatch(AotContributingRepositoryBeanPostProcessor::isInDataNamespace); - } - - private static boolean isInDataNamespace(Class type) { - return type.getPackage().getName().startsWith(TypeContributor.DATA_NAMESPACE); - } - - private static boolean isJavaOrPrimitiveType(Class type) { - if (TypeUtils.type(type).isPartOf("java") || type.isPrimitive() || ClassUtils.isPrimitiveArray(type)) { - return true; - } - return false; - } - - protected void contributeType(Class type, CodeContribution contribution) { - - if (logger.isDebugEnabled()) { - logger.debug(String.format("Contributing type information for %s.", type)); - } - - TypeContributor.contribute(type, it -> true, contribution); - } - - public Predicate> typeFilter() { // like only document ones. - return it -> true; - } - - @Override - public int getOrder() { - return 0; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - - if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { - throw new IllegalArgumentException( - "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); - } - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - - public Map> getConfigMap() { - return configMap; - } - - public void setConfigMap(Map> configMap) { - this.configMap = configMap; - } -} diff --git a/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java b/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java deleted file mode 100644 index 5f1a50622..000000000 --- a/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.BiConsumer; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.aot.generator.CodeContribution; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.generator.AotContributingBeanPostProcessor; -import org.springframework.beans.factory.generator.BeanInstantiationContribution; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.ResolvableType; -import org.springframework.data.ManagedTypes; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; - -/** - * {@link AotContributingBeanPostProcessor} handling {@link #getModulePrefix() prefixed} {@link ManagedTypes} instances. - * This allows to register store specific handling of discovered types. - * - * @author Christoph Strobl - * @since 3.0 - */ -public class AotManagedTypesPostProcessor implements AotContributingBeanPostProcessor, BeanFactoryAware { - - private static final Log logger = LogFactory.getLog(AotManagedTypesPostProcessor.class); - - private BeanFactory beanFactory; - - @Nullable private String modulePrefix; - - @Nullable - @Override - public BeanInstantiationContribution contribute(RootBeanDefinition beanDefinition, Class beanType, - String beanName) { - - if (!ClassUtils.isAssignable(ManagedTypes.class, beanType) || !matchesPrefix(beanName)) { - return null; - } - - return contribute(AotContext.context(beanFactory), beanFactory.getBean(beanName, ManagedTypes.class)); - } - - /** - * Hook to provide a customized flavor of {@link BeanInstantiationContribution}. By overriding this method calls to - * {@link #contributeType(ResolvableType, CodeContribution)} might no longer be issued. - * - * @param aotContext never {@literal null}. - * @param managedTypes never {@literal null}. - * @return new instance of {@link AotManagedTypesPostProcessor} or {@literal null} if nothing to do. - */ - @Nullable - protected BeanInstantiationContribution contribute(AotContext aotContext, ManagedTypes managedTypes) { - return new ManagedTypesContribution(aotContext, managedTypes, this::contributeType); - } - - /** - * Hook to contribute configuration for a given {@literal type}. - * - * @param type never {@literal null}. - * @param contribution never {@literal null}. - */ - protected void contributeType(ResolvableType type, CodeContribution contribution) { - - if (logger.isDebugEnabled()) { - logger.debug(String.format("Contributing type information for %s.", type.getType())); - } - - TypeContributor.contribute(type.toClass(), Collections.singleton(TypeContributor.DATA_NAMESPACE), contribution); - TypeUtils.resolveUsedAnnotations(type.toClass()).forEach(annotation -> TypeContributor - .contribute(annotation.getType(), Collections.singleton(TypeContributor.DATA_NAMESPACE), contribution)); - } - - protected boolean matchesPrefix(String beanName) { - return StringUtils.startsWithIgnoreCase(beanName, getModulePrefix()); - } - - public String getModulePrefix() { - return modulePrefix; - } - - public void setModulePrefix(@Nullable String modulePrefix) { - this.modulePrefix = modulePrefix; - } - - @Override - public int getOrder() { - return 0; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - static class ManagedTypesContribution implements BeanInstantiationContribution { - - private AotContext aotContext; - private ManagedTypes managedTypes; - private BiConsumer contributionAction; - - public ManagedTypesContribution(AotContext aotContext, ManagedTypes managedTypes, - BiConsumer contributionAction) { - - this.aotContext = aotContext; - this.managedTypes = managedTypes; - this.contributionAction = contributionAction; - - } - - @Override - public void applyTo(CodeContribution contribution) { - - List> types = new ArrayList<>(100); - managedTypes.forEach(types::add); - if (types.isEmpty()) { - return; - } - - TypeCollector.inspect(types).forEach(type -> { - contributionAction.accept(type, contribution); - }); - } - - public AotContext getAotContext() { - return aotContext; - } - } -} diff --git a/src/main/java/org/springframework/data/aot/AotRepositoryContext.java b/src/main/java/org/springframework/data/aot/AotRepositoryContext.java index 094ceb662..726949e3a 100644 --- a/src/main/java/org/springframework/data/aot/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/aot/AotRepositoryContext.java @@ -22,37 +22,45 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.repository.core.RepositoryInformation; /** + * {@link AotContext} specific to Spring Data {@link org.springframework.data.repository.Repository} infrastructure. + * * @author Christoph Strobl + * @author John Blum + * @see AotContext + * @since 3.0 */ public interface AotRepositoryContext extends AotContext { /** - * @return the bean name of the repository / factory bean + * @return the {@link String bean name} of the repository / factory bean. */ String getBeanName(); /** - * @return base packages to look for repositories. + * @return a {@link Set} of {@link String base packages} to search for repositories. */ Set getBasePackages(); - /** - * @return metadata about the repository itself - */ - RepositoryInformation getRepositoryInformation(); - /** * @return the {@link Annotation} types used to identify domain types. */ Set> getIdentifyingAnnotations(); /** - * @return all types reachable from the repository. + * @return {@link RepositoryInformation metadata} about the repository itself. + * @see org.springframework.data.repository.core.RepositoryInformation + */ + RepositoryInformation getRepositoryInformation(); + + /** + * @return all {@link MergedAnnotation annotations} reachable from the repository. + * @see org.springframework.core.annotation.MergedAnnotation + */ + Set> getResolvedAnnotations(); + + /** + * @return all {@link Class types} reachable from the repository. */ Set> getResolvedTypes(); - /** - * @return all annotations reachable from the repository. - */ - Set> getResolvedAnnotations(); } diff --git a/src/main/java/org/springframework/data/aot/AotRepositoryInformation.java b/src/main/java/org/springframework/data/aot/AotRepositoryInformation.java index 762ab9362..1ee8f8cca 100644 --- a/src/main/java/org/springframework/data/aot/AotRepositoryInformation.java +++ b/src/main/java/org/springframework/data/aot/AotRepositoryInformation.java @@ -25,12 +25,14 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.lang.Nullable; +import org.springframework.lang.NonNull; /** * {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time. * * @author Christoph Strobl + * @see org.springframework.data.repository.core.RepositoryInformation + * @see org.springframework.data.repository.core.RepositoryMetadata * @since 3.0 */ class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { @@ -45,32 +47,30 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R } @Override - public boolean isCustomMethod(Method method) { - + public boolean isCustomMethod(@NonNull Method method) { // TODO: return false; } @Override - public boolean isBaseClassMethod(Method method) { + public boolean isBaseClassMethod(@NonNull Method method) { // TODO return false; } + @NonNull @Override - public Method getTargetClassMethod(Method method) { - + public Method getTargetClassMethod(@NonNull Method method) { // TODO return method; } /** - * @return + * @return configured repository fragments. * @since 3.0 */ - @Nullable + @NonNull public Set> getFragments() { return new LinkedHashSet<>(fragments.get()); } - } diff --git a/src/main/java/org/springframework/data/aot/DefaultRepositoryContext.java b/src/main/java/org/springframework/data/aot/DefaultAotRepositoryContext.java similarity index 84% rename from src/main/java/org/springframework/data/aot/DefaultRepositoryContext.java rename to src/main/java/org/springframework/data/aot/DefaultAotRepositoryContext.java index 715013897..42ffe8073 100644 --- a/src/main/java/org/springframework/data/aot/DefaultRepositoryContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotRepositoryContext.java @@ -30,53 +30,23 @@ import org.springframework.data.util.Lazy; * Default implementation of {@link AotRepositoryContext} * * @author Christoph Strobl + * @author John Blum + * @see AotRepositoryContext * @since 3.0 */ -class DefaultRepositoryContext implements AotRepositoryContext { +class DefaultAotRepositoryContext implements AotRepositoryContext { private AotContext aotContext; - private String beanName; - private java.util.Set basePackages; + private final Lazy>> resolvedAnnotations = Lazy.of(this::discoverAnnotations); + private final Lazy>> managedTypes = Lazy.of(this::discoverTypes); + private RepositoryInformation repositoryInformation; + + private Set basePackages; private Set> identifyingAnnotations; - private Lazy>> resolvedAnnotations = Lazy.of(this::discoverAnnotations); - private Lazy>> managedTypes = Lazy.of(this::discoverTypes); - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return aotContext.getBeanFactory(); - } - - @Override - public String getBeanName() { - return beanName; - } - - @Override - public Set getBasePackages() { - return basePackages; - } - - @Override - public RepositoryInformation getRepositoryInformation() { - return repositoryInformation; - } - - @Override - public Set> getIdentifyingAnnotations() { - return identifyingAnnotations; - } - - @Override - public Set> getResolvedTypes() { - return managedTypes.get(); - } - - @Override - public Set> getResolvedAnnotations() { - return resolvedAnnotations.get(); - } + private String beanName; public AotContext getAotContext() { return aotContext; @@ -86,28 +56,65 @@ class DefaultRepositoryContext implements AotRepositoryContext { this.aotContext = aotContext; } - public void setBeanName(String beanName) { - this.beanName = beanName; + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return getAotContext().getBeanFactory(); + } + + @Override + public Set getBasePackages() { + return basePackages; } public void setBasePackages(Set basePackages) { this.basePackages = basePackages; } - public void setRepositoryInformation(RepositoryInformation repositoryInformation) { - this.repositoryInformation = repositoryInformation; + @Override + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + @Override + public Set> getIdentifyingAnnotations() { + return identifyingAnnotations; } public void setIdentifyingAnnotations(Set> identifyingAnnotations) { this.identifyingAnnotations = identifyingAnnotations; } + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + public void setRepositoryInformation(RepositoryInformation repositoryInformation) { + this.repositoryInformation = repositoryInformation; + } + + @Override + public Set> getResolvedAnnotations() { + return resolvedAnnotations.get(); + } + + @Override + public Set> getResolvedTypes() { + return managedTypes.get(); + } + protected Set> discoverAnnotations() { - Set> annotations = new LinkedHashSet<>(getResolvedTypes().stream().flatMap(type -> { - return TypeUtils.resolveUsedAnnotations(type).stream(); - }).collect(Collectors.toSet())); + Set> annotations = getResolvedTypes().stream() + .flatMap(type -> TypeUtils.resolveUsedAnnotations(type).stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface())); + return annotations; } @@ -123,7 +130,7 @@ class DefaultRepositoryContext implements AotRepositoryContext { Scanner typeScanner = aotContext.getTypeScanner().scanForTypesAnnotatedWith(getIdentifyingAnnotations()); Set> classes = typeScanner.inPackages(getBasePackages()); - TypeCollector.inspect(classes).list().stream().forEach(types::add); + types.addAll(TypeCollector.inspect(classes).list()); } // context.get diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java new file mode 100644 index 000000000..06d9cada2 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2018 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 + * + * http://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; + +import java.util.List; +import java.util.function.BiConsumer; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.core.ResolvableType; +import org.springframework.data.ManagedTypes; +import org.springframework.lang.NonNull; + +/** + * {@link BeanRegistrationAotContribution} used to contribute a {@link ManagedTypes} registration. + * + * @author John Blum + * @see org.springframework.beans.factory.aot.BeanRegistrationAotContribution + * @since 3.0.0 + */ +public class ManagedTypesRegistrationAotContribution implements BeanRegistrationAotContribution { + + private final AotContext aotContext; + private final ManagedTypes managedTypes; + private final BiConsumer contributionAction; + + public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, + @NonNull BiConsumer contributionAction) { + + this.aotContext = aotContext; + this.managedTypes = managedTypes; + this.contributionAction = contributionAction; + } + + protected AotContext getAotContext() { + return this.aotContext; + } + + @NonNull + protected ManagedTypes getManagedTypes() { + return ManagedTypes.nullSafeManagedTypes(this.managedTypes); + } + + @Override + public void applyTo(@NonNull GenerationContext generationContext, + @NonNull BeanRegistrationCode beanRegistrationCode) { + + List> types = getManagedTypes().toList(); + + if (!types.isEmpty()) { + TypeCollector.inspect(types).forEach(type -> contributionAction.accept(type, generationContext)); + } + } +} diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessor.java new file mode 100644 index 000000000..86aa8f9b2 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessor.java @@ -0,0 +1,131 @@ +/* + * 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; + +import java.util.Collections; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.data.ManagedTypes; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link BeanRegistrationAotProcessor} handling {@link #getModulePrefix() module prefixed} {@link ManagedTypes} + * instances. This AOT processor allows store specific handling of discovered types to be registered. + * + * @author Christoph Strobl + * @author John Blum + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor + * @since 3.0 + */ +public class ManagedTypesRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware { + + private final Log logger = LogFactory.getLog(getClass()); + + private BeanFactory beanFactory; + + @Nullable + private String modulePrefix; + + @Override + public BeanRegistrationAotContribution processAheadOfTime(@NonNull RegisteredBean registeredBean) { + return contribute(registeredBean.getMergedBeanDefinition(), registeredBean.getBeanClass(), registeredBean.getBeanName()); + } + + @Nullable + @SuppressWarnings("unused") + protected BeanRegistrationAotContribution contribute(@NonNull RootBeanDefinition beanDefinition, + @NonNull Class beanType, @NonNull String beanName) { + + return isMatch(beanType, beanName) + ? contribute(AotContext.from(beanFactory), beanFactory.getBean(beanName, ManagedTypes.class)) + : null; + } + + protected boolean isMatch(@Nullable Class beanType, @Nullable String beanName) { + return matchesByType(beanType) && matchesPrefix(beanName); + } + + protected boolean matchesByType(@Nullable Class beanType) { + return beanType != null && ClassUtils.isAssignable(ManagedTypes.class, beanType); + } + + protected boolean matchesPrefix(@Nullable String beanName) { + return StringUtils.startsWithIgnoreCase(beanName, getModulePrefix()); + } + + /** + * Hook to provide a customized flavor of {@link BeanRegistrationAotContribution}. By overriding this method + * calls to {@link #contributeType(ResolvableType, GenerationContext)} might no longer be issued. + * + * @param aotContext never {@literal null}. + * @param managedTypes never {@literal null}. + * @return new instance of {@link ManagedTypesRegistrationAotProcessor} or {@literal null} if nothing to do. + */ + @Nullable + protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes) { + return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, this::contributeType); + } + + /** + * Hook to contribute configuration for a given {@literal type}. + * + * @param type never {@literal null}. + * @param generationContext never {@literal null}. + */ + protected void contributeType(ResolvableType type, GenerationContext generationContext) { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Contributing type information for [%s]", type.getType())); + } + + Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); + + TypeContributor.contribute(type.toClass(), annotationNamespaces, generationContext); + + TypeUtils.resolveUsedAnnotations(type.toClass()).forEach(annotation -> + TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); + } + + @Override + public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + public void setModulePrefix(@Nullable String modulePrefix) { + this.modulePrefix = modulePrefix; + } + + @Nullable + public String getModulePrefix() { + return this.modulePrefix; + } +} diff --git a/src/main/java/org/springframework/data/aot/Predicates.java b/src/main/java/org/springframework/data/aot/Predicates.java new file mode 100644 index 000000000..b3e737c4b --- /dev/null +++ b/src/main/java/org/springframework/data/aot/Predicates.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2018 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 + * + * http://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; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Predicate; + +/** + * Abstract utility class containing common, reusable {@link Predicate Predicates}. + * + * @author John Blum + * @see java.util.function.Predicate + * @since 3.0 + */ +// TODO: Consider moving to the org.springframework.data.util package. +@SuppressWarnings("unused") +public abstract class Predicates { + + public static final Predicate IS_ENUM_MEMBER = member -> member.getDeclaringClass().isEnum(); + public static final Predicate IS_HIBERNATE_MEMBER = member -> member.getName().startsWith("$$_hibernate"); + public static final Predicate IS_OBJECT_MEMBER = member -> Object.class.equals(member.getDeclaringClass()); + public static final Predicate IS_JAVA = member -> member.getDeclaringClass().getPackageName().startsWith("java."); + public static final Predicate IS_NATIVE = member -> Modifier.isNative(member.getModifiers()); + public static final Predicate IS_PRIVATE = member -> Modifier.isPrivate(member.getModifiers()); + public static final Predicate IS_PROTECTED = member -> Modifier.isProtected(member.getModifiers()); + public static final Predicate IS_PUBLIC = member -> Modifier.isPublic(member.getModifiers()); + public static final Predicate IS_SYNTHETIC = Member::isSynthetic; + + public static final Predicate IS_BRIDGE_METHOD = Method::isBridge; + + +} diff --git a/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java b/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java deleted file mode 100644 index d397f0bcb..000000000 --- a/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.Optional; -import java.util.function.BiConsumer; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.aop.SpringProxy; -import org.springframework.aop.framework.Advised; -import org.springframework.aot.generator.CodeContribution; -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.TypeReference; -import org.springframework.beans.factory.generator.BeanInstantiationContribution; -import org.springframework.core.DecoratingProxy; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate; -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.RepositoryFragment; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import org.springframework.util.ClassUtils; - -/** - * The {@link BeanInstantiationContribution} for a specific data repository. - * - * @author Christoph Strobl - * @since 3.0 - */ -public class RepositoryBeanContribution implements BeanInstantiationContribution { - - private static final Log logger = LogFactory.getLog(RepositoryBeanContribution.class); - - private final AotRepositoryContext context; - private final RepositoryInformation repositoryInformation; - private BiConsumer moduleContribution; - - public RepositoryBeanContribution(AotRepositoryContext context) { - - this.context = context; - this.repositoryInformation = context.getRepositoryInformation(); - } - - @Override - public void applyTo(CodeContribution contribution) { - - writeRepositoryInfo(contribution); - - if (moduleContribution != null) { - moduleContribution.accept(context, contribution); - } - } - - private void writeRepositoryInfo(CodeContribution contribution) { - - if (logger.isDebugEnabled()) { - logger.debug(String.format("Contributing data repository information for %s.", - repositoryInformation.getRepositoryInterface())); - } - - // TODO: is this the way? - contribution.runtimeHints().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.getDomainType(), hint -> { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); - }); - - // fragments - for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { - - contribution.runtimeHints().reflection() // - .registerType(fragment.getSignatureContributor(), hint -> { - - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - if (!fragment.getSignatureContributor().isInterface()) { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - }); - } - - // the surrounding proxy - contribution.runtimeHints().proxies() // repository proxy - .registerJdkProxy(repositoryInformation.getRepositoryInterface(), SpringProxy.class, Advised.class, - DecoratingProxy.class); - - context.ifTransactionManagerPresent(txMgrBeanNames -> { - - contribution.runtimeHints().proxies() // transactional proxy - .registerJdkProxy(transactionalRepositoryProxy()); - - if (AnnotationUtils.findAnnotation(repositoryInformation.getRepositoryInterface(), Component.class) != null) { - - TypeReference[] source = transactionalRepositoryProxy(); - TypeReference[] txProxyForSerializableComponent = Arrays.copyOf(source, source.length + 1); - txProxyForSerializableComponent[source.length] = TypeReference.of(Serializable.class); - contribution.runtimeHints().proxies().registerJdkProxy(txProxyForSerializableComponent); - } - }); - - // reactive repo - if (repositoryInformation.isReactiveRepository()) { - // TODO: do we still need this and how to configure it? - // registry.initialization().add(NativeInitializationEntry.ofBuildTimeType(configuration.getRepositoryInterface())); - } - - // Kotlin - Optional> coroutineRepo = context - .resolveType("org.springframework.data.repository.kotlin.CoroutineCrudRepository"); - if (coroutineRepo.isPresent() - && ClassUtils.isAssignable(coroutineRepo.get(), repositoryInformation.getRepositoryInterface())) { - - contribution.runtimeHints().reflection() // - .registerTypes( - Arrays.asList(TypeReference.of("org.springframework.data.repository.kotlin.CoroutineCrudRepository"), - TypeReference.of(Repository.class), TypeReference.of(Iterable.class), - TypeReference.of("kotlinx.coroutines.flow.Flow"), TypeReference.of("kotlin.collections.Iterable"), - TypeReference.of("kotlin.Unit"), TypeReference.of("kotlin.Long"), TypeReference.of("kotlin.Boolean")), - hint -> { - hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS); - }); - } - - // repository methods - repositoryInformation.getQueryMethods().map(repositoryInformation::getReturnedDomainClass) - .filter(Class::isInterface).forEach(type -> { - if (ProjectionPredicate.typeHierarchy().test(type, repositoryInformation.getDomainType())) { - contributeProjection(type, contribution); - } - }); - } - - private TypeReference[] transactionalRepositoryProxy() { - - return new TypeReference[] { TypeReference.of(repositoryInformation.getRepositoryInterface()), - TypeReference.of(Repository.class), - TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"), - TypeReference.of("org.springframework.aop.framework.Advised"), TypeReference.of(DecoratingProxy.class) }; - } - - protected void contributeProjection(Class type, CodeContribution contribution) { - - contribution.runtimeHints().proxies().registerJdkProxy(type, TargetAware.class, SpringProxy.class, - DecoratingProxy.class); - } - - /** - * Callback for module specific contributions. - * - * @param moduleContribution can be {@literal null}. - * @return this. - */ - public RepositoryBeanContribution setModuleContribution( - @Nullable BiConsumer moduleContribution) { - - this.moduleContribution = moduleContribution; - return this; - } - - public RepositoryInformation getRepositoryInformation() { - return repositoryInformation; - } -} diff --git a/src/main/java/org/springframework/data/aot/RepositoryBeanDefinitionReader.java b/src/main/java/org/springframework/data/aot/RepositoryBeanDefinitionReader.java index 7ea4023e9..558af4e79 100644 --- a/src/main/java/org/springframework/data/aot/RepositoryBeanDefinitionReader.java +++ b/src/main/java/org/springframework/data/aot/RepositoryBeanDefinitionReader.java @@ -31,42 +31,53 @@ import org.springframework.data.util.Lazy; import org.springframework.util.ClassUtils; /** - * Reader that allows to extract {@link RepositoryInformation} from metadata. + * Reader used to extract {@link RepositoryInformation} from {@link RepositoryMetadata}. * * @author Christoph Strobl - * @since 3.0 + * @author John Blum + * @see org.springframework.data.repository.config.RepositoryFragmentConfiguration + * @see org.springframework.data.repository.config.RepositoryMetadata + * @see org.springframework.data.repository.core.RepositoryInformation + * @see org.springframework.data.repository.core.support.RepositoryFragment + * @since 3.0.0 */ class RepositoryBeanDefinitionReader { - static RepositoryInformation readRepositoryInformation(RepositoryMetadata metadata, + static RepositoryInformation readRepositoryInformation(RepositoryMetadata metadata, ConfigurableListableBeanFactory beanFactory) { return new AotRepositoryInformation(metadataSupplier(metadata, beanFactory), repositoryBaseClass(metadata, beanFactory), fragments(metadata, beanFactory)); } + @SuppressWarnings({ "rawtypes", "unchecked" }) private static Supplier>> fragments(RepositoryMetadata metadata, ConfigurableListableBeanFactory beanFactory) { - return Lazy - .of(() -> (Collection>) metadata.getFragmentConfiguration().stream().flatMap(it -> { - RepositoryFragmentConfiguration fragmentConfiguration = (RepositoryFragmentConfiguration) it; - List fragments = new ArrayList<>(2); - if (fragmentConfiguration.getClassName() != null) { - fragments.add(RepositoryFragment.implemented(forName(fragmentConfiguration.getClassName(), beanFactory))); - } - if (fragmentConfiguration.getInterfaceName() != null) { - fragments - .add(RepositoryFragment.structural(forName(fragmentConfiguration.getInterfaceName(), beanFactory))); - } + return Lazy.of(() -> (Collection>) metadata.getFragmentConfiguration().stream() + .flatMap(it -> { - return fragments.stream(); - }).collect(Collectors.toList())); + RepositoryFragmentConfiguration fragmentConfiguration = (RepositoryFragmentConfiguration) it; + List fragments = new ArrayList<>(2); + + if (fragmentConfiguration.getClassName() != null) { + fragments.add(RepositoryFragment.implemented(forName(fragmentConfiguration.getClassName(), beanFactory))); + } + if (fragmentConfiguration.getInterfaceName() != null) { + fragments.add(RepositoryFragment.structural(forName(fragmentConfiguration.getInterfaceName(), beanFactory))); + } + + return fragments.stream(); + }) + .collect(Collectors.toList())); } + @SuppressWarnings({ "rawtypes", "unchecked" }) private static Supplier> repositoryBaseClass(RepositoryMetadata metadata, ConfigurableListableBeanFactory beanFactory) { - return Lazy.of(() -> (Class) metadata.getRepositoryBaseClassName().map(it -> forName(it.toString(), beanFactory)) + + return Lazy.of(() -> + (Class) metadata.getRepositoryBaseClassName().map(it -> forName(it.toString(), beanFactory)) .orElseGet(() -> { // TODO: retrieve the default without loading the actual RepositoryBeanFactory return Object.class; @@ -74,15 +85,16 @@ class RepositoryBeanDefinitionReader { } static Supplier metadataSupplier( - RepositoryMetadata metadata, ConfigurableListableBeanFactory beanFactory) { + RepositoryMetadata metadata, ConfigurableListableBeanFactory beanFactory) { + return Lazy.of(() -> new DefaultRepositoryMetadata(forName(metadata.getRepositoryInterface(), beanFactory))); } static Class forName(String name, ConfigurableListableBeanFactory beanFactory) { try { return ClassUtils.forName(name, beanFactory.getBeanClassLoader()); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + } catch (ClassNotFoundException cause) { + throw new TypeNotPresentException(name, cause); } } } diff --git a/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotContribution.java new file mode 100644 index 000000000..c4bfea655 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotContribution.java @@ -0,0 +1,441 @@ +/* + * Copyright 2012-2018 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 + * + * http://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; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.projection.TargetAware; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.config.RepositoryMetadata; +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.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanRegistrationAotContribution} used to contribute repository registrations. + * + * @author John Blum + * @see org.springframework.aot.generate.GenerationContext + * @see org.springframework.beans.factory.aot.BeanRegistrationAotContribution + * @see org.springframework.beans.factory.support.RegisteredBean + * @see org.springframework.data.aot.RepositoryRegistrationAotProcessor + * @since 3.0.0 + */ +public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { + + private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = + "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; + + /** + * Factory method used to construct a new instance of {@link RepositoryRegistrationAotContribution} initialized with + * the given, required {@link RepositoryRegistrationAotProcessor} from which this contribution was created. + * + * @param repositoryRegistrationAotProcessor reference back to the {@link RepositoryRegistrationAotProcessor} from which this + * contribution was created. + * @return a new instance of {@link RepositoryRegistrationAotContribution}. + * @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. + * @see org.springframework.data.aot.RepositoryRegistrationAotProcessor + */ + public static RepositoryRegistrationAotContribution fromProcessor( + @NonNull RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor) { + + return new RepositoryRegistrationAotContribution(repositoryRegistrationAotProcessor); + } + + private AotRepositoryContext repositoryContext; + + private BiConsumer moduleContribution; + + @NonNull + private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor; + + /** + * Constructs a new instance of the {@link RepositoryRegistrationAotContribution} initialized with the given, + * required {@link RepositoryRegistrationAotProcessor} from which this contribution was created. + * + * @param repositoryRegistrationAotProcessor reference back to the {@link RepositoryRegistrationAotProcessor} from which this + * contribution was created. + * @throws IllegalArgumentException if the {@link RepositoryRegistrationAotProcessor} is {@literal null}. + * @see org.springframework.data.aot.RepositoryRegistrationAotProcessor + */ + protected RepositoryRegistrationAotContribution( + @NonNull RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor) { + + Assert.notNull(repositoryRegistrationAotProcessor, + "RepositoryRegistrationAotProcessor must not be null"); + + this.repositoryRegistrationAotProcessor = repositoryRegistrationAotProcessor; + } + + @NonNull + protected ConfigurableListableBeanFactory getBeanFactory() { + return getRepositoryRegistrationAotProcessor().getBeanFactory(); + } + + protected Optional> getModuleContribution() { + return Optional.ofNullable(this.moduleContribution); + } + + @NonNull + protected AotRepositoryContext getRepositoryContext() { + + Assert.state(this.repositoryContext != null, + "The AOT RepositoryContext was not properly initialized; did you call the forBean(:RegisteredBean) method"); + + return this.repositoryContext; + } + + @NonNull + protected RepositoryRegistrationAotProcessor getRepositoryRegistrationAotProcessor() { + return this.repositoryRegistrationAotProcessor; + } + + public RepositoryInformation getRepositoryInformation() { + return getRepositoryContext().getRepositoryInformation(); + } + + private void logTrace(String message, Object... arguments) { + getRepositoryRegistrationAotProcessor().logTrace(message, arguments); + } + + /** + * Builds a {@link RepositoryRegistrationAotContribution} for given, required {@link RegisteredBean} + * representing the {@link Repository} registered in the bean registry. + * + * @param repositoryBean {@link RegisteredBean} for the {@link Repository}; must not be {@literal null}. + * @return a {@link RepositoryRegistrationAotContribution} to contribute AOT metadata and code + * for the {@link Repository} {@link RegisteredBean}. + * @throws IllegalArgumentException if the {@link RegisteredBean} is {@literal null}. + * @see org.springframework.beans.factory.support.RegisteredBean + */ + public RepositoryRegistrationAotContribution forBean(@NonNull RegisteredBean repositoryBean) { + + Assert.notNull(repositoryBean, "The RegisteredBean for the repository must not be null"); + + RepositoryMetadata repositoryMetadata = getRepositoryRegistrationAotProcessor().getRepositoryMetadata(repositoryBean); + + this.repositoryContext = buildAotRepositoryContext(repositoryBean, repositoryMetadata); + + enhanceRepositoryBeanDefinition(repositoryBean, repositoryMetadata, this.repositoryContext); + + return this; + } + + protected DefaultAotRepositoryContext buildAotRepositoryContext(@NonNull RegisteredBean bean, + @NonNull RepositoryMetadata repositoryMetadata) { + + RepositoryInformation repositoryInformation = resolveRepositoryInformation(repositoryMetadata); + + DefaultAotRepositoryContext repositoryContext = new DefaultAotRepositoryContext(); + + repositoryContext.setAotContext(this::getBeanFactory); + repositoryContext.setBeanName(bean.getBeanName()); + repositoryContext.setBasePackages(repositoryMetadata.getBasePackages().toSet()); + repositoryContext.setIdentifyingAnnotations(resolveIdentifyingAnnotations()); + repositoryContext.setRepositoryInformation(repositoryInformation); + + return repositoryContext; + } + + private Set> resolveIdentifyingAnnotations() { + + Set> identifyingAnnotations = Collections.emptySet(); + + try { + // TODO: Getting all beans of type RepositoryConfigurationExtensionSupport will have the effect that + // if the user is currently operating in multi-store mode, then all identifying annotations from + // all stores will be included in the resulting Set. + // When using AOT, is multi-store mode allowed? I don't see why not, but does this work correctly + // with AOT, ATM? + Map repositoryConfigurationExtensionBeans = + getBeanFactory().getBeansOfType(RepositoryConfigurationExtensionSupport.class); + + repositoryConfigurationExtensionBeans.values().stream() + .map(RepositoryConfigurationExtensionSupport::getIdentifyingAnnotations) + .flatMap(Collection::stream) + .collect(Collectors.toCollection(() -> identifyingAnnotations)); + + } catch (Throwable ignore) { + // Possible BeansException because no bean exists of type RepositoryConfigurationExtension, + // which included non-Singletons and occurred during eager initialization. + } + + return identifyingAnnotations; + } + + private RepositoryInformation resolveRepositoryInformation(RepositoryMetadata repositoryMetadata) { + return RepositoryBeanDefinitionReader.readRepositoryInformation(repositoryMetadata, getBeanFactory()); + } + + /** + * Helps the AOT processing render the {@link FactoryBean} type correctly that is used to tell the outcome of the {@link FactoryBean}. + * + * We just need to set the target {@link Repository} {@link Class type} of the {@link RepositoryFactoryBeanSupport} + * while keeping the actual ID and DomainType set to {@link Object}. If the generic type signature does not match, + * then we do not try to resolve and remap the types, but rather set the {@literal factoryBeanObjectType} attribute + * on the {@link RootBeanDefinition}. + */ + protected void enhanceRepositoryBeanDefinition(@NonNull RegisteredBean repositoryBean, + @NonNull RepositoryMetadata repositoryMetadata, @NonNull AotRepositoryContext repositoryContext) { + + logTrace(String.format("Enhancing repository factory bean definition [%s]", repositoryBean.getBeanName())); + + Class repositoryFactoryBeanType = repositoryContext + .resolveType(repositoryMetadata.getRepositoryFactoryBeanClassName()) + .orElse(RepositoryFactoryBeanSupport.class); + + ResolvableType resolvedRepositoryFactoryBeanType = ResolvableType.forClass(repositoryFactoryBeanType); + + RootBeanDefinition repositoryBeanDefinition = repositoryBean.getMergedBeanDefinition(); + + if (isRepositoryWithTypeParameters(resolvedRepositoryFactoryBeanType)) { + repositoryBeanDefinition.setTargetType(ResolvableType.forClassWithGenerics(repositoryFactoryBeanType, + repositoryContext.getRepositoryInformation().getRepositoryInterface(), Object.class, Object.class)); + } else { + repositoryBeanDefinition.setTargetType(resolvedRepositoryFactoryBeanType); + repositoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, + repositoryContext.getRepositoryInformation().getRepositoryInterface()); + } + } + + private boolean isRepositoryWithTypeParameters(ResolvableType type) { + return type != null && type.getGenerics().length == 3; + } + + /** + * {@link BiConsumer Callback} for data module specific contributions. + * + * @param moduleContribution {@link BiConsumer} used by data modules to submit contributions; can be {@literal null}. + * @return this. + */ + @NonNull + @SuppressWarnings("unused") + public RepositoryRegistrationAotContribution withModuleContribution( + @Nullable BiConsumer moduleContribution) { + this.moduleContribution = moduleContribution; + return this; + } + + @Override + public void applyTo(@NonNull GenerationContext generationContext, + @NonNull BeanRegistrationCode beanRegistrationCode) { + + contributeRepositoryInfo(this.repositoryContext, generationContext); + getModuleContribution().ifPresent(it -> it.accept(getRepositoryContext(), generationContext)); + } + + private void contributeRepositoryInfo(@NonNull AotRepositoryContext repositoryContext, + @NonNull GenerationContext contribution) { + + RepositoryInformation repositoryInformation = getRepositoryInformation(); + + logTrace("Contributing repository information for [%s]", + repositoryInformation.getRepositoryInterface()); + + // TODO: is this the way? + 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.getDomainType(), hint -> + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); + + // Repository Fragments + for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { + + Class repositoryFragmentType = fragment.getSignatureContributor(); + + contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { + + hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + + if (!repositoryFragmentType.isInterface()) { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + }); + } + + // Repository Proxy + contribution.getRuntimeHints().proxies().registerJdkProxy(repositoryInformation.getRepositoryInterface(), + SpringProxy.class, Advised.class, DecoratingProxy.class); + + // Transactional Repository Proxy + repositoryContext.ifTransactionManagerPresent(transactionManagerBeanNames -> { + + // TODO: Is the following double JDK Proxy registration above necessary or would a single JDK Proxy + // registration suffice? + // In other words, simply having a single JDK Proxy registration either with or without + // the additional Serializable TypeReference? + // NOTE: Using a single JDK Proxy registration causes the + // simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() test case method to fail. + List transactionalRepositoryProxyTypeReferences = + transactionalRepositoryProxyTypeReferences(repositoryInformation); + + contribution.getRuntimeHints().proxies() + .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); + + if (isComponentAnnotatedRepository(repositoryInformation)) { + transactionalRepositoryProxyTypeReferences.add(TypeReference.of(Serializable.class)); + contribution.getRuntimeHints().proxies() + .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); + } + }); + + // Reactive Repositories + if (repositoryInformation.isReactiveRepository()) { + // TODO: do we still need this and how to configure it? + // registry.initialization().add(NativeInitializationEntry.ofBuildTimeType(configuration.getRepositoryInterface())); + } + + // Kotlin + if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { + contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), + hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + } + + // Repository query methods + repositoryInformation.getQueryMethods() + .map(repositoryInformation::getReturnedDomainClass) + .filter(Class::isInterface) + .forEach(type -> { + if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .test(type, repositoryInformation.getDomainType())) { + contributeProjection(type, contribution); + } + }); + } + + private boolean isComponentAnnotatedRepository(RepositoryInformation repositoryInformation) { + return AnnotationUtils.findAnnotation(repositoryInformation.getRepositoryInterface(), Component.class) != null; + } + + private boolean isKotlinCoroutineRepository(AotRepositoryContext repositoryContext, + RepositoryInformation repositoryInformation) { + + return repositoryContext.resolveType(KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME) + .filter(it -> ClassUtils.isAssignable(it, repositoryInformation.getRepositoryInterface())) + .isPresent(); + } + + private List kotlinRepositoryReflectionTypeReferences() { + + return new ArrayList<>(Arrays.asList( + TypeReference.of("org.springframework.data.repository.kotlin.CoroutineCrudRepository"), + TypeReference.of(Repository.class), + TypeReference.of(Iterable.class), + TypeReference.of("kotlinx.coroutines.flow.Flow"), + TypeReference.of("kotlin.collections.Iterable"), + TypeReference.of("kotlin.Unit"), + TypeReference.of("kotlin.Long"), + TypeReference.of("kotlin.Boolean") + )); + } + + private List transactionalRepositoryProxyTypeReferences(RepositoryInformation repositoryInformation) { + + return new ArrayList<>(Arrays.asList( + TypeReference.of(repositoryInformation.getRepositoryInterface()), + TypeReference.of(Repository.class), + TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"), + TypeReference.of("org.springframework.aop.framework.Advised"), + TypeReference.of(DecoratingProxy.class) + )); + } + + private void contributeProjection(Class type, GenerationContext generationContext) { + + generationContext.getRuntimeHints().proxies() + .registerJdkProxy(type, TargetAware.class, SpringProxy.class, DecoratingProxy.class); + } + + protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + + repositoryContext.getResolvedTypes().stream() + .filter(it -> !isJavaOrPrimitiveType(it)) + .forEach(it -> contributeType(it, generationContext)); + + repositoryContext.getResolvedAnnotations().stream() + .filter(this::isSpringDataManagedAnnotation) + .map(MergedAnnotation::getType) + .forEach(it -> contributeType(it, generationContext)); + } + + private boolean isJavaOrPrimitiveType(Class type) { + + return TypeUtils.type(type).isPartOf("java") + || type.isPrimitive() + || ClassUtils.isPrimitiveArray(type); + } + + private boolean isSpringDataManagedAnnotation(MergedAnnotation annotation) { + + return annotation != null + && (isInSpringDataNamespace(annotation.getType()) || annotation.getMetaTypes().stream() + .anyMatch(this::isInSpringDataNamespace)); + } + + private boolean isInSpringDataNamespace(Class type) { + return type != null && type.getPackage().getName().startsWith(TypeContributor.DATA_NAMESPACE); + } + + protected void contributeType(Class type, GenerationContext generationContext) { + + logTrace("Contributing type information for [%s]", type); + TypeContributor.contribute(type, it -> true, generationContext); + } + + // TODO What was this meant to be used for? Was this type filter maybe meant to be used in + // the TypeContributor.contribute(:Class, :Predicate :GenerationContext) method + // used in the contributeType(..) method above? + public Predicate> typeFilter() { // like only document ones. // TODO: As in MongoDB? + return it -> true; + } +} diff --git a/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotProcessor.java new file mode 100644 index 000000000..4e9752f98 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/RepositoryRegistrationAotProcessor.java @@ -0,0 +1,161 @@ +/* + * 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; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link BeanRegistrationAotProcessor} responsible processing and providing AOT configuration for repositories. + *

+ * Processes {@link RepositoryFactoryBeanSupport repository factory beans} to provide generic type information to + * the AOT tooling to allow deriving target type from the {@link RootBeanDefinition bean definition}. If generic types + * do not match due to customization of the factory bean by the user, at least the target repository type is provided + * via the {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE}. + *

+ *

+ * With {@link RepositoryRegistrationAotContribution#contribute(AotRepositoryContext, GenerationContext)}, stores + * can provide custom logic for contributing additional (eg. reflection) configuration. By default, reflection + * configuration will be added for types reachable from the repository declaration and query methods as well as + * all used {@link Annotation annotations} from the {@literal org.springframework.data} namespace. + *

+ * The processor is typically configured via {@link RepositoryConfigurationExtension#getRepositoryAotProcessor()} + * and gets added by the {@link org.springframework.data.repository.config.RepositoryConfigurationDelegate}. + * + * @author Christoph Strobl + * @author John Blum + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor + * @since 3.0.0 + */ +@SuppressWarnings("unused") +public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware { + + private ConfigurableListableBeanFactory beanFactory; + + private final Log logger = LogFactory.getLog(getClass()); + + private Map> configMap; + + @Nullable + @Override + public BeanRegistrationAotContribution processAheadOfTime(@NonNull RegisteredBean bean) { + + return isRepositoryBean(bean) + ? newRepositoryRegistrationAotContribution(bean) + : null; + } + + private boolean isRepositoryBean(RegisteredBean bean) { + return bean != null && getConfigMap().containsKey(bean.getBeanName()); + } + + protected RepositoryRegistrationAotContribution newRepositoryRegistrationAotContribution( + @NonNull RegisteredBean repositoryBean) { + + RepositoryRegistrationAotContribution contribution = + RepositoryRegistrationAotContribution.fromProcessor(this).forBean(repositoryBean); + + return contribution.withModuleContribution(contribution::contribute); + } + + @Override + public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException { + + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, + () -> "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); + + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + @NonNull + protected ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + public void setConfigMap(@Nullable Map> configMap) { + this.configMap = configMap; + } + + @NonNull + public Map> getConfigMap() { + return nullSafeMap(this.configMap); + } + + @NonNull + private Map nullSafeMap(@Nullable Map map) { + return map != null ? map : Collections.emptyMap(); + } + + @Nullable + protected RepositoryMetadata getRepositoryMetadata(@NonNull RegisteredBean bean) { + return getConfigMap().get(nullSafeBeanName(bean)); + } + + private String nullSafeBeanName(@Nullable RegisteredBean bean) { + + String beanName = bean != null ? bean.getBeanName() : null; + + return StringUtils.hasText(beanName) ? beanName : ""; + } + + @NonNull + protected Log getLogger() { + return this.logger; + } + + private void logAt(Predicate logLevelPredicate, BiConsumer logOperation, + String message, Object... arguments) { + + Log logger = getLogger(); + + if (logLevelPredicate.test(logger)) { + logOperation.accept(logger, String.format(message, arguments)); + } + } + + protected void logDebug(String message, Object... arguments) { + logAt(Log::isDebugEnabled, Log::debug, message, arguments); + } + + protected void logTrace(String message, Object... arguments) { + logAt(Log::isTraceEnabled, Log::trace, message, arguments); + } +} diff --git a/src/main/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessor.java b/src/main/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessor.java similarity index 54% rename from src/main/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessor.java rename to src/main/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessor.java index e77760358..151db5484 100644 --- a/src/main/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessor.java +++ b/src/main/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessor.java @@ -19,49 +19,67 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; -import org.springframework.beans.factory.generator.AotContributingBeanFactoryPostProcessor; -import org.springframework.beans.factory.generator.BeanFactoryContribution; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.data.ManagedTypes; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** - * {@link AotContributingBeanFactoryPostProcessor} implementation to capture common data infrastructure concerns. + * {@link BeanFactoryInitializationAotProcessor} implementation used to encapsulate common data infrastructure concerns + * and preprocess the {@link ConfigurableListableBeanFactory} ahead of the AOT compilation in order to prepare + * the Spring Data {@link BeanDefinition BeanDefinitions} for AOT processing. * * @author Christoph Strobl + * @author John Blum + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory + * @see org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution + * @see org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor * @since 3.0 */ -public class AotDataComponentsBeanFactoryPostProcessor implements AotContributingBeanFactoryPostProcessor { +public class SpringDataBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final Log logger = LogFactory.getLog(AotContributingBeanFactoryPostProcessor.class); + private static final Log logger = LogFactory.getLog(BeanFactoryInitializationAotProcessor.class); @Nullable @Override - public BeanFactoryContribution contribute(ConfigurableListableBeanFactory beanFactory) { - postProcessManagedTypes(beanFactory); + public BeanFactoryInitializationAotContribution processAheadOfTime( + @NonNull ConfigurableListableBeanFactory beanFactory) { + + processManagedTypes(beanFactory); return null; } - private void postProcessManagedTypes(ConfigurableListableBeanFactory beanFactory) { + private void processManagedTypes(@NonNull ConfigurableListableBeanFactory beanFactory) { if (beanFactory instanceof BeanDefinitionRegistry registry) { for (String beanName : beanFactory.getBeanNamesForType(ManagedTypes.class)) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); - ValueHolder argumentValue = beanDefinition.getConstructorArgumentValues().getArgumentValue(0, null, null, null); + + ValueHolder argumentValue = beanDefinition.getConstructorArgumentValues() + .getArgumentValue(0, null, null, null); + if (argumentValue.getValue() instanceof Supplier supplier) { if (logger.isDebugEnabled()) { logger.info(String.format("Replacing ManagedType bean definition %s.", beanName)); } + BeanDefinition beanDefinitionReplacement = BeanDefinitionBuilder + .rootBeanDefinition(ManagedTypes.class) + .setFactoryMethod("of") + .addConstructorArgValue(supplier.get()) + .getBeanDefinition(); + registry.removeBeanDefinition(beanName); - registry.registerBeanDefinition(beanName, BeanDefinitionBuilder.rootBeanDefinition(ManagedTypes.class) - .setFactoryMethod("of").addConstructorArgValue(supplier.get()).getBeanDefinition()); + registry.registerBeanDefinition(beanName, beanDefinitionReplacement); } } } diff --git a/src/main/java/org/springframework/data/aot/TypeCollector.java b/src/main/java/org/springframework/data/aot/TypeCollector.java index 2c17ffc1b..e7392658c 100644 --- a/src/main/java/org/springframework/data/aot/TypeCollector.java +++ b/src/main/java/org/springframework/data/aot/TypeCollector.java @@ -17,8 +17,8 @@ package org.springframework.data.aot; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; @@ -37,57 +37,64 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.ResolvableType; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; /** * @author Christoph Strobl * @author Sebastien Deleuze + * @author John Blum */ public class TypeCollector { - private static Log logger = LogFactory.getLog(TypeCollector.class); + private static final Log logger = LogFactory.getLog(TypeCollector.class); static final Set EXCLUDED_DOMAINS = new HashSet<>(Arrays.asList("java", "sun.", "jdk.", "reactor.", "kotlinx.", "kotlin.", "org.springframework.core.", "org.springframework.boot.")); - private Predicate> excludedDomainsFilter = (type) -> { - return EXCLUDED_DOMAINS.stream().noneMatch(type.getPackageName()::startsWith); + private final Predicate> excludedDomainsFilter = type -> { + String packageName = type.getPackageName(); + return EXCLUDED_DOMAINS.stream().noneMatch(packageName::startsWith); }; - Predicate> typeFilter = excludedDomainsFilter; + private Predicate> typeFilter = excludedDomainsFilter; - private final Predicate methodFilter = (method) -> { - if (method.getName().startsWith("$$_hibernate")) { - return false; - } - if (method.getDeclaringClass().getPackageName().startsWith("java.") || method.getDeclaringClass().isEnum() - || EXCLUDED_DOMAINS.stream().anyMatch(it -> method.getDeclaringClass().getPackageName().startsWith(it))) { - return false; - } - if (method.isBridge() || method.isSynthetic()) { - return false; - } - return (!Modifier.isNative(method.getModifiers()) && !Modifier.isPrivate(method.getModifiers()) - && !Modifier.isProtected(method.getModifiers())) || !method.getDeclaringClass().equals(Object.class); + private final Predicate methodFilter = method -> { + + Predicate excludedDomainsPredicate = methodToTest -> + excludedDomainsFilter.test(methodToTest.getDeclaringClass()); + + Predicate excludedMethodsPredicate = Predicates.IS_BRIDGE_METHOD + .or(Predicates.IS_OBJECT_MEMBER) + .or(Predicates.IS_HIBERNATE_MEMBER) + .or(Predicates.IS_ENUM_MEMBER) + .or(Predicates.IS_NATIVE) + .or(Predicates.IS_PRIVATE) + .or(Predicates.IS_PROTECTED) + .or(Predicates.IS_SYNTHETIC) + .or(excludedDomainsPredicate.negate()); + + return !excludedMethodsPredicate.test(method); }; - private Predicate fieldFilter = (field) -> { - if (field.isSynthetic() | field.getName().startsWith("$$_hibernate")) { - return false; - } - if (field.getDeclaringClass().getPackageName().startsWith("java.")) { - return false; - } - return true; + private Predicate fieldFilter = field -> { + + Predicate excludedFieldPredicate = Predicates.IS_HIBERNATE_MEMBER + .or(Predicates.IS_SYNTHETIC) + .or(Predicates.IS_JAVA); + + return !excludedFieldPredicate.test(field); }; - public TypeCollector filterFields(Predicate filter) { + @NonNull + public TypeCollector filterFields(@NonNull Predicate filter) { this.fieldFilter = filter.and(filter); return this; } - public TypeCollector filterTypes(Predicate> filter) { + @NonNull + public TypeCollector filterTypes(@NonNull Predicate> filter) { this.typeFilter = this.typeFilter.and(filter); return this; } @@ -98,10 +105,12 @@ public class TypeCollector { * @param types the types to inspect * @return a type model collector for the type */ + @NonNull public static ReachableTypes inspect(Class... types) { return inspect(Arrays.asList(types)); } + @NonNull public static ReachableTypes inspect(Collection> types) { return new ReachableTypes(new TypeCollector(), types); } @@ -168,8 +177,8 @@ public class TypeCollector { } } }); - } catch (Exception ex) { - logger.warn(ex); + } catch (Exception cause) { + logger.warn(cause); } return new HashSet<>(discoveredTypes); } @@ -191,11 +200,11 @@ public class TypeCollector { public static class ReachableTypes { - private TypeCollector typeCollector; private final Iterable> roots; private final Lazy>> reachableTypes = Lazy.of(this::collect); + private final TypeCollector typeCollector; - public ReachableTypes(TypeCollector typeCollector, Iterable> roots) { + public ReachableTypes(@NonNull TypeCollector typeCollector, @NonNull Iterable> roots) { this.typeCollector = typeCollector; this.roots = roots; @@ -220,24 +229,24 @@ public class TypeCollector { private final Map mutableCache = new LinkedHashMap<>(); - public void add(ResolvableType resolvableType) { + public void add(@NonNull ResolvableType resolvableType) { mutableCache.put(resolvableType.toString(), resolvableType); } - public boolean contains(ResolvableType key) { - return mutableCache.containsKey(key.toString()); + public void clear() { + mutableCache.clear(); } - public int size() { - return mutableCache.size(); + public boolean contains(@NonNull ResolvableType key) { + return mutableCache.containsKey(key.toString()); } public boolean isEmpty() { return mutableCache.isEmpty(); } - public void clear() { - mutableCache.clear(); + public int size() { + return mutableCache.size(); } } } diff --git a/src/main/java/org/springframework/data/aot/TypeContributor.java b/src/main/java/org/springframework/data/aot/TypeContributor.java index b77a3fa0f..ca4a7e4bf 100644 --- a/src/main/java/org/springframework/data/aot/TypeContributor.java +++ b/src/main/java/org/springframework/data/aot/TypeContributor.java @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.Set; import java.util.function.Predicate; -import org.springframework.aot.generator.CodeContribution; +import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.SynthesizedAnnotation; @@ -39,7 +39,7 @@ class TypeContributor { * @param type * @param contribution */ - static void contribute(Class type, CodeContribution contribution) { + static void contribute(Class type, GenerationContext contribution) { contribute(type, Collections.emptySet(), contribution); } @@ -50,7 +50,8 @@ class TypeContributor { * @param filter * @param contribution */ - static void contribute(Class type, Predicate> filter, CodeContribution contribution) { + @SuppressWarnings("unchecked") + static void contribute(Class type, Predicate> filter, GenerationContext contribution) { if (type.isPrimitive()) { return; @@ -58,27 +59,24 @@ class TypeContributor { if (type.isAnnotation() && filter.test((Class) type)) { - contribution.runtimeHints().reflection().registerType(type, hint -> { - hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS); - }); + contribution.getRuntimeHints().reflection().registerType(type, hint -> + hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); // TODO: do we need this if meta annotated with SD annotation? if (type.getPackage().getName().startsWith(DATA_NAMESPACE)) { - contribution.runtimeHints().proxies().registerJdkProxy(type, SynthesizedAnnotation.class); + contribution.getRuntimeHints().proxies().registerJdkProxy(type, SynthesizedAnnotation.class); } return; } if (type.isInterface()) { - contribution.runtimeHints().reflection().registerType(type, hint -> { - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - }); + contribution.getRuntimeHints().reflection().registerType(type, hint -> + hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); return; } - contribution.runtimeHints().reflection().registerType(type, hint -> { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); - }); + contribution.getRuntimeHints().reflection().registerType(type, hint -> + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); } /** @@ -89,7 +87,7 @@ class TypeContributor { * @param annotationNamespaces * @param contribution */ - static void contribute(Class type, Set annotationNamespaces, CodeContribution contribution) { + static void contribute(Class type, Set annotationNamespaces, GenerationContext contribution) { contribute(type, it -> isPartOfOrMetaAnnotatedWith(it, annotationNamespaces), contribution); } diff --git a/src/main/java/org/springframework/data/aot/TypeScanner.java b/src/main/java/org/springframework/data/aot/TypeScanner.java index cdd06b055..158d136ff 100644 --- a/src/main/java/org/springframework/data/aot/TypeScanner.java +++ b/src/main/java/org/springframework/data/aot/TypeScanner.java @@ -18,6 +18,7 @@ package org.springframework.data.aot; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; @@ -26,37 +27,89 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** + * Scanner used to scan for {@link Class types} that will be added to the AOT processing infrastructure. + * * @author Christoph Strobl + * @author John Blum */ -public class TypeScanner { // TODO: replace this with AnnotatedTypeScanner maybe? +// TODO: Replace this with AnnotatedTypeScanner, maybe? +public class TypeScanner { - private final ClassLoader classLoader; - - public TypeScanner(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - static TypeScanner scanner(ClassLoader classLoader) { + /** + * Factory method used to construct a new {@link TypeScanner} initialized with the given {@link ClassLoader} + * used to resolve scanned {@link Class type}. + * + * @param classLoader {@link ClassLoader} used to resolve scanned {@link Class types}. + * @return a new {@link TypeScanner}. + */ + @NonNull + static TypeScanner scanner(@Nullable ClassLoader classLoader) { return new TypeScanner(classLoader); } + private final ClassLoader classLoader; + + /** + * Constructs a new instance of {@link TypeScanner} initialized with the given {@link ClassLoader} + * used to resolve scanned {@link Class types}. + * + * @param classLoader {@link ClassLoader} used to resolve scanned {@link Class types}. + */ + public TypeScanner(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Scans for {@link Class types} potentially annotated with any of the given {@link Annotation} types. + * + * @param annotations array of {@link Annotation} types used to filter the scanned {@link Class types}; + * must not be {@literal null}. + * @return new instance of {@link Scanner}. + * @see #scanForTypesAnnotatedWith(Collection) + * @see Scanner + */ + @SuppressWarnings("unchecked") public Scanner scanForTypesAnnotatedWith(Class... annotations) { return scanForTypesAnnotatedWith(Arrays.asList(annotations)); } + /** + * Scans for {@link Class types} potentially annotated with any of the given {@link Annotation} types. + * + * @param annotations {@link Collection} of {@link Annotation} types used to filter the scanned {@link Class types}. + * @return new instance of {@link Scanner}. + * @see #scanForTypesAnnotatedWith(Class[]) + * @see Scanner + */ public Scanner scanForTypesAnnotatedWith(Collection> annotations) { return new ScannerImpl().includeTypesAnnotatedWith(annotations); } public interface Scanner { + /** + * Collects the {@link String names} of packages to scan. + * + * @param packageNames array of {@link String package names} ot scan; must not be {@literal null}. + * @return the resolved, scanned {@link Class types}. + * @see #inPackages(Collection) + */ default Set> inPackages(String... packageNames) { return inPackages(Arrays.asList(packageNames)); } + /** + * Collects the {@link String names} of packages to scan. + * + * @param packageNames {@link Collection} of {@link String package names} ot scan. + * @return the resolved, scanned {@link Class types}. + * @see #inPackages(String...) + */ Set> inPackages(Collection packageNames); } @@ -64,7 +117,7 @@ public class TypeScanner { // TODO: replace this with AnnotatedTypeScanner maybe ClassPathScanningCandidateComponentProvider componentProvider; - public ScannerImpl() { + ScannerImpl() { componentProvider = new ClassPathScanningCandidateComponentProvider(false); componentProvider.setEnvironment(new StandardEnvironment()); @@ -72,7 +125,11 @@ public class TypeScanner { // TODO: replace this with AnnotatedTypeScanner maybe } ScannerImpl includeTypesAnnotatedWith(Collection> annotations) { - annotations.stream().map(AnnotationTypeFilter::new).forEach(componentProvider::addIncludeFilter); + + nullSafeCollection(annotations).stream() + .map(AnnotationTypeFilter::new) + .forEach(componentProvider::addIncludeFilter); + return this; } @@ -81,27 +138,29 @@ public class TypeScanner { // TODO: replace this with AnnotatedTypeScanner maybe Set> types = new LinkedHashSet<>(); - packageNames.forEach(pkg -> { - componentProvider.findCandidateComponents(pkg).forEach(it -> { - resolveType(it.getBeanClassName()).ifPresent(types::add); - }); - }); + nullSafeCollection(packageNames).forEach(pkg -> + componentProvider.findCandidateComponents(pkg).forEach(it -> + resolveType(it.getBeanClassName()).ifPresent(types::add))); return types; } + + @NonNull + private Collection nullSafeCollection(@Nullable Collection collection) { + return collection != null ? collection : Collections.emptySet(); + } } private Optional> resolveType(String typeName) { - if (!ClassUtils.isPresent(typeName, classLoader)) { - return Optional.empty(); - } - try { - return Optional.of(ClassUtils.forName(typeName, classLoader)); - } catch (ClassNotFoundException e) { - // just do nothing + if (ClassUtils.isPresent(typeName, classLoader)) { + try { + return Optional.of(ClassUtils.forName(typeName, classLoader)); + } catch (ClassNotFoundException ignore) { + // just do nothing + } } + return Optional.empty(); } - } diff --git a/src/main/java/org/springframework/data/aot/TypeUtils.java b/src/main/java/org/springframework/data/aot/TypeUtils.java index 47cda5cb3..53980effc 100644 --- a/src/main/java/org/springframework/data/aot/TypeUtils.java +++ b/src/main/java/org/springframework/data/aot/TypeUtils.java @@ -38,6 +38,7 @@ import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl */ +// TODO: Consider moving to the org.springframework.data.util package. public class TypeUtils { /** diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index 73fcddc31..804ac244b 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -48,11 +49,11 @@ import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; import org.springframework.data.ManagedTypes; -import org.springframework.data.aot.AotDataComponentsBeanFactoryPostProcessor; import org.springframework.data.aot.TypeScanner; import org.springframework.data.repository.config.RepositoryConfigurationDelegate.LazyRepositoryInjectionPointResolver.ManagedTypesBean; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StopWatch; @@ -67,6 +68,7 @@ import org.springframework.util.StopWatch; * @author Jens Schauder * @author Mark Paluch * @author Christoph Strobl + * @author John Blum */ public class RepositoryConfigurationDelegate { @@ -114,7 +116,8 @@ public class RepositoryConfigurationDelegate { * * @param environment can be {@literal null}. * @param resourceLoader can be {@literal null}. - * @return + * @return the given {@link Environment} if not {@literal null}, a configured {@link Environment}, + * or a default {@link Environment}. */ private static Environment defaultEnvironment(@Nullable Environment environment, @Nullable ResourceLoader resourceLoader) { @@ -128,11 +131,13 @@ public class RepositoryConfigurationDelegate { } /** - * Registers the found repositories in the given {@link BeanDefinitionRegistry}. + * Registers the discovered repositories in the given {@link BeanDefinitionRegistry}. * - * @param registry - * @param extension + * @param registry {@link BeanDefinitionRegistry} in which to register the repository bean. + * @param extension {@link RepositoryConfigurationExtension} for the module. * @return {@link BeanComponentDefinition}s for all repository bean definitions found. + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension + * @see org.springframework.beans.factory.support.BeanDefinitionRegistry */ public List registerRepositoriesIn(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension) { @@ -146,9 +151,6 @@ public class RepositoryConfigurationDelegate { RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, configurationSource, resourceLoader, environment); - List definitions = new ArrayList<>(); - - StopWatch watch = new StopWatch(); if (logger.isDebugEnabled()) { logger.debug(LogMessage.format("Scanning for %s repositories in packages %s.", // @@ -156,19 +158,22 @@ public class RepositoryConfigurationDelegate { configurationSource.getBasePackages().stream().collect(Collectors.joining(", ")))); } + StopWatch watch = new StopWatch(); ApplicationStartup startup = getStartup(registry); StartupStep repoScan = startup.start("spring.data.repository.scanning"); + repoScan.tag("dataModule", extension.getModuleName()); repoScan.tag("basePackages", () -> configurationSource.getBasePackages().stream().collect(Collectors.joining(", "))); - watch.start(); Collection> configurations = extension .getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode); + List definitions = new ArrayList<>(); + Map> configurationsByRepositoryName = new HashMap<>(configurations.size()); - Map> metadataMap = new HashMap<>(configurations.size()); + Map> metadataByRepositoryBeanName = new HashMap<>(configurations.size()); for (RepositoryConfiguration configuration : configurations) { @@ -185,6 +190,8 @@ public class RepositoryConfigurationDelegate { } AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition(); + + beanDefinition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, configuration.getRepositoryInterface()); beanDefinition.setResourceDescription(configuration.getResourceDescription()); String beanName = configurationSource.generateBeanName(beanDefinition); @@ -194,9 +201,7 @@ public class RepositoryConfigurationDelegate { configuration.getRepositoryInterface(), configuration.getRepositoryFactoryBeanClassName())); } - metadataMap.put(beanName, builder.buildMetadata(configuration)); - - beanDefinition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, configuration.getRepositoryInterface()); + metadataByRepositoryBeanName.put(beanName, builder.buildMetadata(configuration)); registry.registerBeanDefinition(beanName, beanDefinition); definitions.add(new BeanComponentDefinition(beanDefinition, beanName)); } @@ -204,68 +209,62 @@ public class RepositoryConfigurationDelegate { potentiallyLazifyRepositories(configurationsByRepositoryName, registry, configurationSource.getBootstrapMode()); watch.stop(); - repoScan.tag("repository.count", Integer.toString(configurations.size())); repoScan.end(); if (logger.isInfoEnabled()) { - logger.info( - LogMessage.format("Finished Spring Data repository scanning in %s ms. Found %s %s repository interfaces.", // - watch.getLastTaskTimeMillis(), configurations.size(), extension.getModuleName())); + logger.info(LogMessage.format("Finished Spring Data repository scanning in %s ms. Found %s %s repository interfaces.", + watch.getLastTaskTimeMillis(), configurations.size(), extension.getModuleName())); } // TODO: AOT Processing -> guard this one with a flag so it's not always present - registerAotComponents(registry, extension, metadataMap); + // TODO: With regard to AOT Processing, perhaps we need to be smart and detect whether "core" AOT components are + // (or rather configuration is) present on the classpath to enable Spring Data AOT component registration. + registerAotComponents(registry, extension, metadataByRepositoryBeanName); return definitions; } private void registerAotComponents(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, - Map> metadataMap) { + Map> metadataByRepositoryBeanName) { - { // overall general data bean factory postprocessor - TODO: move this to spring factories!!! - if (!registry.isBeanNameInUse(AotDataComponentsBeanFactoryPostProcessor.class.getName())) { - registry.registerBeanDefinition(AotDataComponentsBeanFactoryPostProcessor.class.getName(), BeanDefinitionBuilder - .rootBeanDefinition(AotDataComponentsBeanFactoryPostProcessor.class).getBeanDefinition()); + // Managed types lookup if possible + if (extension instanceof RepositoryConfigurationExtensionSupport extensionSupport) { + + String targetManagedTypesBeanName = String.format("%s.managed-types", extension.getModulePrefix()); + + if (!registry.isBeanNameInUse(targetManagedTypesBeanName)) { + + // this needs to be lazy or we'd resolve types to early maybe + Supplier>> args = () -> { + + Set packages = metadataByRepositoryBeanName.values().stream() + .flatMap(it -> it.getBasePackages().stream()) + .collect(Collectors.toSet()); + + return new TypeScanner(resourceLoader.getClassLoader()) + .scanForTypesAnnotatedWith(extensionSupport.getIdentifyingAnnotations()) + .inPackages(packages); + }; + + registry.registerBeanDefinition(targetManagedTypesBeanName, BeanDefinitionBuilder + .rootBeanDefinition(ManagedTypesBean.class).addConstructorArgValue(args).getBeanDefinition()); } } - { // Managed types lookup if possible - if (extension instanceof RepositoryConfigurationExtensionSupport configExtensionSupport) { + // module-specific repository aot processor + String repositoryAotProcessorBeanName = String.format("data-%s.repository-aot-processor" /* might be duplicate */, + extension.getModulePrefix()); - String targetManagedTypesBeanName = String.format("%s.managed-types", extension.getModulePrefix()); - if (!registry.isBeanNameInUse(targetManagedTypesBeanName)) { + if (!registry.isBeanNameInUse(repositoryAotProcessorBeanName)) { - // this needs to be lazy or we'd resolve types to early maybe - Supplier>> args = new Supplier>>() { + BeanDefinitionBuilder repositoryAotProcessor = BeanDefinitionBuilder + .rootBeanDefinition(extension.getRepositoryAotProcessor()) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - @Override - public Set> get() { + repositoryAotProcessor.addPropertyValue("configMap", metadataByRepositoryBeanName); - Set packages = metadataMap.values().stream().flatMap(it -> it.getBasePackages().stream()) - .collect(Collectors.toSet()); - return new TypeScanner(resourceLoader.getClassLoader()) - .scanForTypesAnnotatedWith(configExtensionSupport.getIdentifyingAnnotations()).inPackages(packages); - } - }; - - registry.registerBeanDefinition(targetManagedTypesBeanName, BeanDefinitionBuilder - .rootBeanDefinition(ManagedTypesBean.class).addConstructorArgValue(args).getBeanDefinition()); - } - } - } - - { // module specific repository post processor - String aotRepoPostProcessorBeanName = String.format("data-%s.repository-post-processor" /* might be duplicate */, - extension.getModulePrefix()); - - if (!registry.isBeanNameInUse(aotRepoPostProcessorBeanName)) { - - BeanDefinitionBuilder aotRepoPostProcessor = BeanDefinitionBuilder - .rootBeanDefinition(extension.getAotPostProcessor()); - aotRepoPostProcessor.addPropertyValue("configMap", metadataMap); - registry.registerBeanDefinition(aotRepoPostProcessorBeanName, aotRepoPostProcessor.getBeanDefinition()); - } + registry.registerBeanDefinition(repositoryAotProcessorBeanName, repositoryAotProcessor.getBeanDefinition()); } } @@ -280,7 +279,7 @@ public class RepositoryConfigurationDelegate { private static void potentiallyLazifyRepositories(Map> configurations, BeanDefinitionRegistry registry, BootstrapMode mode) { - if (!DefaultListableBeanFactory.class.isInstance(registry) || mode.equals(BootstrapMode.DEFAULT)) { + if (!DefaultListableBeanFactory.class.isInstance(registry) || BootstrapMode.DEFAULT.equals(mode)) { return; } @@ -315,7 +314,8 @@ public class RepositoryConfigurationDelegate { * than a single type is considered a multi-store configuration scenario which will trigger stricter repository * scanning. * - * @return + * @return {@literal true} if multiple data store repository implementations are present in the application. + * This typically means an Spring application is using more than 1 type of data store. */ private boolean multipleStoresDetected() { @@ -360,11 +360,12 @@ public class RepositoryConfigurationDelegate { } /** - * Returns a new {@link LazyRepositoryInjectionPointResolver} that will have its configurations augmented with the - * given ones. + * Returns a new {@link LazyRepositoryInjectionPointResolver} that will have its configurations augmented with + * the given ones. * * @param configurations must not be {@literal null}. - * @return + * @return a new {@link LazyRepositoryInjectionPointResolver} that will have its configurations augmented with + * the given ones. */ LazyRepositoryInjectionPointResolver withAdditionalConfigurations( Map> configurations) { @@ -398,14 +399,14 @@ public class RepositoryConfigurationDelegate { static class ManagedTypesBean implements ManagedTypes { - private Lazy>> types; + private final Lazy>> types; public ManagedTypesBean(Supplier>> types) { this.types = Lazy.of(types); } @Override - public void forEach(Consumer> action) { + public void forEach(@NonNull Consumer> action) { types.get().forEach(action); } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java index 213d2930d..37942bde2 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -18,12 +18,14 @@ package org.springframework.data.repository.config; import java.util.Collection; import java.util.Locale; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.generator.AotContributingBeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.io.ResourceLoader; -import org.springframework.data.aot.AotContributingRepositoryBeanPostProcessor; +import org.springframework.data.aot.RepositoryRegistrationAotProcessor; +import org.springframework.lang.NonNull; import org.springframework.util.StringUtils; /** @@ -32,6 +34,7 @@ import org.springframework.util.StringUtils; * @see RepositoryConfigurationExtensionSupport * @author Oliver Gierke * @author Christoph Strobl + * @author John Blum */ public interface RepositoryConfigurationExtension { @@ -57,13 +60,34 @@ public interface RepositoryConfigurationExtension { return StringUtils.capitalize(getModulePrefix()); } + /** + * Returns a {@link String prefix} identifying the module. + * + * @return a {@link String prefix} identifying the module. + * @see #getModuleName() + */ String getModulePrefix(); + /** + * Returns the {@link BeanRegistrationAotProcessor} type responsible for contributing AOT/native configuration + * required by the Spring Data Repository infrastructure components at native runtime. + * + * @return the {@link BeanRegistrationAotProcessor} type responsible for contributing AOT/native configuration. + * Defaults to {@link RepositoryRegistrationAotProcessor}. Must not be {@literal null}. + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor + * @since 3.0 + */ + @NonNull + default Class getRepositoryAotProcessor() { + return RepositoryRegistrationAotProcessor.class; + } + /** * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. * - * @param configSource - * @param loader + * @param configSource {@link RepositoryConfigurationSource} encapsulating the source (XML, Annotation) of + * the repository configuration. + * @param loader {@link ResourceLoader} used to load resources. * @param strictMatchesOnly whether to return strict repository matches only. Handing in {@literal true} will cause * the repository interfaces and domain types handled to be checked whether they are managed by the current * store. @@ -81,28 +105,27 @@ public interface RepositoryConfigurationExtension { String getDefaultNamedQueryLocation(); /** - * Returns the name of the repository factory class to be used. + * Returns the {@link String name} of the repository factory class to be used. * * @return will never be {@literal null}. */ String getRepositoryFactoryBeanClassName(); /** - * @return the {@link AotContributingBeanPostProcessor} type responsible for contributing AOT/native configuration. - * Defaults to {@link AotContributingRepositoryBeanPostProcessor}. Must not be {@literal null} - * @since 3.0 + * Returns the default location of the Spring Data named queries. + * + * @return must not be {@literal null} or empty. */ - default Class getAotPostProcessor() { - return AotContributingRepositoryBeanPostProcessor.class; - } + String getDefaultNamedQueryLocation(); /** * Callback to register additional bean definitions for a {@literal repositories} root node. This usually includes * beans you have to set up once independently of the number of repositories to be created. Will be called before any * repositories bean definitions have been registered. * - * @param registry - * @param configurationSource + * @param registry {@link BeanDefinitionRegistry} containing bean definitions. + * @param configurationSource {@link RepositoryConfigurationSource} encapsulating the source (e.g. XML, Annotation) + * of the repository configuration. */ void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource); @@ -130,4 +153,5 @@ public interface RepositoryConfigurationExtension { * @param config will never be {@literal null}. */ void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config); + } diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java index 8ce664743..37f9d9961 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryInformation.java @@ -27,19 +27,13 @@ import org.springframework.data.util.Streamable; public interface RepositoryInformation extends RepositoryMetadata { /** - * Returns the base class to be used to create the proxy backing instance. + * Returns whether the given method is logically a base class method. This also includes methods (re)declared in the + * repository interface that match the signatures of the base implementation. * + * @param method must not be {@literal null}. * @return */ - Class getRepositoryBaseClass(); - - /** - * Returns if the configured repository interface has custom methods, that might have to be delegated to a custom - * implementation. This is used to verify repository configuration. - * - * @return - */ - boolean hasCustomMethod(); + boolean isBaseClassMethod(Method method); /** * Returns whether the given method is a custom repository method. @@ -57,15 +51,6 @@ public interface RepositoryInformation extends RepositoryMetadata { */ boolean isQueryMethod(Method method); - /** - * Returns whether the given method is logically a base class method. This also includes methods (re)declared in the - * repository interface that match the signatures of the base implementation. - * - * @param method must not be {@literal null}. - * @return - */ - boolean isBaseClassMethod(Method method); - /** * Returns all methods considered to be query methods. * @@ -73,6 +58,13 @@ public interface RepositoryInformation extends RepositoryMetadata { */ Streamable getQueryMethods(); + /** + * Returns the base class to be used to create the proxy backing instance. + * + * @return + */ + Class getRepositoryBaseClass(); + /** * Returns the target class method that is backing the given method. This can be necessary if a repository interface * redeclares a method of the core repository interface (e.g. for transaction behavior customization). Returns the @@ -84,6 +76,16 @@ public interface RepositoryInformation extends RepositoryMetadata { */ Method getTargetClassMethod(Method method); + /** + * Returns if the configured repository interface has custom methods, that might have to be delegated to a custom + * implementation. This is used to verify repository configuration. + * + * @return + */ + default boolean hasCustomMethod() { + return getQueryMethods().stream().anyMatch(this::isCustomMethod); + } + default boolean hasQueryMethods() { return getQueryMethods().iterator().hasNext(); } diff --git a/src/main/resources/META-INF/spring/aot.factories b/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000..5d378a6dd --- /dev/null +++ b/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ + org.springframework.data.aot.SpringDataBeanFactoryInitializationAotProcessor diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java index 41a336f10..b9a67c980 100644 --- a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -15,106 +15,112 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.stream.Stream; import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.Assertions; -import org.springframework.aot.generator.CodeContribution; -import org.springframework.aot.generator.ProtectedAccess; + +import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.ClassProxyHint; import org.springframework.aot.hint.JdkProxyHint; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.javapoet.support.MultiStatement; /** + * AssertJ {@link AbstractAssert Assertion} for code contributions originating from + * Spring Data Repository infrastructure AOT processing. + * * @author Christoph Strobl - * @since 2022/04 + * @author John Blum + * @see org.assertj.core.api.AbstractAssert + * @see org.springframework.aot.generate.GenerationContext + * @since 3.0.0 */ -public class CodeContributionAssert extends AbstractAssert - implements CodeContribution { +public class CodeContributionAssert extends AbstractAssert { - public CodeContributionAssert(CodeContribution contribution) { + public CodeContributionAssert(GenerationContext contribution) { super(contribution, CodeContributionAssert.class); } - public CodeContributionAssert doesNotContributeReflectionFor(Class... types) { - - for (Class type : types) { - assertThat(this.actual.runtimeHints().reflection().getTypeHint(type)) - .describedAs("Reflection entry found for %s", type).isNull(); - } - return this; - } - public CodeContributionAssert contributesReflectionFor(Class... types) { for (Class type : types) { - assertThat(this.actual.runtimeHints().reflection().getTypeHint(type)) - .describedAs("No reflection entry found for %s", type).isNotNull(); + assertThat(this.actual.getRuntimeHints().reflection().getTypeHint(type)) + .describedAs("No reflection entry found for [%s]", type) + .isNotNull(); } + + return this; + } + + public CodeContributionAssert doesNotContributeReflectionFor(Class... types) { + + for (Class type : types) { + assertThat(this.actual.getRuntimeHints().reflection().getTypeHint(type)) + .describedAs("Reflection entry found for [%s]", type) + .isNull(); + } + return this; } public CodeContributionAssert contributesJdkProxyFor(Class entryPoint) { - assertThat(jdkProxiesFor(entryPoint).findFirst()).describedAs("No jdk proxy found for %s", entryPoint).isPresent(); + + assertThat(jdkProxiesFor(entryPoint).findFirst()) + .describedAs("No JDK proxy found for [%s]", entryPoint) + .isPresent(); + return this; } public CodeContributionAssert doesNotContributeJdkProxyFor(Class entryPoint) { - assertThat(jdkProxiesFor(entryPoint).findFirst()).describedAs("Found jdk proxy matching %s though it should not be present.", entryPoint).isNotPresent(); - return this; - } - public CodeContributionAssert doesNotContributeJdkProxy(Class... proxyInterfaces) { + assertThat(jdkProxiesFor(entryPoint).findFirst()) + .describedAs("Found JDK proxy matching [%s] though it should not be present", entryPoint) + .isNotPresent(); - assertThat(jdkProxiesFor(proxyInterfaces[0])).describedAs("Found jdk proxy matching %s though it should not be present.", Arrays.asList(proxyInterfaces)).noneSatisfy(it -> { - new JdkProxyAssert(it).matches(proxyInterfaces); - }); return this; } public CodeContributionAssert contributesJdkProxy(Class... proxyInterfaces) { - assertThat(jdkProxiesFor(proxyInterfaces[0])).describedAs("Unable to find jdk proxy matching %s", Arrays.asList(proxyInterfaces)).anySatisfy(it -> { - new JdkProxyAssert(it).matches(proxyInterfaces); - }); + assertThat(jdkProxiesFor(proxyInterfaces[0])) + .describedAs("Unable to find JDK proxy matching [%s]", Arrays.asList(proxyInterfaces)) + .anySatisfy(it -> new JdkProxyAssert(it).matches(proxyInterfaces)); + + return this; + } + + public CodeContributionAssert doesNotContributeJdkProxy(Class... proxyInterfaces) { + + assertThat(jdkProxiesFor(proxyInterfaces[0])) + .describedAs("Found JDK proxy matching [%s] though it should not be present", + Arrays.asList(proxyInterfaces)) + .noneSatisfy(it -> new JdkProxyAssert(it).matches(proxyInterfaces)); return this; } private Stream jdkProxiesFor(Class entryPoint) { - return this.actual.runtimeHints().proxies().jdkProxies().filter(jdkProxyHint -> { - return jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName().equals(entryPoint.getCanonicalName()); - }); + + return this.actual.getRuntimeHints().proxies().jdkProxies() + .filter(jdkProxyHint -> jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName() + .equals(entryPoint.getCanonicalName())); } public CodeContributionAssert contributesClassProxy(Class... proxyInterfaces) { - assertThat(classProxiesFor(proxyInterfaces[0])).describedAs("Unable to find jdk proxy matching %s", Arrays.asList(proxyInterfaces)).anySatisfy(it -> { - new ClassProxyAssert(it).matches(proxyInterfaces); - }); + assertThat(classProxiesFor(proxyInterfaces[0])) + .describedAs("Unable to find JDK proxy matching [%s]", Arrays.asList(proxyInterfaces)) + .anySatisfy(it -> new ClassProxyAssert(it).matches(proxyInterfaces)); return this; } private Stream classProxiesFor(Class entryPoint) { - return this.actual.runtimeHints().proxies().classProxies().filter(jdkProxyHint -> { - return jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName().equals(entryPoint.getCanonicalName()); - }); - } - public MultiStatement statements() { - return actual.statements(); - } - - public RuntimeHints runtimeHints() { - return actual.runtimeHints(); - } - - public ProtectedAccess protectedAccess() { - return actual.protectedAccess(); + return this.actual.getRuntimeHints().proxies().classProxies() + .filter(jdkProxyHint -> jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName() + .equals(entryPoint.getCanonicalName())); } } diff --git a/src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java b/src/test/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessorUnitTests.java similarity index 68% rename from src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java rename to src/test/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessorUnitTests.java index e8e98f777..69a40be3d 100644 --- a/src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/aot/ManagedTypesRegistrationAotProcessorUnitTests.java @@ -21,10 +21,13 @@ import java.util.Collections; import java.util.function.Consumer; import org.junit.jupiter.api.Test; -import org.springframework.aot.generator.DefaultCodeContribution; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.InMemoryGeneratedFiles; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.generator.BeanInstantiationContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -33,7 +36,7 @@ import org.springframework.data.ManagedTypes; /** * @author Christoph Strobl */ -class AotManagedTypesPostProcessorUnitTests { +class ManagedTypesRegistrationAotProcessorUnitTests { final RootBeanDefinition managedTypesDefinition = (RootBeanDefinition) BeanDefinitionBuilder .rootBeanDefinition(ManagedTypes.class).setFactoryMethod("of") @@ -45,7 +48,7 @@ class AotManagedTypesPostProcessorUnitTests { @Test // GH-2593 void processesBeanWithMatchingModulePrefix() { - BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + BeanRegistrationAotContribution contribution = createPostProcessor("commons", bf -> { bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); }).contribute(managedTypesDefinition, ManagedTypes.class, "commons.managed-types"); @@ -55,14 +58,16 @@ class AotManagedTypesPostProcessorUnitTests { @Test // GH-2593 void contributesReflectionForManagedTypes() { - BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + BeanRegistrationAotContribution contribution = createPostProcessor("commons", bf -> { bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); }).contribute(managedTypesDefinition, ManagedTypes.class, "commons.managed-types"); - DefaultCodeContribution codeContribution = new DefaultCodeContribution(new RuntimeHints()); - contribution.applyTo(codeContribution); + DefaultGenerationContext generationContext = new DefaultGenerationContext(new ClassNameGenerator(), + new InMemoryGeneratedFiles(), new RuntimeHints()); - new CodeContributionAssert(codeContribution) // + contribution.applyTo(generationContext, null); + + new CodeContributionAssert(generationContext) // .contributesReflectionFor(A.class) // .doesNotContributeReflectionFor(B.class); } @@ -70,7 +75,7 @@ class AotManagedTypesPostProcessorUnitTests { @Test // GH-2593 void processesMatchingSubtypeBean() { - BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + BeanRegistrationAotContribution contribution = createPostProcessor("commons", bf -> { bf.registerBeanDefinition("commons.managed-types", myManagedTypesDefinition); }).contribute(myManagedTypesDefinition, MyManagedTypes.class, "commons.managed-types"); @@ -80,7 +85,7 @@ class AotManagedTypesPostProcessorUnitTests { @Test // GH-2593 void ignoresBeanNotMatchingRequiredType() { - BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + BeanRegistrationAotContribution contribution = createPostProcessor("commons", bf -> { bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); }).contribute(managedTypesDefinition, Object.class, "commons.managed-types"); @@ -90,29 +95,29 @@ class AotManagedTypesPostProcessorUnitTests { @Test // GH-2593 void ignoresBeanNotMatchingPrefix() { - BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + BeanRegistrationAotContribution contribution = createPostProcessor("commons", bf -> { bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); }).contribute(managedTypesDefinition, ManagedTypes.class, "jpa.managed-types"); assertThat(contribution).isNull(); } - private AotManagedTypesPostProcessor createPostProcessor(String prefix, Consumer action) { + private ManagedTypesRegistrationAotProcessor createPostProcessor(String prefix, Consumer action) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); action.accept(beanFactory); - AotManagedTypesPostProcessor postProcessor = createPostProcessor(beanFactory); + ManagedTypesRegistrationAotProcessor postProcessor = createPostProcessor(beanFactory); postProcessor.setModulePrefix(prefix); return postProcessor; } - private AotManagedTypesPostProcessor createPostProcessor(BeanFactory beanFactory) { + private ManagedTypesRegistrationAotProcessor createPostProcessor(BeanFactory beanFactory) { - AotManagedTypesPostProcessor aotManagedTypesPostProcessor = new AotManagedTypesPostProcessor(); - aotManagedTypesPostProcessor.setBeanFactory(beanFactory); - return aotManagedTypesPostProcessor; + ManagedTypesRegistrationAotProcessor managedTypesRegistrationAotProcessor = new ManagedTypesRegistrationAotProcessor(); + managedTypesRegistrationAotProcessor.setBeanFactory(beanFactory); + return managedTypesRegistrationAotProcessor; } static class A {} diff --git a/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java b/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java deleted file mode 100644 index 49aa94be1..000000000 --- a/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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; - -import static org.assertj.core.api.Assertions.*; - -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.function.Consumer; - -import org.assertj.core.api.AbstractAssert; -import org.springframework.aot.generator.CodeContribution; -import org.springframework.aot.generator.DefaultCodeContribution; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.support.RepositoryFragment; - -/** - * @author Christoph Strobl - * @since 2022/04 - */ -public class RepositoryBeanContributionAssert - extends AbstractAssert { - - public RepositoryBeanContributionAssert(RepositoryBeanContribution actual) { - super(actual, RepositoryBeanContributionAssert.class); - } - - public static RepositoryBeanContributionAssert assertThatContribution(RepositoryBeanContribution actual) { - return new RepositoryBeanContributionAssert(actual); - } - - public RepositoryBeanContributionAssert targetRepositoryTypeIs(Class expected) { - - assertThat(getRepositoryInformation().getRepositoryInterface()).isEqualTo(expected); - return myself; - } - - public RepositoryBeanContributionAssert hasNoFragments() { - assertThat(getRepositoryInformation().getFragments()).isEmpty(); - return this; - } - - public RepositoryBeanContributionAssert hasFragments() { - - assertThat(getRepositoryInformation().getFragments()).isNotEmpty(); - return this; - } - - public RepositoryBeanContributionAssert verifyFragments(Consumer>> consumer) { - - assertThat(getRepositoryInformation().getFragments()).satisfies(it -> consumer.accept(new LinkedHashSet<>(it))); - return this; - } - - public RepositoryBeanContributionAssert codeContributionSatisfies(Consumer consumer) { - - DefaultCodeContribution codeContribution = new DefaultCodeContribution(new RuntimeHints()); - this.actual.applyTo(codeContribution); - consumer.accept(new CodeContributionAssert(codeContribution)); - return this; - } - - private RepositoryInformation getRepositoryInformation() { - assertThat(this.actual).describedAs("No repository interface found on null bean contribution.").isNotNull(); - assertThat(this.actual.getRepositoryInformation()) - .describedAs("No repository interface found on null repository information.").isNotNull(); - return this.actual.getRepositoryInformation(); - } - - - -} diff --git a/src/test/java/org/springframework/data/aot/RepositoryRegistrationAotContributionAssert.java b/src/test/java/org/springframework/data/aot/RepositoryRegistrationAotContributionAssert.java new file mode 100644 index 000000000..dc09f71e9 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/RepositoryRegistrationAotContributionAssert.java @@ -0,0 +1,116 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Consumer; + +import org.assertj.core.api.AbstractAssert; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.lang.NonNull; + +/** + * AssertJ {@link AbstractAssert Assertion} for {@link RepositoryRegistrationAotContribution}. + * + * @author Christoph Strobl + * @author John Blum + * @see org.mockito.Mockito + * @see org.assertj.core.api.AbstractAssert + * @see org.springframework.data.aot.RepositoryRegistrationAotContribution + * @since 3.0.0 + */ +public class RepositoryRegistrationAotContributionAssert + extends AbstractAssert { + + @NonNull + public static RepositoryRegistrationAotContributionAssert assertThatContribution( + @NonNull RepositoryRegistrationAotContribution actual) { + + return new RepositoryRegistrationAotContributionAssert(actual); + } + + public RepositoryRegistrationAotContributionAssert(@NonNull RepositoryRegistrationAotContribution actual) { + super(actual, RepositoryRegistrationAotContributionAssert.class); + } + + public RepositoryRegistrationAotContributionAssert targetRepositoryTypeIs(Class expected) { + + assertThat(getRepositoryInformation().getRepositoryInterface()).isEqualTo(expected); + + return this.myself; + } + + public RepositoryRegistrationAotContributionAssert hasNoFragments() { + + assertThat(getRepositoryInformation().getFragments()).isEmpty(); + + return this; + } + + public RepositoryRegistrationAotContributionAssert hasFragments() { + + assertThat(getRepositoryInformation().getFragments()).isNotEmpty(); + + return this; + } + + public RepositoryRegistrationAotContributionAssert verifyFragments(Consumer>> consumer) { + + assertThat(getRepositoryInformation().getFragments()) + .satisfies(it -> consumer.accept(new LinkedHashSet<>(it))); + + return this; + } + + public RepositoryRegistrationAotContributionAssert codeContributionSatisfies( + Consumer assertWith) { + + BeanRegistrationCode mockBeanRegistrationCode = mock(BeanRegistrationCode.class); + + DefaultGenerationContext generationContext = + new DefaultGenerationContext(new ClassNameGenerator(), new InMemoryGeneratedFiles(), new RuntimeHints()); + + this.actual.applyTo(generationContext, mockBeanRegistrationCode); + + assertWith.accept(new CodeContributionAssert(generationContext)); + + return this; + } + + private RepositoryInformation getRepositoryInformation() { + + assertThat(this.actual) + .describedAs("No repository interface found on null bean contribution") + .isNotNull(); + + assertThat(this.actual.getRepositoryInformation()) + .describedAs("No repository interface found on null repository information") + .isNotNull(); + + return this.actual.getRepositoryInformation(); + } +} diff --git a/src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java b/src/test/java/org/springframework/data/aot/RepositoryRegistrationAotProcessorIntegrationTests.java similarity index 74% rename from src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java rename to src/test/java/org/springframework/data/aot/RepositoryRegistrationAotProcessorIntegrationTests.java index 3f9666f16..3562d9cbf 100644 --- a/src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java +++ b/src/test/java/org/springframework/data/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -15,16 +15,18 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.aot.RepositoryBeanContributionAssert.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.aot.RepositoryRegistrationAotContributionAssert.assertThatContribution; import java.io.Serializable; import org.junit.jupiter.api.Test; + import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.DecoratingProxy; import org.springframework.core.annotation.SynthesizedAnnotation; @@ -46,14 +48,20 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.transaction.interceptor.TransactionalProxy; /** + * Integration Tests for {@link RepositoryRegistrationAotProcessor}. + * * @author Christoph Strobl + * @see org.junit.jupiter.api.Test + * @see org.springframework.data.aot.RepositoryRegistrationAotProcessor + * @see org.springframework.data.aot.RepositoryRegistrationAotContributionAssert + * @author John Blum */ -public class AotContributingRepositoryBeanPostProcessorTests { +public class RepositoryRegistrationAotProcessorIntegrationTests { @Test // GH-2593 void simpleRepositoryNoTxManagerNoKotlinNoReactiveNoComponent() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithSimpleCrudRepository.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithSimpleCrudRepository.class) .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); assertThatContribution(repositoryBeanContribution) // @@ -75,7 +83,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithTransactionManagerPresent.class).forRepository(ConfigWithTransactionManagerPresent.MyTxRepo.class); assertThatContribution(repositoryBeanContribution) // @@ -100,7 +108,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.class) .forRepository(ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class); @@ -134,7 +142,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void contributesFragmentsCorrectly() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithFragments.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithFragments.class) .forRepository(ConfigWithFragments.RepositoryWithFragments.class); assertThatContribution(repositoryBeanContribution) // @@ -167,7 +175,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void contributesCustomImplementationCorrectly() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithCustomImplementation.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithCustomImplementation.class) .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); assertThatContribution(repositoryBeanContribution) // @@ -187,21 +195,21 @@ public class AotContributingRepositoryBeanPostProcessorTests { } @Test // GH-2593 - void contributesDomainTypeAndReachablesCorrectly() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithSimpleCrudRepository.class) - .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); + void contributesDomainTypeAndReachableTypesCorrectly() { - assertThatContribution(repositoryBeanContribution) // - .codeContributionSatisfies(contribution -> { - contribution.contributesReflectionFor(ConfigWithSimpleCrudRepository.Person.class, - ConfigWithSimpleCrudRepository.Address.class); - }); + RepositoryRegistrationAotContribution repositoryBeanContribution = + computeAotConfiguration(ConfigWithSimpleCrudRepository.class) + .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); + + assertThatContribution(repositoryBeanContribution).codeContributionSatisfies(contribution -> + contribution.contributesReflectionFor(ConfigWithSimpleCrudRepository.Person.class, + ConfigWithSimpleCrudRepository.Address.class)); } @Test // GH-2593 void contributesReactiveRepositoryCorrectly() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ReactiveConfig.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ReactiveConfig.class) .forRepository(ReactiveConfig.CustomerRepositoryReactive.class); assertThatContribution(repositoryBeanContribution) // @@ -218,7 +226,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void contributesRepositoryBaseClassCorrectly() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomRepositoryBaseClass.class) .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); @@ -237,19 +245,18 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void contributesTypesFromQueryMethods() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithQueryMethods.class) .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); - assertThatContribution(repositoryBeanContribution) // - .codeContributionSatisfies(contribution -> { - contribution.contributesReflectionFor(ProjectionInterface.class); - }); + assertThatContribution(repositoryBeanContribution) + .codeContributionSatisfies(contribution -> + contribution.contributesReflectionFor(ProjectionInterface.class)); } @Test // GH-2593 void contributesProxiesForPotentialProjections() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithQueryMethods.class) .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); assertThatContribution(repositoryBeanContribution) // @@ -264,7 +271,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void contributesProxiesForDataAnnotations() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithQueryMethods.class) .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); assertThatContribution(repositoryBeanContribution) // @@ -279,7 +286,7 @@ public class AotContributingRepositoryBeanPostProcessorTests { @Test // GH-2593 void doesNotCareAboutNonDataAnnotations() { - RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithSimpleCrudRepository.class) + RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration(ConfigWithSimpleCrudRepository.class) .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); assertThatContribution(repositoryBeanContribution) // @@ -289,34 +296,46 @@ public class AotContributingRepositoryBeanPostProcessorTests { }); } - BeanContributionBuilder computeConfiguration(Class configuration, AnnotationConfigApplicationContext ctx) { + RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { + return computeAotConfiguration(configuration, new AnnotationConfigApplicationContext()); + } - ctx.register(configuration); - ctx.refreshForAotProcessing(); + RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration, + AnnotationConfigApplicationContext applicationContext) { - return it -> { + applicationContext.register(configuration); + applicationContext.refreshForAotProcessing(); - String[] repoBeanNames = ctx.getBeanNamesForType(it); - assertThat(repoBeanNames).describedAs("Unable to find repository %s in configuration %s.", it, configuration) + return repositoryType -> { + + String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); + + assertThat(repositoryBeanNames) + .describedAs("Unable to find repository [%s] in configuration [%s]", + repositoryType, configuration) .hasSize(1); - String beanName = repoBeanNames[0]; - BeanDefinition beanDefinition = ctx.getBeanDefinition(beanName); + String repositoryBeanName = repositoryBeanNames[0]; - AotContributingRepositoryBeanPostProcessor postProcessor = ctx - .getBean(AotContributingRepositoryBeanPostProcessor.class); + ConfigurableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); - postProcessor.setBeanFactory(ctx.getDefaultListableBeanFactory()); + RepositoryRegistrationAotProcessor repositoryAotProcessor = + applicationContext.getBean(RepositoryRegistrationAotProcessor.class); - return postProcessor.contribute((RootBeanDefinition) beanDefinition, it, beanName); + repositoryAotProcessor.setBeanFactory(beanFactory); + + RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); + + BeanRegistrationAotContribution beanContribution = repositoryAotProcessor.processAheadOfTime(bean); + + assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); + + return (RepositoryRegistrationAotContribution) beanContribution; }; } - BeanContributionBuilder computeConfiguration(Class configuration) { - return computeConfiguration(configuration, new AnnotationConfigApplicationContext()); - } - - interface BeanContributionBuilder { - RepositoryBeanContribution forRepository(Class repositoryInterface); + @FunctionalInterface + interface RepositoryRegistrationAotContributionBuilder { + RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); } } diff --git a/src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java b/src/test/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessorUnitTests.java similarity index 72% rename from src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java rename to src/test/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessorUnitTests.java index 977de6379..15a4219bf 100644 --- a/src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/aot/SpringDataBeanFactoryInitializationAotProcessorUnitTests.java @@ -15,15 +15,18 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import java.util.Collections; import java.util.LinkedHashSet; import java.util.function.Supplier; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -33,25 +36,28 @@ import org.springframework.data.ManagedTypes; /** * @author Christoph Strobl */ -class AotDataComponentsBeanFactoryPostProcessorUnitTests { +class SpringDataBeanFactoryInitializationAotProcessorUnitTests { @Test // Gh-2593 - void replacesManagedTypesBeanDefinitionUsingSupplierForCtorValue() { + @SuppressWarnings("all") + void replacesManagedTypesBeanDefinitionUsingSupplierForConstructorValue() { Supplier>> typesSupplier = mock(Supplier.class); - Mockito.when(typesSupplier.get()).thenReturn(Collections.singleton(DomainType.class)); + doReturn(Collections.singleton(DomainType.class)).when(typesSupplier).get(); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("data.managed-types", BeanDefinitionBuilder .rootBeanDefinition(ManagedTypes.class).addConstructorArgValue(typesSupplier).getBeanDefinition()); - new AotDataComponentsBeanFactoryPostProcessor().contribute(beanFactory); + new SpringDataBeanFactoryInitializationAotProcessor().processAheadOfTime(beanFactory); assertThat(beanFactory.getBeanNamesForType(ManagedTypes.class)).hasSize(1); verify(typesSupplier).get(); BeanDefinition beanDefinition = beanFactory.getBeanDefinition("data.managed-types"); + assertThat(beanDefinition.getFactoryMethodName()).isEqualTo("of"); assertThat(beanDefinition.hasConstructorArgumentValues()).isTrue(); assertThat(beanDefinition.getConstructorArgumentValues().getArgumentValue(0, null).getValue()) @@ -59,21 +65,23 @@ class AotDataComponentsBeanFactoryPostProcessorUnitTests { } @Test // Gh-2593 - void leavesManagedTypesBeanDefinitionNotUsingSupplierForCtorValue() { + void leavesManagedTypesBeanDefinitionNotUsingSupplierForConstructorValue() { Iterable> types = spy(new LinkedHashSet<>(Collections.singleton(DomainType.class))); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - AbstractBeanDefinition sourceBD = BeanDefinitionBuilder.rootBeanDefinition(ManagedTypes.class) - .addConstructorArgValue(types).getBeanDefinition(); - beanFactory.registerBeanDefinition("data.managed-types", sourceBD); - new AotDataComponentsBeanFactoryPostProcessor().contribute(beanFactory); + AbstractBeanDefinition sourceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(ManagedTypes.class) + .addConstructorArgValue(types).getBeanDefinition(); + + beanFactory.registerBeanDefinition("data.managed-types", sourceBeanDefinition); + + new SpringDataBeanFactoryInitializationAotProcessor().processAheadOfTime(beanFactory); assertThat(beanFactory.getBeanNamesForType(ManagedTypes.class)).hasSize(1); verifyNoInteractions(types); - assertThat(beanFactory.getBeanDefinition("data.managed-types")).isSameAs(sourceBD); + assertThat(beanFactory.getBeanDefinition("data.managed-types")).isSameAs(sourceBeanDefinition); } private static class DomainType {} diff --git a/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java b/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java index 1524c34a1..5ad7b3a3e 100644 --- a/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java +++ b/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java @@ -18,6 +18,7 @@ package org.springframework.data.aot.types; /** * @author Christoph Strobl */ +@SuppressWarnings("unused") public class TypesInMethodSignatures { TypesInMethodSignatures(String ctorArg) { @@ -40,8 +41,6 @@ public class TypesInMethodSignatures { } - - Object methodArg(Integer methodArg) { return null; }