diff --git a/src/main/java/org/springframework/data/ManagedTypes.java b/src/main/java/org/springframework/data/ManagedTypes.java new file mode 100644 index 000000000..33edb47ba --- /dev/null +++ b/src/main/java/org/springframework/data/ManagedTypes.java @@ -0,0 +1,64 @@ +/* + * 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; + +import java.util.ArrayList; +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; + +/** + * Types managed by a Spring Data implementation. Used to predefine a set of know entities that might need processing + * during container/repository initialization phase. + * + * @author Christoph Strobl + * @since 3.0 + */ +public interface ManagedTypes { + + void forEach(Consumer> action); + + default List> toList() { + + List> tmp = new ArrayList<>(100); + forEach(tmp::add); + return tmp; + } + + static ManagedTypes of(Iterable> types) { + return types::forEach; + } + + static ManagedTypes of(Stream> types) { + return types::forEach; + } + + static ManagedTypes o(Supplier>> dataProvider) { + + return new ManagedTypes() { + + Lazy>> lazyProvider = Lazy.of(dataProvider); + + @Override + public void forEach(Consumer> action) { + lazyProvider.get().forEach(action); + } + }; + } +} diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java new file mode 100644 index 000000000..e94e9f0d8 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -0,0 +1,146 @@ +/* + * 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.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +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.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! + * + * @author Christoph Strobl + */ +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}. + */ + static AotContext context(BeanFactory beanFactory) { + + Assert.notNull(beanFactory, "BeanFactory must not be null!"); + + return new AotContext() { + + private final ConfigurableListableBeanFactory bf = beanFactory instanceof ConfigurableListableBeanFactory + ? (ConfigurableListableBeanFactory) beanFactory + : new DefaultListableBeanFactory(beanFactory); + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return bf; + } + }; + } + + ConfigurableListableBeanFactory getBeanFactory(); + + default ClassLoader getClassLoader() { + return getBeanFactory().getBeanClassLoader(); + } + + default boolean isTypePresent(String typeName) { + return ClassUtils.isPresent(typeName, getBeanFactory().getBeanClassLoader()); + } + + default TypeScanner getTypeScanner() { + return new TypeScanner(getClassLoader()); + } + + default Set> scanPackageForTypes(Collection> identifyingAnnotations, + Collection packageNames) { + return getTypeScanner().scanForTypesAnnotatedWith(identifyingAnnotations).inPackages(packageNames); + } + + default Optional> resolveType(String typeName) { + + 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); + } + } + + @Nullable + default Class resolveType(BeanReference beanReference) { + return getBeanFactory().getType(beanReference.getBeanName(), false); + } + + default BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + return getBeanFactory().getBeanDefinition(beanName); + } + + default RootBeanDefinition getRootBeanDefinition(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)); + } + return RootBeanDefinition.class.cast(val); + } + + default boolean isFactoryBean(String beanName) { + return getBeanFactory().isFactoryBean(beanName); + } + + default boolean isTransactionManagerPresent() { + + return resolveType("org.springframework.transaction.TransactionManager") // + .map(it -> !ObjectUtils.isEmpty(getBeanFactory().getBeanNamesForType(it))) // + .orElse(false); + } + + default void ifTypePresent(String typeName, Consumer> action) { + resolveType(typeName).ifPresent(action); + } + + default void ifTransactionManagerPresent(Consumer beanNamesConsumer) { + + ifTypePresent("org.springframework.transaction.TransactionManager", txMgrType -> { + String[] txMgrBeanNames = getBeanFactory().getBeanNamesForType(txMgrType); + if (!ObjectUtils.isEmpty(txMgrBeanNames)) { + beanNamesConsumer.accept(txMgrBeanNames); + } + }); + } +} diff --git a/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java b/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java new file mode 100644 index 000000000..e8ec1a266 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessor.java @@ -0,0 +1,189 @@ +/* + * 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/AotDataComponentsBeanFactoryPostProcessor.java b/src/main/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessor.java new file mode 100644 index 000000000..e77760358 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessor.java @@ -0,0 +1,69 @@ +/* + * 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.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.Nullable; + +/** + * {@link AotContributingBeanFactoryPostProcessor} implementation to capture common data infrastructure concerns. + * + * @author Christoph Strobl + * @since 3.0 + */ +public class AotDataComponentsBeanFactoryPostProcessor implements AotContributingBeanFactoryPostProcessor { + + private static final Log logger = LogFactory.getLog(AotContributingBeanFactoryPostProcessor.class); + + @Nullable + @Override + public BeanFactoryContribution contribute(ConfigurableListableBeanFactory beanFactory) { + postProcessManagedTypes(beanFactory); + return null; + } + + private void postProcessManagedTypes(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); + if (argumentValue.getValue() instanceof Supplier supplier) { + + if (logger.isDebugEnabled()) { + logger.info(String.format("Replacing ManagedType bean definition %s.", beanName)); + } + + registry.removeBeanDefinition(beanName); + registry.registerBeanDefinition(beanName, BeanDefinitionBuilder.rootBeanDefinition(ManagedTypes.class) + .setFactoryMethod("of").addConstructorArgValue(supplier.get()).getBeanDefinition()); + } + } + } + } +} diff --git a/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java b/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java new file mode 100644 index 000000000..5f1a50622 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotManagedTypesPostProcessor.java @@ -0,0 +1,150 @@ +/* + * 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 new file mode 100644 index 000000000..094ceb662 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotRepositoryContext.java @@ -0,0 +1,58 @@ +/* + * 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.Set; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.repository.core.RepositoryInformation; + +/** + * @author Christoph Strobl + */ +public interface AotRepositoryContext extends AotContext { + + /** + * @return the bean name of the repository / factory bean + */ + String getBeanName(); + + /** + * @return base packages to look 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. + */ + 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 new file mode 100644 index 000000000..762ab9362 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotRepositoryInformation.java @@ -0,0 +1,76 @@ +/* + * 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.reflect.Method; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Supplier; + +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; + +/** + * {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time. + * + * @author Christoph Strobl + * @since 3.0 + */ +class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { + + private final Supplier>> fragments; + + AotRepositoryInformation(Supplier repositoryMetadata, Supplier> repositoryBaseClass, + Supplier>> fragments) { + + super(repositoryMetadata, repositoryBaseClass); + this.fragments = fragments; + } + + @Override + public boolean isCustomMethod(Method method) { + + // TODO: + return false; + } + + @Override + public boolean isBaseClassMethod(Method method) { + // TODO + return false; + } + + @Override + public Method getTargetClassMethod(Method method) { + + // TODO + return method; + } + + /** + * @return + * @since 3.0 + */ + @Nullable + 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/DefaultRepositoryContext.java new file mode 100644 index 000000000..715013897 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/DefaultRepositoryContext.java @@ -0,0 +1,132 @@ +/* + * 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.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.aot.TypeScanner.Scanner; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.util.Lazy; + +/** + * Default implementation of {@link AotRepositoryContext} + * + * @author Christoph Strobl + * @since 3.0 + */ +class DefaultRepositoryContext implements AotRepositoryContext { + + private AotContext aotContext; + + private String beanName; + private java.util.Set basePackages; + private RepositoryInformation repositoryInformation; + 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(); + } + + public AotContext getAotContext() { + return aotContext; + } + + public void setAotContext(AotContext aotContext) { + this.aotContext = aotContext; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public void setBasePackages(Set basePackages) { + this.basePackages = basePackages; + } + + public void setRepositoryInformation(RepositoryInformation repositoryInformation) { + this.repositoryInformation = repositoryInformation; + } + + public void setIdentifyingAnnotations(Set> identifyingAnnotations) { + this.identifyingAnnotations = identifyingAnnotations; + } + + protected Set> discoverAnnotations() { + + Set> annotations = new LinkedHashSet<>(getResolvedTypes().stream().flatMap(type -> { + return TypeUtils.resolveUsedAnnotations(type).stream(); + }).collect(Collectors.toSet())); + annotations.addAll(TypeUtils.resolveUsedAnnotations(repositoryInformation.getRepositoryInterface())); + return annotations; + } + + protected Set> discoverTypes() { + + Set> types = new LinkedHashSet<>(TypeCollector.inspect(repositoryInformation.getDomainType()).list()); + + repositoryInformation.getQueryMethods() + .flatMap(it -> TypeUtils.resolveTypesInSignature(repositoryInformation.getRepositoryInterface(), it).stream()) + .flatMap(it -> TypeCollector.inspect(it).list().stream()).forEach(types::add); + + if (!getIdentifyingAnnotations().isEmpty()) { + + Scanner typeScanner = aotContext.getTypeScanner().scanForTypesAnnotatedWith(getIdentifyingAnnotations()); + Set> classes = typeScanner.inPackages(getBasePackages()); + TypeCollector.inspect(classes).list().stream().forEach(types::add); + } + + // context.get + return types; + } +} diff --git a/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java b/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java new file mode 100644 index 000000000..d397f0bcb --- /dev/null +++ b/src/main/java/org/springframework/data/aot/RepositoryBeanContribution.java @@ -0,0 +1,185 @@ +/* + * 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 new file mode 100644 index 000000000..7ea4023e9 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/RepositoryBeanDefinitionReader.java @@ -0,0 +1,88 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.data.repository.config.RepositoryFragmentConfiguration; +import org.springframework.data.repository.config.RepositoryMetadata; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Lazy; +import org.springframework.util.ClassUtils; + +/** + * Reader that allows to extract {@link RepositoryInformation} from metadata. + * + * @author Christoph Strobl + * @since 3.0 + */ +class RepositoryBeanDefinitionReader { + + static RepositoryInformation readRepositoryInformation(RepositoryMetadata metadata, + ConfigurableListableBeanFactory beanFactory) { + + return new AotRepositoryInformation(metadataSupplier(metadata, beanFactory), + repositoryBaseClass(metadata, beanFactory), fragments(metadata, beanFactory)); + } + + 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 fragments.stream(); + }).collect(Collectors.toList())); + } + + private static Supplier> repositoryBaseClass(RepositoryMetadata metadata, + ConfigurableListableBeanFactory 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; + })); + } + + static Supplier metadataSupplier( + 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); + } + } +} diff --git a/src/main/java/org/springframework/data/aot/TypeCollector.java b/src/main/java/org/springframework/data/aot/TypeCollector.java new file mode 100644 index 000000000..2c17ffc1b --- /dev/null +++ b/src/main/java/org/springframework/data/aot/TypeCollector.java @@ -0,0 +1,243 @@ +/* + * Copyright 2019-2021 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.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +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.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * @author Christoph Strobl + * @author Sebastien Deleuze + */ +public class TypeCollector { + + private static 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); + }; + + 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 Predicate fieldFilter = (field) -> { + if (field.isSynthetic() | field.getName().startsWith("$$_hibernate")) { + return false; + } + if (field.getDeclaringClass().getPackageName().startsWith("java.")) { + return false; + } + return true; + }; + + public TypeCollector filterFields(Predicate filter) { + this.fieldFilter = filter.and(filter); + return this; + } + + public TypeCollector filterTypes(Predicate> filter) { + this.typeFilter = this.typeFilter.and(filter); + return this; + } + + /** + * Inspect the given type and resolve those reachable via fields, methods, generics, ... + * + * @param types the types to inspect + * @return a type model collector for the type + */ + public static ReachableTypes inspect(Class... types) { + return inspect(Arrays.asList(types)); + } + + public static ReachableTypes inspect(Collection> types) { + return new ReachableTypes(new TypeCollector(), types); + } + + private void process(Class root, Consumer consumer) { + processType(ResolvableType.forType(root), new InspectionCache(), consumer); + } + + private void processType(ResolvableType type, InspectionCache cache, Consumer callback) { + + if (ResolvableType.NONE.equals(type) || cache.contains(type)) { + return; + } + + cache.add(type); + + // continue inspection but only add those matching the filter criteria to the result + if (typeFilter.test(type.toClass())) { + callback.accept(type); + } + + Set additionalTypes = new LinkedHashSet<>(); + additionalTypes.addAll(TypeUtils.resolveTypesInSignature(type)); + additionalTypes.addAll(visitConstructorsOfType(type)); + additionalTypes.addAll(visitMethodsOfType(type)); + additionalTypes.addAll(visitFieldsOfType(type)); + if (!ObjectUtils.isEmpty(type.toClass().getDeclaredClasses())) { + additionalTypes.addAll(Arrays.asList(type.toClass().getDeclaredClasses())); + } + for (Type discoveredType : additionalTypes) { + processType(ResolvableType.forType(discoveredType, type), cache, callback); + } + } + + Set visitConstructorsOfType(ResolvableType type) { + if (!typeFilter.test(type.toClass())) { + return Collections.emptySet(); + } + Set discoveredTypes = new LinkedHashSet<>(); + for (Constructor constructor : type.toClass().getDeclaredConstructors()) { + for (Class signatureType : TypeUtils.resolveTypesInSignature(type.toClass(), constructor)) { + if (typeFilter.test(signatureType)) { + discoveredTypes.add(signatureType); + } + } + } + return new HashSet<>(discoveredTypes); + } + + Set visitMethodsOfType(ResolvableType type) { + if (!typeFilter.test(type.toClass())) { + return Collections.emptySet(); + } + + Set discoveredTypes = new LinkedHashSet<>(); + try { + ReflectionUtils.doWithLocalMethods(type.toClass(), method -> { + if (!methodFilter.test(method)) { + return; + } + for (Class signatureType : TypeUtils.resolveTypesInSignature(type.toClass(), method)) { + if (typeFilter.test(signatureType)) { + discoveredTypes.add(signatureType); + } + } + }); + } catch (Exception ex) { + logger.warn(ex); + } + return new HashSet<>(discoveredTypes); + } + + Set visitFieldsOfType(ResolvableType type) { + Set discoveredTypes = new LinkedHashSet<>(); + ReflectionUtils.doWithLocalFields(type.toClass(), field -> { + if (!fieldFilter.test(field)) { + return; + } + for (Class signatureType : TypeUtils.resolveTypesInSignature(ResolvableType.forField(field, type))) { + if (typeFilter.test(signatureType)) { + discoveredTypes.add(signatureType); + } + } + }); + return discoveredTypes; + } + + public static class ReachableTypes { + + private TypeCollector typeCollector; + private final Iterable> roots; + private final Lazy>> reachableTypes = Lazy.of(this::collect); + + public ReachableTypes(TypeCollector typeCollector, Iterable> roots) { + + this.typeCollector = typeCollector; + this.roots = roots; + } + + public void forEach(Consumer consumer) { + roots.forEach(it -> typeCollector.process(it, consumer)); + } + + public List> list() { + return reachableTypes.get(); + } + + private List> collect() { + List> target = new ArrayList<>(); + forEach(it -> target.add(it.toClass())); + return target; + } + } + + static class InspectionCache { + + private final Map mutableCache = new LinkedHashMap<>(); + + public void add(ResolvableType resolvableType) { + mutableCache.put(resolvableType.toString(), resolvableType); + } + + public boolean contains(ResolvableType key) { + return mutableCache.containsKey(key.toString()); + } + + public int size() { + return mutableCache.size(); + } + + public boolean isEmpty() { + return mutableCache.isEmpty(); + } + + public void clear() { + mutableCache.clear(); + } + } +} diff --git a/src/main/java/org/springframework/data/aot/TypeContributor.java b/src/main/java/org/springframework/data/aot/TypeContributor.java new file mode 100644 index 000000000..b77a3fa0f --- /dev/null +++ b/src/main/java/org/springframework/data/aot/TypeContributor.java @@ -0,0 +1,108 @@ +/* + * 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.Set; +import java.util.function.Predicate; + +import org.springframework.aot.generator.CodeContribution; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.SynthesizedAnnotation; + +/** + * @author Christoph Strobl + * @since 3.0 + */ +class TypeContributor { + + public static final String DATA_NAMESPACE = "org.springframework.data"; + + /** + * Contribute the type with default reflection configuration, skip annotations. + * + * @param type + * @param contribution + */ + static void contribute(Class type, CodeContribution contribution) { + contribute(type, Collections.emptySet(), contribution); + } + + /** + * Contribute the type with default reflection configuration and only include matching annotations. + * + * @param type + * @param filter + * @param contribution + */ + static void contribute(Class type, Predicate> filter, CodeContribution contribution) { + + if (type.isPrimitive()) { + return; + } + + if (type.isAnnotation() && filter.test((Class) type)) { + + contribution.runtimeHints().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); + } + return; + } + + if (type.isInterface()) { + contribution.runtimeHints().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); + }); + } + + /** + * Contribute the type with default reflection configuration and only include annotations from a certain namespace and + * those meta annotated with one of them. + * + * @param type + * @param annotationNamespaces + * @param contribution + */ + static void contribute(Class type, Set annotationNamespaces, CodeContribution contribution) { + contribute(type, it -> isPartOfOrMetaAnnotatedWith(it, annotationNamespaces), contribution); + } + + private static boolean isPartOf(Class type, Set namespaces) { + return namespaces.stream().anyMatch(namespace -> type.getPackageName().startsWith(namespace)); + } + + protected static boolean isPartOfOrMetaAnnotatedWith(Class annotation, Set namespaces) { + + if (isPartOf(annotation, namespaces)) { + return true; + } + + return MergedAnnotation.of(annotation).getMetaTypes().stream().anyMatch(it -> isPartOf(annotation, namespaces)); + } +} diff --git a/src/main/java/org/springframework/data/aot/TypeScanner.java b/src/main/java/org/springframework/data/aot/TypeScanner.java new file mode 100644 index 000000000..cdd06b055 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/TypeScanner.java @@ -0,0 +1,107 @@ +/* + * 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.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; + +/** + * @author Christoph Strobl + */ +public class TypeScanner { // TODO: replace this with AnnotatedTypeScanner maybe? + + private final ClassLoader classLoader; + + public TypeScanner(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + static TypeScanner scanner(ClassLoader classLoader) { + return new TypeScanner(classLoader); + } + + public Scanner scanForTypesAnnotatedWith(Class... annotations) { + return scanForTypesAnnotatedWith(Arrays.asList(annotations)); + } + + public Scanner scanForTypesAnnotatedWith(Collection> annotations) { + return new ScannerImpl().includeTypesAnnotatedWith(annotations); + } + + public interface Scanner { + + default Set> inPackages(String... packageNames) { + return inPackages(Arrays.asList(packageNames)); + } + + Set> inPackages(Collection packageNames); + } + + class ScannerImpl implements Scanner { + + ClassPathScanningCandidateComponentProvider componentProvider; + + public ScannerImpl() { + + componentProvider = new ClassPathScanningCandidateComponentProvider(false); + componentProvider.setEnvironment(new StandardEnvironment()); + componentProvider.setResourceLoader(new DefaultResourceLoader(classLoader)); + } + + ScannerImpl includeTypesAnnotatedWith(Collection> annotations) { + annotations.stream().map(AnnotationTypeFilter::new).forEach(componentProvider::addIncludeFilter); + return this; + } + + @Override + public Set> inPackages(Collection packageNames) { + + Set> types = new LinkedHashSet<>(); + + packageNames.forEach(pkg -> { + componentProvider.findCandidateComponents(pkg).forEach(it -> { + resolveType(it.getBeanClassName()).ifPresent(types::add); + }); + }); + + return types; + } + } + + 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 + } + 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 new file mode 100644 index 000000000..47cda5cb3 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/TypeUtils.java @@ -0,0 +1,253 @@ +/* + * Copyright 2019-2021 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.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationFilter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class TypeUtils { + + /** + * Resolve ALL annotations present for a given type. Will inspect type, constructors, parameters, methods, fields,... + * + * @param type + * @return never {@literal null}. + */ + public static Set> resolveUsedAnnotations(Class type) { + + Set> annotations = new LinkedHashSet<>(); + annotations.addAll(TypeUtils.resolveAnnotationsFor(type).collect(Collectors.toSet())); + for (Constructor ctor : type.getDeclaredConstructors()) { + annotations.addAll(TypeUtils.resolveAnnotationsFor(ctor).collect(Collectors.toSet())); + for (Parameter parameter : ctor.getParameters()) { + annotations.addAll(TypeUtils.resolveAnnotationsFor(parameter).collect(Collectors.toSet())); + } + } + for (Field field : type.getDeclaredFields()) { + annotations.addAll(TypeUtils.resolveAnnotationsFor(field).collect(Collectors.toSet())); + } + try { + for (Method method : type.getDeclaredMethods()) { + annotations.addAll(TypeUtils.resolveAnnotationsFor(method).collect(Collectors.toSet())); + for (Parameter parameter : method.getParameters()) { + annotations.addAll(TypeUtils.resolveAnnotationsFor(parameter).collect(Collectors.toSet())); + } + } + } catch (NoClassDefFoundError e) { + // ignore an move on + } + return annotations; + } + + public static Stream> resolveAnnotationsFor(AnnotatedElement element) { + return resolveAnnotationsFor(element, AnnotationFilter.PLAIN); + } + + public static Stream> resolveAnnotationsFor(AnnotatedElement element, + AnnotationFilter filter) { + return MergedAnnotations + .from(element, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.standardRepeatables(), filter).stream(); + } + + public static Set> resolveAnnotationTypesFor(AnnotatedElement element, AnnotationFilter filter) { + return MergedAnnotations + .from(element, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.standardRepeatables(), filter).stream() + .map(MergedAnnotation::getType).collect(Collectors.toSet()); + } + + public static Set> resolveAnnotationTypesFor(AnnotatedElement element) { + return resolveAnnotationTypesFor(element, AnnotationFilter.PLAIN); + } + + public static boolean isAnnotationFromOrMetaAnnotated(Class annotation, String prefix) { + if (annotation.getPackage().getName().startsWith(prefix)) { + return true; + } + return TypeUtils.resolveAnnotationsFor(annotation) + .anyMatch(it -> it.getType().getPackage().getName().startsWith(prefix)); + } + + public static boolean hasAnnotatedField(Class type, String annotationName) { + + for (Field field : type.getDeclaredFields()) { + MergedAnnotations fieldAnnotations = MergedAnnotations.from(field); + boolean hasAnnotation = fieldAnnotations.get(annotationName).isPresent(); + if (hasAnnotation) { + return true; + } + } + return false; + } + + public static Set getAnnotatedField(Class type, String annotationName) { + + Set fields = new LinkedHashSet<>(); + for (Field field : type.getDeclaredFields()) { + if (MergedAnnotations.from(field).get(annotationName).isPresent()) { + fields.add(field); + } + } + return fields; + } + + public static Set> resolveTypesInSignature(Class owner, Method method) { + Set> signature = new LinkedHashSet<>(); + signature.addAll(resolveTypesInSignature(ResolvableType.forMethodReturnType(method, owner))); + for (Parameter parameter : method.getParameters()) { + signature + .addAll(resolveTypesInSignature(ResolvableType.forMethodParameter(MethodParameter.forParameter(parameter)))); + } + return signature; + } + + public static Set> resolveTypesInSignature(Class owner, Constructor constructor) { + Set> signature = new LinkedHashSet<>(); + for (int i = 0; i < constructor.getParameterCount(); i++) { + signature.addAll(resolveTypesInSignature(ResolvableType.forConstructorParameter(constructor, i, owner))); + } + return signature; + } + + public static Set> resolveTypesInSignature(Class root) { + Set> signature = new LinkedHashSet<>(); + resolveTypesInSignature(ResolvableType.forClass(root), signature); + return signature; + } + + public static Set> resolveTypesInSignature(ResolvableType root) { + Set> signature = new LinkedHashSet<>(); + resolveTypesInSignature(root, signature); + return signature; + } + + private static void resolveTypesInSignature(ResolvableType current, Set> signatures) { + + if (ResolvableType.NONE.equals(current) || ObjectUtils.nullSafeEquals(Void.TYPE, current.getType()) + || ObjectUtils.nullSafeEquals(Object.class, current.getType())) { + return; + } + if (signatures.contains(current.toClass())) { + return; + } + signatures.add(current.toClass()); + resolveTypesInSignature(current.getSuperType(), signatures); + for (ResolvableType type : current.getGenerics()) { + resolveTypesInSignature(type, signatures); + } + for (ResolvableType type : current.getInterfaces()) { + resolveTypesInSignature(type, signatures); + } + } + + public static TypeOps type(Class type) { + return new TypeOpsImpl(type); + } + + public interface TypeOps { + + Class getType(); + + default boolean isPartOf(String... packageNames) { + return isPartOf(TypeUtils.PackageFilter.of(packageNames)); + } + + default boolean isPartOf(PackageFilter... packageFilters) { + for (PackageFilter filter : packageFilters) { + if (filter.matches(getType().getName())) { + return true; + } + } + return false; + } + + default Set> signatureTypes() { + return TypeUtils.resolveTypesInSignature(getType()); + } + + interface PackageFilter { + + default boolean matches(Class type) { + return matches(type.getName()); + } + + boolean matches(String typeName); + + static PackageFilter of(String... packages) { + return TypeUtils.PackageFilter.of(packages); + } + } + } + + private static class TypeOpsImpl implements TypeOps { + + private Class type; + + TypeOpsImpl(Class type) { + this.type = type; + } + + public Class getType() { + return type; + } + } + + private static class PackageFilter implements TypeOps.PackageFilter { + + Set packageNames; + + PackageFilter(Set packageNames) { + this.packageNames = packageNames; + } + + static PackageFilter of(String... packageNames) { + Set target = new LinkedHashSet<>(); + for (String pkgName : packageNames) { + target.add(pkgName.endsWith(".") ? pkgName : (pkgName + '.')); + } + return new PackageFilter(target); + } + + @Override + public boolean matches(String typeName) { + for (String pgkName : packageNames) { + if (typeName.startsWith(pgkName)) { + return true; + } + } + return false; + } + } +} diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index 0b133286e..464106731 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -17,6 +17,8 @@ package org.springframework.data.repository.config; import static org.springframework.beans.factory.config.BeanDefinition.*; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -130,6 +132,44 @@ class RepositoryBeanDefinitionBuilder { return builder; } + // TODO: merge that with the one that creates the BD + RepositoryMetadata buildMetadata(RepositoryConfiguration configuration) { + + ImplementationDetectionConfiguration config = configuration + .toImplementationDetectionConfiguration(metadataReaderFactory); + + List repositoryFragmentConfigurationStream = fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) // + .map(it -> detectRepositoryFragmentConfiguration(it, config)) // + .flatMap(Optionals::toStream) + .collect(Collectors.toList());// + + if(repositoryFragmentConfigurationStream.isEmpty()) { + + ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); + + if(beanDefinition.isPresent()) { + repositoryFragmentConfigurationStream = new ArrayList<>(1); + + List interfaceNames = fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()).collect(Collectors.toList()); + String implClassName = beanDefinition.get().getBeanClassName(); + + try { + for (String iName : metadataReaderFactory.getMetadataReader(implClassName).getClassMetadata().getInterfaceNames()) { + if(interfaceNames.contains(iName)) { + repositoryFragmentConfigurationStream.add(new RepositoryFragmentConfiguration(iName, implClassName)); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + return new RepositoryMetadata(configuration, repositoryFragmentConfigurationStream); + } + private Optional registerCustomImplementation(RepositoryConfiguration configuration) { ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory); 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 ee964cb4e..73fcddc31 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -21,10 +21,14 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; 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.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -43,7 +47,12 @@ import org.springframework.core.io.support.SpringFactoriesLoader; 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.Nullable; import org.springframework.util.Assert; import org.springframework.util.StopWatch; @@ -65,7 +74,7 @@ public class RepositoryConfigurationDelegate { private static final String MULTIPLE_MODULES = "Multiple Spring Data modules found, entering strict repository configuration mode"; private static final String NON_DEFAULT_AUTOWIRE_CANDIDATE_RESOLVER = "Non-default AutowireCandidateResolver (%s) detected. Skipping the registration of LazyRepositoryInjectionPointResolver. Lazy repository injection will not be working"; - static final String FACTORY_BEAN_OBJECT_TYPE = "factoryBeanObjectType"; + static final String FACTORY_BEAN_OBJECT_TYPE = FactoryBean.OBJECT_TYPE_ATTRIBUTE; // "factoryBeanObjectType"; private static final Log logger = LogFactory.getLog(RepositoryConfigurationDelegate.class); @@ -159,6 +168,7 @@ public class RepositoryConfigurationDelegate { .getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode); Map> configurationsByRepositoryName = new HashMap<>(configurations.size()); + Map> metadataMap = new HashMap<>(configurations.size()); for (RepositoryConfiguration configuration : configurations) { @@ -180,12 +190,13 @@ public class RepositoryConfigurationDelegate { String beanName = configurationSource.generateBeanName(beanDefinition); if (logger.isTraceEnabled()) { - logger.trace(LogMessage.format(REPOSITORY_REGISTRATION, extension.getModuleName(), beanName, configuration.getRepositoryInterface(), - configuration.getRepositoryFactoryBeanClassName())); + logger.trace(LogMessage.format(REPOSITORY_REGISTRATION, extension.getModuleName(), beanName, + configuration.getRepositoryInterface(), configuration.getRepositoryFactoryBeanClassName())); } - beanDefinition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, configuration.getRepositoryInterface()); + metadataMap.put(beanName, builder.buildMetadata(configuration)); + beanDefinition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, configuration.getRepositoryInterface()); registry.registerBeanDefinition(beanName, beanDefinition); definitions.add(new BeanComponentDefinition(beanDefinition, beanName)); } @@ -198,13 +209,66 @@ public class RepositoryConfigurationDelegate { 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); + return definitions; } + private void registerAotComponents(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, + Map> metadataMap) { + + { // 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 configExtensionSupport) { + + 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 = new Supplier>>() { + + @Override + public Set> get() { + + 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()); + } + } + } + /** * Registers a {@link LazyRepositoryInjectionPointResolver} over the default * {@link ContextAnnotationAutowireCandidateResolver} to make injection points of lazy repositories lazy, too. Will @@ -325,10 +389,25 @@ public class RepositoryConfigurationDelegate { boolean lazyInit = configuration.isLazyInit(); if (lazyInit) { - logger.debug(LogMessage.format("Creating lazy injection proxy for %s…", configuration.getRepositoryInterface())); + logger + .debug(LogMessage.format("Creating lazy injection proxy for %s…", configuration.getRepositoryInterface())); } return lazyInit; } + + static class ManagedTypesBean implements ManagedTypes { + + private Lazy>> types; + + public ManagedTypesBean(Supplier>> types) { + this.types = Lazy.of(types); + } + + @Override + public void forEach(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 27001ae36..213d2930d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -19,9 +19,12 @@ import java.util.Collection; import java.util.Locale; 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.util.StringUtils; /** * SPI to implement store specific extension to the repository bean definition registration process. @@ -50,7 +53,11 @@ public interface RepositoryConfigurationExtension { * * @return will never be {@literal null}. */ - String getModuleName(); + default String getModuleName() { + return StringUtils.capitalize(getModulePrefix()); + } + + String getModulePrefix(); /** * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. @@ -80,6 +87,15 @@ public interface RepositoryConfigurationExtension { */ 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 + */ + default Class getAotPostProcessor() { + return AotContributingRepositoryBeanPostProcessor.class; + } + /** * 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 diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java index adad5c6a6..7d8169c1d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -59,11 +59,6 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit private boolean noMultiStoreSupport = false; - @Override - public String getModuleName() { - return StringUtils.capitalize(getModulePrefix()); - } - public Collection> getRepositoryConfigurations( T configSource, ResourceLoader loader) { return getRepositoryConfigurations(configSource, loader, false); @@ -132,7 +127,7 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit * @return * @since 1.9 */ - protected Collection> getIdentifyingAnnotations() { + public Collection> getIdentifyingAnnotations() { return Collections.emptySet(); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryMetadata.java b/src/main/java/org/springframework/data/repository/config/RepositoryMetadata.java new file mode 100644 index 000000000..8516ff437 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/RepositoryMetadata.java @@ -0,0 +1,122 @@ +/* + * 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.repository.config; + +import java.util.List; +import java.util.Optional; + +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 3.0 + */ +public class RepositoryMetadata implements RepositoryConfiguration { + + RepositoryConfiguration repositoryConfiguration; + List fragmentConfiguration; + + public RepositoryMetadata(RepositoryConfiguration repositoryConfiguration, + List fragmentConfiguration) { + + this.repositoryConfiguration = repositoryConfiguration; + this.fragmentConfiguration = fragmentConfiguration; + } + + @Override + public Streamable getBasePackages() { + return repositoryConfiguration.getBasePackages(); + } + + @Override + public Streamable getImplementationBasePackages() { + return repositoryConfiguration.getImplementationBasePackages(); + } + + @Override + public String getRepositoryInterface() { + return repositoryConfiguration.getRepositoryInterface(); + } + + @Override + public Object getQueryLookupStrategyKey() { + return repositoryConfiguration.getQueryLookupStrategyKey(); + } + + @Override + public Optional getNamedQueriesLocation() { + return repositoryConfiguration.getNamedQueriesLocation(); + } + + @Override + public Optional getRepositoryBaseClassName() { + return repositoryConfiguration.getRepositoryBaseClassName(); + } + + @Override + public String getRepositoryFactoryBeanClassName() { + return repositoryConfiguration.getRepositoryFactoryBeanClassName(); + } + + @Override + @Nullable + public Object getSource() { + return repositoryConfiguration.getSource(); + } + + @Override + public T getConfigurationSource() { + return repositoryConfiguration.getConfigurationSource(); + } + + @Override + public boolean isLazyInit() { + return repositoryConfiguration.isLazyInit(); + } + + @Override + public boolean isPrimary() { + return repositoryConfiguration.isPrimary(); + } + + @Override + public Streamable getExcludeFilters() { + return repositoryConfiguration.getExcludeFilters(); + } + + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + return repositoryConfiguration.toImplementationDetectionConfiguration(factory); + } + + @Override + public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { + return repositoryConfiguration.toLookupConfiguration(factory); + } + + @Override + @Nullable + public String getResourceDescription() { + return repositoryConfiguration.getResourceDescription(); + } + + public List getFragmentConfiguration() { + return fragmentConfiguration; + } +} diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java b/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java new file mode 100644 index 000000000..c5e3876b4 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/RepositoryInformationSupport.java @@ -0,0 +1,182 @@ +/* + * 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.repository.core; + +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Common base class for {@link RepositoryInformation} that delays resolution of {@link RepositoryMetadata} and the + * repository base to the latest possible time. + * + * @author Christoph Strobl + * @since 3.0 + */ +public abstract class RepositoryInformationSupport implements RepositoryInformation { + + private final Supplier metadata; + private final Supplier> repositoryBaseClass; + + public RepositoryInformationSupport(Supplier metadata, Supplier> repositoryBaseClass) { + + Assert.notNull(metadata, "Repository metadata must not be null!"); + Assert.notNull(repositoryBaseClass, "Repository base class must not be null!"); + + this.metadata = Lazy.of(metadata); + this.repositoryBaseClass = Lazy.of(repositoryBaseClass); + } + + @Override + public Streamable getQueryMethods() { + + Set result = new HashSet<>(); + + for (Method method : getRepositoryInterface().getMethods()) { + method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); + if (isQueryMethodCandidate(method)) { + result.add(method); + } + } + + return Streamable.of(Collections.unmodifiableSet(result)); + } + + private RepositoryMetadata getMetadata() { + return metadata.get(); + } + + @Override + public Class getIdType() { + return getMetadata().getIdType(); + } + + @Override + public Class getDomainType() { + return getMetadata().getDomainType(); + } + + @Override + public Class getRepositoryInterface() { + return getMetadata().getRepositoryInterface(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return getMetadata().getReturnType(method); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return getMetadata().getReturnedDomainClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return getMetadata().getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return getMetadata().isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return getMetadata().getAlternativeDomainTypes(); + } + + @Override + public boolean isReactiveRepository() { + return getMetadata().isReactiveRepository(); + } + + @Override + public Class getRepositoryBaseClass() { + return repositoryBaseClass.get(); + } + + @Override + public boolean isQueryMethod(Method method) { + return getQueryMethods().stream().anyMatch(it -> it.equals(method)); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return getMetadata().getDomainTypeInformation(); + } + + @Override + public TypeInformation getIdTypeInformation() { + return getMetadata().getIdTypeInformation(); + } + + @Override + public boolean hasCustomMethod() { + + Class repositoryInterface = getRepositoryInterface(); + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + /** + * Checks whether the given method contains a custom store specific query annotation annotated with + * {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation. + * + * @param method + * @return + */ + protected boolean isQueryAnnotationPresentOn(Method method) { + + return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; + } + + /** + * Checks whether the given method is a query method candidate. + * + * @param method + * @return + */ + protected boolean isQueryMethodCandidate(Method method) { + return !method.isBridge() && !method.isDefault() // + && !Modifier.isStatic(method.getModifiers()) // + && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); + } +} diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryMetadata.java b/src/main/java/org/springframework/data/repository/core/RepositoryMetadata.java index 8786ec1e1..9788e65f7 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryMetadata.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMetadata.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Set; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.support.Repositories; import org.springframework.data.util.TypeInformation; @@ -126,4 +127,12 @@ public interface RepositoryMetadata { * @since 2.0 */ boolean isReactiveRepository(); + + /** + * + * @return + * @since 3.0 + * + */ + Set> getFragments(); } diff --git a/src/main/java/org/springframework/data/repository/core/support/AnnotationRepositoryMetadata.java b/src/main/java/org/springframework/data/repository/core/support/AnnotationRepositoryMetadata.java index 946a6a460..85aa7fc9c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/AnnotationRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/repository/core/support/AnnotationRepositoryMetadata.java @@ -16,6 +16,8 @@ package org.springframework.data.repository.core.support; import java.util.function.Function; +import java.util.Collections; +import java.util.Set; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.repository.RepositoryDefinition; @@ -79,4 +81,9 @@ public class AnnotationRepositoryMetadata extends AbstractRepositoryMetadata { return TypeInformation.of(extractor.apply(annotation)); } + + @Override + public Set> getFragments() { + return Collections.emptySet(); + } } diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index e427877e0..35d880abc 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -15,26 +15,19 @@ */ package org.springframework.data.repository.core.support; -import static org.springframework.data.repository.util.ClassUtils.*; import static org.springframework.util.ReflectionUtils.*; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.data.annotation.QueryAnnotation; -import org.springframework.data.repository.core.CrudMethods; 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.util.Streamable; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Default implementation of {@link RepositoryInformation}. @@ -45,12 +38,10 @@ import org.springframework.util.ClassUtils; * @author Christoph Strobl * @author Alessandro Nistico */ -class DefaultRepositoryInformation implements RepositoryInformation { +class DefaultRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { private final Map methodCache = new ConcurrentHashMap<>(); - private final RepositoryMetadata metadata; - private final Class repositoryBaseClass; private final RepositoryComposition composition; private final RepositoryComposition baseComposition; @@ -64,33 +55,15 @@ class DefaultRepositoryInformation implements RepositoryInformation { public DefaultRepositoryInformation(RepositoryMetadata metadata, Class repositoryBaseClass, RepositoryComposition composition) { - Assert.notNull(metadata, "Repository metadata must not be null"); - Assert.notNull(repositoryBaseClass, "Repository base class must not be null"); + super(() -> metadata, () -> repositoryBaseClass); Assert.notNull(composition, "Repository composition must not be null"); - this.metadata = metadata; - this.repositoryBaseClass = repositoryBaseClass; this.composition = composition; this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // .withArgumentConverter(composition.getArgumentConverter()) // .withMethodLookup(composition.getMethodLookup()); } - @Override - public TypeInformation getDomainTypeInformation() { - return metadata.getDomainTypeInformation(); - } - - @Override - public TypeInformation getIdTypeInformation() { - return metadata.getIdTypeInformation(); - } - - @Override - public Class getRepositoryBaseClass() { - return this.repositoryBaseClass; - } - @Override public Method getTargetClassMethod(Method method) { @@ -117,55 +90,11 @@ class DefaultRepositoryInformation implements RepositoryInformation { return value; } - @Override - public Streamable getQueryMethods() { - - Set result = new HashSet<>(); - - for (Method method : getRepositoryInterface().getMethods()) { - method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); - if (isQueryMethodCandidate(method)) { - result.add(method); - } - } - - return Streamable.of(Collections.unmodifiableSet(result)); - } - - /** - * Checks whether the given method is a query method candidate. - * - * @param method - * @return - */ - private boolean isQueryMethodCandidate(Method method) { - return !method.isBridge() && !method.isDefault() // - && !Modifier.isStatic(method.getModifiers()) // - && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); - } - - /** - * Checks whether the given method contains a custom store specific query annotation annotated with - * {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation. - * - * @param method - * @return - */ - private boolean isQueryAnnotationPresentOn(Method method) { - - return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; - } - @Override public boolean isCustomMethod(Method method) { return composition.getMethod(method) != null; } - @Override - public boolean isQueryMethod(Method method) { - return getQueryMethods().stream().anyMatch(it -> it.equals(method)); - } - @Override public boolean isBaseClassMethod(Method method) { @@ -173,57 +102,15 @@ class DefaultRepositoryInformation implements RepositoryInformation { return baseComposition.getMethod(method) != null; } - @Override - public boolean hasCustomMethod() { - - Class repositoryInterface = getRepositoryInterface(); - - // No detection required if no typing interface was configured - if (isGenericRepositoryInterface(repositoryInterface)) { - return false; - } - - for (Method method : repositoryInterface.getMethods()) { - if (isCustomMethod(method) && !isBaseClassMethod(method)) { - return true; - } - } - - return false; - } - - @Override - public Class getRepositoryInterface() { - return metadata.getRepositoryInterface(); - } - - @Override - public Class getReturnedDomainClass(Method method) { - return metadata.getReturnedDomainClass(method); - } - - @Override - public TypeInformation getReturnType(Method method) { - return metadata.getReturnType(method); - } - - @Override - public CrudMethods getCrudMethods() { - return metadata.getCrudMethods(); - } - - @Override - public boolean isPagingRepository() { - return metadata.isPagingRepository(); - } - - @Override - public Set> getAlternativeDomainTypes() { - return metadata.getAlternativeDomainTypes(); + /** + * + * @return + * @since 3.0 + * + */ + @Nullable + public Set> getFragments() { + return composition.getFragments().toSet(); } - @Override - public boolean isReactiveRepository() { - return metadata.isReactiveRepository(); - } } diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMetadata.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMetadata.java index 3f0ab9d9e..1f70e4e91 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMetadata.java @@ -15,7 +15,9 @@ */ package org.springframework.data.repository.core.support; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Supplier; import org.springframework.data.repository.Repository; @@ -79,4 +81,9 @@ public class DefaultRepositoryMetadata extends AbstractRepositoryMetadata { return arguments.get(index); } + + @Override + public Set> getFragments() { + return Collections.emptySet(); + } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java index 54ed4ceca..683a9ce90 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java @@ -193,7 +193,13 @@ public interface RepositoryFragment { @SuppressWarnings({ "rawtypes", "unchecked" }) public Class getSignatureContributor() { - return interfaceClass.orElse((Class) implementation.getClass()); + return interfaceClass.orElseGet(() -> { + + if(implementation instanceof Class type) { + return type; + } + return (Class) implementation.getClass(); + }); } @Override diff --git a/src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java b/src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java new file mode 100644 index 000000000..3f9666f16 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotContributingRepositoryBeanPostProcessorTests.java @@ -0,0 +1,322 @@ +/* + * 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 static org.springframework.data.aot.RepositoryBeanContributionAssert.*; + +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.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.annotation.SynthesizedAnnotation; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.aot.sample.ConfigWithCustomImplementation; +import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass; +import org.springframework.data.aot.sample.ConfigWithFragments; +import org.springframework.data.aot.sample.ConfigWithQueryMethods; +import org.springframework.data.aot.sample.ConfigWithQueryMethods.ProjectionInterface; +import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository; +import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresent; +import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty; +import org.springframework.data.aot.sample.ReactiveConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; +import org.springframework.transaction.interceptor.TransactionalProxy; + +/** + * @author Christoph Strobl + */ +public class AotContributingRepositoryBeanPostProcessorTests { + + @Test // GH-2593 + void simpleRepositoryNoTxManagerNoKotlinNoReactiveNoComponent() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithSimpleCrudRepository.class) + .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ConfigWithSimpleCrudRepository.MyRepo.class) // + .hasNoFragments() // + .codeContributionSatisfies(contribution -> { // + contribution.contributesReflectionFor(ConfigWithSimpleCrudRepository.MyRepo.class) // repository interface + .contributesReflectionFor(PagingAndSortingRepository.class) // base repository + .contributesReflectionFor(ConfigWithSimpleCrudRepository.Person.class) // repository domain type + .contributesJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, SpringProxy.class, Advised.class, + DecoratingProxy.class) // + .doesNotContributeJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, + TransactionalProxy.class, Advised.class, DecoratingProxy.class) + .doesNotContributeJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, + TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + }); + } + + @Test // GH-2593 + void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + ConfigWithTransactionManagerPresent.class).forRepository(ConfigWithTransactionManagerPresent.MyTxRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ConfigWithTransactionManagerPresent.MyTxRepo.class) // + .hasNoFragments() // + .codeContributionSatisfies(contribution -> { // + contribution.contributesReflectionFor(ConfigWithTransactionManagerPresent.MyTxRepo.class) // repository + // interface + .contributesReflectionFor(PagingAndSortingRepository.class) // base repository + .contributesReflectionFor(ConfigWithTransactionManagerPresent.Person.class) // repository domain type + + // proxies + .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, SpringProxy.class, Advised.class, + DecoratingProxy.class) + .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, + TransactionalProxy.class, Advised.class, DecoratingProxy.class) + .doesNotContributeJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, + TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + }); + } + + @Test // GH-2593 + void simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.class) + .forRepository(ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class) // + .hasNoFragments() // + .codeContributionSatisfies(contribution -> { // + contribution + .contributesReflectionFor( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class) // repository + // interface + .contributesReflectionFor(PagingAndSortingRepository.class) // base repository + .contributesReflectionFor( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.Person.class) // repository domain + // type + + // proxies + .contributesJdkProxy( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class, + SpringProxy.class, Advised.class, DecoratingProxy.class) + .contributesJdkProxy( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class, + Repository.class, TransactionalProxy.class, Advised.class, DecoratingProxy.class) + .contributesJdkProxy( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class, + Repository.class, TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + }); + } + + @Test // GH-2593 + void contributesFragmentsCorrectly() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithFragments.class) + .forRepository(ConfigWithFragments.RepositoryWithFragments.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ConfigWithFragments.RepositoryWithFragments.class) // + .hasFragments() // + .codeContributionSatisfies(contribution -> { // + contribution.contributesReflectionFor(ConfigWithFragments.RepositoryWithFragments.class) // repository + // interface + .contributesReflectionFor(PagingAndSortingRepository.class) // base repository + .contributesReflectionFor(ConfigWithFragments.Person.class) // repository domain type + + // fragments + .contributesReflectionFor(ConfigWithFragments.CustomImplInterface1.class, + ConfigWithFragments.CustomImplInterface1Impl.class) + .contributesReflectionFor(ConfigWithFragments.CustomImplInterface2.class, + ConfigWithFragments.CustomImplInterface2Impl.class) + + // proxies + .contributesJdkProxy(ConfigWithFragments.RepositoryWithFragments.class, SpringProxy.class, Advised.class, + DecoratingProxy.class) + .doesNotContributeJdkProxy( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class, + Repository.class, TransactionalProxy.class, Advised.class, DecoratingProxy.class) + .doesNotContributeJdkProxy( + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo.class, + Repository.class, TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + }); + } + + @Test // GH-2593 + void contributesCustomImplementationCorrectly() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithCustomImplementation.class) + .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class) // + .hasFragments() // + .codeContributionSatisfies(contribution -> { // + contribution.contributesReflectionFor(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class) // repository + // interface + .contributesReflectionFor(PagingAndSortingRepository.class) // base repository + .contributesReflectionFor(ConfigWithCustomImplementation.Person.class) // repository domain type + + // fragments + .contributesReflectionFor(ConfigWithCustomImplementation.CustomImplInterface.class, + ConfigWithCustomImplementation.RepositoryWithCustomImplementationImpl.class); + + }); + } + + @Test // GH-2593 + void contributesDomainTypeAndReachablesCorrectly() { + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(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) + .forRepository(ReactiveConfig.CustomerRepositoryReactive.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ReactiveConfig.CustomerRepositoryReactive.class) // + .hasNoFragments() // + .codeContributionSatisfies(contribution -> { // + // interface + contribution.contributesReflectionFor(ReactiveConfig.CustomerRepositoryReactive.class) // repository + .contributesReflectionFor(ReactiveSortingRepository.class) // base repo class + .contributesReflectionFor(ReactiveConfig.Person.class); // repository domain type + }); + } + + @Test // GH-2593 + void contributesRepositoryBaseClassCorrectly() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration( + ConfigWithCustomRepositoryBaseClass.class) + .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // + .hasNoFragments() // + .codeContributionSatisfies(contribution -> { // + // interface + contribution + .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository + .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class + .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type + }); + } + + @Test // GH-2593 + void contributesTypesFromQueryMethods() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor(ProjectionInterface.class); + }); + } + + @Test // GH-2593 + void contributesProxiesForPotentialProjections() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + + contribution.contributesJdkProxyFor(ProjectionInterface.class); + contribution.doesNotContributeJdkProxyFor(Page.class); + contribution.doesNotContributeJdkProxyFor(ConfigWithQueryMethods.Person.class); + }); + } + + @Test // GH-2593 + void contributesProxiesForDataAnnotations() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithQueryMethods.class) + .forRepository(ConfigWithQueryMethods.CustomerRepositoryWithQueryMethods.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + + contribution.contributesJdkProxy(Param.class, SynthesizedAnnotation.class); + contribution.contributesJdkProxy(ConfigWithQueryMethods.CustomQuery.class, SynthesizedAnnotation.class); + contribution.contributesJdkProxy(QueryAnnotation.class, SynthesizedAnnotation.class); + }); + } + + @Test // GH-2593 + void doesNotCareAboutNonDataAnnotations() { + + RepositoryBeanContribution repositoryBeanContribution = computeConfiguration(ConfigWithSimpleCrudRepository.class) + .forRepository(ConfigWithSimpleCrudRepository.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.doesNotContributeReflectionFor(javax.annotation.Nullable.class); + contribution.doesNotContributeJdkProxyFor(javax.annotation.Nullable.class); + }); + } + + BeanContributionBuilder computeConfiguration(Class configuration, AnnotationConfigApplicationContext ctx) { + + ctx.register(configuration); + ctx.refreshForAotProcessing(); + + return it -> { + + String[] repoBeanNames = ctx.getBeanNamesForType(it); + assertThat(repoBeanNames).describedAs("Unable to find repository %s in configuration %s.", it, configuration) + .hasSize(1); + + String beanName = repoBeanNames[0]; + BeanDefinition beanDefinition = ctx.getBeanDefinition(beanName); + + AotContributingRepositoryBeanPostProcessor postProcessor = ctx + .getBean(AotContributingRepositoryBeanPostProcessor.class); + + postProcessor.setBeanFactory(ctx.getDefaultListableBeanFactory()); + + return postProcessor.contribute((RootBeanDefinition) beanDefinition, it, beanName); + }; + } + + BeanContributionBuilder computeConfiguration(Class configuration) { + return computeConfiguration(configuration, new AnnotationConfigApplicationContext()); + } + + interface BeanContributionBuilder { + RepositoryBeanContribution forRepository(Class repositoryInterface); + } +} diff --git a/src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java b/src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java new file mode 100644 index 000000000..977de6379 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotDataComponentsBeanFactoryPostProcessorUnitTests.java @@ -0,0 +1,81 @@ +/* + * 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 static org.mockito.Mockito.*; + +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; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.data.ManagedTypes; + +/** + * @author Christoph Strobl + */ +class AotDataComponentsBeanFactoryPostProcessorUnitTests { + + @Test // Gh-2593 + void replacesManagedTypesBeanDefinitionUsingSupplierForCtorValue() { + + Supplier>> typesSupplier = mock(Supplier.class); + + Mockito.when(typesSupplier.get()).thenReturn(Collections.singleton(DomainType.class)); + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("data.managed-types", BeanDefinitionBuilder + .rootBeanDefinition(ManagedTypes.class).addConstructorArgValue(typesSupplier).getBeanDefinition()); + + new AotDataComponentsBeanFactoryPostProcessor().contribute(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()) + .isEqualTo(Collections.singleton(DomainType.class)); + } + + @Test // Gh-2593 + void leavesManagedTypesBeanDefinitionNotUsingSupplierForCtorValue() { + + 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); + + assertThat(beanFactory.getBeanNamesForType(ManagedTypes.class)).hasSize(1); + verifyNoInteractions(types); + + assertThat(beanFactory.getBeanDefinition("data.managed-types")).isSameAs(sourceBD); + } + + private static class DomainType {} + +} diff --git a/src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java b/src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java new file mode 100644 index 000000000..e8e98f777 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotManagedTypesPostProcessorUnitTests.java @@ -0,0 +1,129 @@ +/* + * 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.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.generator.DefaultCodeContribution; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.generator.BeanInstantiationContribution; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.data.ManagedTypes; + +/** + * @author Christoph Strobl + */ +class AotManagedTypesPostProcessorUnitTests { + + final RootBeanDefinition managedTypesDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(ManagedTypes.class).setFactoryMethod("of") + .addConstructorArgValue(Collections.singleton(A.class)).getBeanDefinition(); + + final RootBeanDefinition myManagedTypesDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(MyManagedTypes.class).getBeanDefinition(); + + @Test // GH-2593 + void processesBeanWithMatchingModulePrefix() { + + BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); + }).contribute(managedTypesDefinition, ManagedTypes.class, "commons.managed-types"); + + assertThat(contribution).isNotNull(); + } + + @Test // GH-2593 + void contributesReflectionForManagedTypes() { + + BeanInstantiationContribution 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); + + new CodeContributionAssert(codeContribution) // + .contributesReflectionFor(A.class) // + .doesNotContributeReflectionFor(B.class); + } + + @Test // GH-2593 + void processesMatchingSubtypeBean() { + + BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + bf.registerBeanDefinition("commons.managed-types", myManagedTypesDefinition); + }).contribute(myManagedTypesDefinition, MyManagedTypes.class, "commons.managed-types"); + + assertThat(contribution).isNotNull(); + } + + @Test // GH-2593 + void ignoresBeanNotMatchingRequiredType() { + + BeanInstantiationContribution contribution = createPostProcessor("commons", bf -> { + bf.registerBeanDefinition("commons.managed-types", managedTypesDefinition); + }).contribute(managedTypesDefinition, Object.class, "commons.managed-types"); + + assertThat(contribution).isNull(); + } + + @Test // GH-2593 + void ignoresBeanNotMatchingPrefix() { + + BeanInstantiationContribution 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) { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + action.accept(beanFactory); + + AotManagedTypesPostProcessor postProcessor = createPostProcessor(beanFactory); + postProcessor.setModulePrefix(prefix); + + return postProcessor; + } + + private AotManagedTypesPostProcessor createPostProcessor(BeanFactory beanFactory) { + + AotManagedTypesPostProcessor aotManagedTypesPostProcessor = new AotManagedTypesPostProcessor(); + aotManagedTypesPostProcessor.setBeanFactory(beanFactory); + return aotManagedTypesPostProcessor; + } + + static class A {} + + static class B {} + + static class MyManagedTypes implements ManagedTypes { + + @Override + public void forEach(Consumer> action) { + // just do nothing ¯\_(ツ)_/¯ + } + } +} diff --git a/src/test/java/org/springframework/data/aot/ClassProxyAssert.java b/src/test/java/org/springframework/data/aot/ClassProxyAssert.java new file mode 100644 index 000000000..7f124fa48 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/ClassProxyAssert.java @@ -0,0 +1,45 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.assertj.core.api.AbstractAssert; +import org.springframework.aot.hint.ClassProxyHint; +import org.springframework.aot.hint.TypeReference; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +public class ClassProxyAssert extends AbstractAssert { + + protected ClassProxyAssert(ClassProxyHint classProxyHint) { + super(classProxyHint, ClassProxyAssert.class); + } + + public void matches(Class... proxyInterfaces) { + assertThat(actual.getProxiedInterfaces().stream().map(TypeReference::getCanonicalName)) + .containsExactly(Arrays.stream(proxyInterfaces).map(Class::getCanonicalName).toArray(String[]::new)); + } + + public List getProxiedInterfaces() { + return actual.getProxiedInterfaces(); + } +} diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java new file mode 100644 index 000000000..41a336f10 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -0,0 +1,120 @@ +/* + * 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.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.hint.ClassProxyHint; +import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.javapoet.support.MultiStatement; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +public class CodeContributionAssert extends AbstractAssert + implements CodeContribution { + + public CodeContributionAssert(CodeContribution 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(); + } + return this; + } + + public CodeContributionAssert contributesJdkProxyFor(Class entryPoint) { + 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(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); + }); + + return this; + } + + private Stream jdkProxiesFor(Class entryPoint) { + return this.actual.runtimeHints().proxies().jdkProxies().filter(jdkProxyHint -> { + return 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); + }); + + 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(); + } +} diff --git a/src/test/java/org/springframework/data/aot/JdkProxyAssert.java b/src/test/java/org/springframework/data/aot/JdkProxyAssert.java new file mode 100644 index 000000000..1b4489fa2 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/JdkProxyAssert.java @@ -0,0 +1,46 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.assertj.core.api.AbstractAssert; +import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.TypeReference; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +public class JdkProxyAssert extends AbstractAssert { + + public JdkProxyAssert(JdkProxyHint jdkProxyHint) { + super(jdkProxyHint, JdkProxyAssert.class); + } + + public void matches(Class... proxyInterfaces) { + assertThat(actual.getProxiedInterfaces().stream().map(TypeReference::getCanonicalName)) + .containsExactly(Arrays.stream(proxyInterfaces).map(Class::getCanonicalName).toArray(String[]::new)); + } + + public List getProxiedInterfaces() { + return actual.getProxiedInterfaces(); + } + +} diff --git a/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java b/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java new file mode 100644 index 000000000..49aa94be1 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/RepositoryBeanContributionAssert.java @@ -0,0 +1,86 @@ +/* + * 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/TypeCollectorUnitTests.java b/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java new file mode 100644 index 000000000..639171525 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/TypeCollectorUnitTests.java @@ -0,0 +1,63 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.springframework.data.aot.types.*; + +/** + * @author Christoph Strobl + */ +public class TypeCollectorUnitTests { + + @Test // GH-2593 + void detectsSignatureTypes() { + assertThat(TypeCollector.inspect(FieldsAndMethods.class).list()).containsExactlyInAnyOrder(FieldsAndMethods.class, + AbstractType.class, InterfaceType.class); + } + + @Test // GH-2593 + void detectsMethodArgs() { + assertThat(TypeCollector.inspect(TypesInMethodSignatures.class).list()) + .containsExactlyInAnyOrder(TypesInMethodSignatures.class, EmptyType1.class, EmptyType2.class); + } + + @Test // GH-2593 + void doesNotOverflowOnCyclicPropertyReferences() { + assertThat(TypeCollector.inspect(CyclicPropertiesA.class).list()).containsExactlyInAnyOrder(CyclicPropertiesA.class, + CyclicPropertiesB.class); + } + + @Test + void doesNotOverflowOnCyclicSelfReferences() { + assertThat(TypeCollector.inspect(CyclicPropertiesSelf.class).list()) + .containsExactlyInAnyOrder(CyclicPropertiesSelf.class); + } + + @Test + void doesNotOverflowOnCyclicGenericsReferences() { + assertThat(TypeCollector.inspect(CyclicGenerics.class).list()).containsExactlyInAnyOrder(CyclicGenerics.class); + } + + @Test + void includesDeclaredClassesInInspection() { + assertThat(TypeCollector.inspect(WithDeclaredClass.class).list()).containsExactlyInAnyOrder(WithDeclaredClass.class, + WithDeclaredClass.SomeEnum.class); + } + +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomFactoryBeanBaseClass.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomFactoryBeanBaseClass.java new file mode 100644 index 000000000..1ed57f035 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomFactoryBeanBaseClass.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.sample.ConfigWithCustomFactoryBeanBaseClass.MyFixedRepoFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableRepositories(repositoryFactoryBeanClass = MyFixedRepoFactory.class, considerNestedRepositories = true, + includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*FixedFactoryRepository") }) +public class ConfigWithCustomFactoryBeanBaseClass { + + public interface FixedFactoryRepository extends CrudRepository { + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } + + public static class MyFixedRepoFactory extends DummyRepositoryFactoryBean { + + public MyFixedRepoFactory(Class repositoryInterface) { + super(FixedFactoryRepository.class); + } + } +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomImplementation.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomImplementation.java new file mode 100644 index 000000000..7b26859c7 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomImplementation.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import java.util.Collections; +import java.util.List; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.stereotype.Component; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableRepositories(considerNestedRepositories = true, + includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*RepositoryWithCustomImplementation") }) +public class ConfigWithCustomImplementation { + + public interface RepositoryWithCustomImplementation extends Repository, CustomImplInterface { + + } + + public interface CustomImplInterface { + + List findMyCustomer(); + } + + @Component + public static class RepositoryWithCustomImplementationImpl implements CustomImplInterface { + + @Override + public List findMyCustomer() { + return Collections.emptyList(); + } + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java new file mode 100644 index 000000000..b179eb715 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithCustomRepositoryBaseClass.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import lombok.experimental.Delegate; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.RepoBaseClass; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableRepositories(repositoryBaseClass = RepoBaseClass.class, considerNestedRepositories = true, + includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryWithCustomBaseRepo$") }) +public class ConfigWithCustomRepositoryBaseClass { + + public interface CustomerRepositoryWithCustomBaseRepo extends CrudRepository { + + } + + public static class RepoBaseClass implements CrudRepository { + + private @Delegate CrudRepository delegate; + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithFragments.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithFragments.java new file mode 100644 index 000000000..3a8c7e868 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithFragments.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import java.util.Collections; +import java.util.List; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.stereotype.Component; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableRepositories(considerNestedRepositories = true, + includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*RepositoryWithFragments") }) +public class ConfigWithFragments { + + public interface RepositoryWithFragments + extends Repository, CustomImplInterface1, CustomImplInterface2 { + + } + + public interface CustomImplInterface1 { + + List findMyCustomer(); + } + + @Component + public static class CustomImplInterface1Impl implements CustomImplInterface1 { + + @Override + public List findMyCustomer() { + return Collections.emptyList(); + } + } + + public interface CustomImplInterface2 { + + } + + @Component + public static class CustomImplInterface2Impl implements CustomImplInterface2 { + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } + + public static class Customer { + + } +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithQueryMethods.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithQueryMethods.java new file mode 100644 index 000000000..2713df3f5 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithQueryMethods.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.annotation.Nullable; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.domain.Page; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.query.Param; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableRepositories(considerNestedRepositories = true, includeFilters = {@Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryWithQueryMethods")}) +public class ConfigWithQueryMethods { + + public interface CustomerRepositoryWithQueryMethods extends Repository { + + Page findAllBy(@Param("longValue") Long val); + + @CustomQuery + String customQuery(); + + ProjectionInterface findProjectionBy(); + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } + + public interface ProjectionInterface {} + + @Nullable + @QueryAnnotation + @Retention(RetentionPolicy.RUNTIME) + public @interface CustomQuery { + + } + +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java new file mode 100644 index 000000000..1ac19ec66 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithSimpleCrudRepository.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository.MyRepo; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; + +/** + * @author Christoph Strobl + */ +@EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyRepo.class) }, + basePackageClasses = ConfigWithSimpleCrudRepository.class, considerNestedRepositories = true) +public class ConfigWithSimpleCrudRepository { + + public interface MyRepo extends CrudRepository { + + } + + public static class Person { + + @javax.annotation.Nullable + Address address; + + } + + public static class Address { + String street; + } + +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresent.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresent.java new file mode 100644 index 000000000..6b080da2f --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresent.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresent.MyTxRepo; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.transaction.TransactionManager; + +/** + * @author Christoph Strobl + */ +@EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyTxRepo.class) }, + basePackageClasses = ConfigWithTransactionManagerPresent.class, considerNestedRepositories = true) +public class ConfigWithTransactionManagerPresent { + + public interface MyTxRepo extends CrudRepository { + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } + + @Bean + TransactionManager txManager() { + return Mockito.mock(TransactionManager.class); + } + +} diff --git a/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.java b/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.java new file mode 100644 index 000000000..db0b3b096 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.MyComponentTxRepo; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.stereotype.Component; +import org.springframework.transaction.TransactionManager; + +/** + * @author Christoph Strobl + */ +@EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyComponentTxRepo.class) }, + basePackageClasses = ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty.class, + considerNestedRepositories = true) +public class ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepoisoty { + + @Component + public interface MyComponentTxRepo extends CrudRepository { + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } + + @Bean + TransactionManager txManager() { + return Mockito.mock(TransactionManager.class); + } + +} diff --git a/src/test/java/org/springframework/data/aot/sample/ReactiveConfig.java b/src/test/java/org/springframework/data/aot/sample/ReactiveConfig.java new file mode 100644 index 000000000..94d17c17e --- /dev/null +++ b/src/test/java/org/springframework/data/aot/sample/ReactiveConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot.sample; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.repository.config.EnableReactiveRepositories; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +/** + * @author Christoph Strobl + */ +@Configuration +@EnableReactiveRepositories(considerNestedRepositories = true, includeFilters = {@Filter(type = FilterType.REGEX, pattern = ".*CustomerRepositoryReactive$")}) +public class ReactiveConfig { + + public interface CustomerRepositoryReactive extends ReactiveCrudRepository { + + } + + public static class Person { + + Address address; + + } + + public static class Address { + String street; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/AbstractType.java b/src/test/java/org/springframework/data/aot/types/AbstractType.java new file mode 100644 index 000000000..e99b74253 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/AbstractType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public abstract class AbstractType { + + private Object fieldInAbstractType; + + abstract Object abstractMethod(); + + Object methodDefinedInAbstractType() { + return null; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/Address.java b/src/test/java/org/springframework/data/aot/types/Address.java new file mode 100644 index 000000000..bac51ca9c --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/Address.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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.types; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; +import org.springframework.data.geo.Point; + +/** + * @author Christoph Strobl + */ +public class Address implements LocationHolder { + + String street; + Point location; + + Address() { + } + + @PersistenceCreator + Address(String street) { + this.street = street; + } + + @Override + public Point getLocation() { + return location; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/BaseEntity.java b/src/test/java/org/springframework/data/aot/types/BaseEntity.java new file mode 100644 index 000000000..15e68857b --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/BaseEntity.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2021 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.types; + +/** + * @author Christoph Strobl + */ +public class BaseEntity { + + Address address; +} diff --git a/src/test/java/org/springframework/data/aot/types/Customer.java b/src/test/java/org/springframework/data/aot/types/Customer.java new file mode 100644 index 000000000..817c64374 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/Customer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2021 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.types; + +import java.time.Instant; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; +import org.springframework.data.annotation.TypeAlias; + +/** + * @author Christoph Strobl + */ +@TypeAlias("cu") +public class Customer extends BaseEntity { + + @Id + String id; + + @Transient + String transientProperty; + + @LastModifiedDate + Instant modifiedAt; +} diff --git a/src/test/java/org/springframework/data/aot/types/CyclicGenerics.java b/src/test/java/org/springframework/data/aot/types/CyclicGenerics.java new file mode 100644 index 000000000..d783204c1 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/CyclicGenerics.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public class CyclicGenerics>> { + + T property; +} diff --git a/src/test/java/org/springframework/data/aot/types/CyclicPropertiesA.java b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesA.java new file mode 100644 index 000000000..e16924359 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesA.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2021 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.types; + +/** + * @author Christoph Strobl + */ +public class CyclicPropertiesA { + + CyclicPropertiesB b; +} diff --git a/src/test/java/org/springframework/data/aot/types/CyclicPropertiesB.java b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesB.java new file mode 100644 index 000000000..bfd6de2a3 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesB.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public class CyclicPropertiesB { + + CyclicPropertiesA refToA; +} diff --git a/src/test/java/org/springframework/data/aot/types/CyclicPropertiesSelf.java b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesSelf.java new file mode 100644 index 000000000..ec0d09423 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/CyclicPropertiesSelf.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public class CyclicPropertiesSelf { + + CyclicPropertiesSelf refSelf; +} diff --git a/src/test/java/org/springframework/data/aot/types/DomainObjectWithSimpleTypesOnly.java b/src/test/java/org/springframework/data/aot/types/DomainObjectWithSimpleTypesOnly.java new file mode 100644 index 000000000..04b364e1c --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/DomainObjectWithSimpleTypesOnly.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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.types; + +import org.springframework.data.annotation.Id; + +/** + * @author Christoph Strobl + */ +public class DomainObjectWithSimpleTypesOnly { + + @Id + String id; + Long longValue; + int primValue; +} diff --git a/src/test/java/org/springframework/data/aot/types/EmptyType1.java b/src/test/java/org/springframework/data/aot/types/EmptyType1.java new file mode 100644 index 000000000..58f27b811 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/EmptyType1.java @@ -0,0 +1,23 @@ +/* + * 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.types; + +/** + * @author Christoph Strobl + */ +public class EmptyType1 { + +} diff --git a/src/test/java/org/springframework/data/aot/types/EmptyType2.java b/src/test/java/org/springframework/data/aot/types/EmptyType2.java new file mode 100644 index 000000000..47f17fc0e --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/EmptyType2.java @@ -0,0 +1,23 @@ +/* + * 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.types; + +/** + * @author Christoph Strobl + */ +public class EmptyType2 { + +} diff --git a/src/test/java/org/springframework/data/aot/types/FieldsAndMethods.java b/src/test/java/org/springframework/data/aot/types/FieldsAndMethods.java new file mode 100644 index 000000000..d8b04cabb --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/FieldsAndMethods.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2021 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.types; + +/** + * @author Christoph Strobl + */ +public class FieldsAndMethods extends AbstractType implements InterfaceType { + + public static final Object CONSTANT_FIELD = null; + + private Object privateField; + Object packagePrivateField; + protected Object protectedField; + public Object publicField; + + @Override + Long abstractMethod() { + return null; + } + + @Override + public Integer someDefaultMethod() { + return null; + } + + private Object privateMethod() { + return null; + } + + Object packagePrivateMethod() { + return null; + } + + protected Object protectedMethod() { + return null; + } + + public Object publicMethod() { + return null; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/InterfaceType.java b/src/test/java/org/springframework/data/aot/types/InterfaceType.java new file mode 100644 index 000000000..b80c62545 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/InterfaceType.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public interface InterfaceType { + + default Object someDefaultMethod() { + return null; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/LocationHolder.java b/src/test/java/org/springframework/data/aot/types/LocationHolder.java new file mode 100644 index 000000000..ad65781a4 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/LocationHolder.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2021 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.types; + +import org.springframework.data.geo.Point; + +/** + * @author Christoph Strobl + */ +public interface LocationHolder { + + Point getLocation(); +} diff --git a/src/test/java/org/springframework/data/aot/types/ProjectionInterface.java b/src/test/java/org/springframework/data/aot/types/ProjectionInterface.java new file mode 100644 index 000000000..85cc06c77 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/ProjectionInterface.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public interface ProjectionInterface { + +} diff --git a/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java b/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java new file mode 100644 index 000000000..1524c34a1 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/TypesInMethodSignatures.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2021 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.types; + +/** + * @author Christoph Strobl + */ +public class TypesInMethodSignatures { + + TypesInMethodSignatures(String ctorArg) { + + } + + Long returnValue() { + return null; + } + + void voidReturn() { + + } + + EmptyType1 aDomainType() { + return null; + } + + void setSomething(EmptyType2 something) { + + } + + + + Object methodArg(Integer methodArg) { + return null; + } +} diff --git a/src/test/java/org/springframework/data/aot/types/WithDeclaredClass.java b/src/test/java/org/springframework/data/aot/types/WithDeclaredClass.java new file mode 100644 index 000000000..5d0e4f46b --- /dev/null +++ b/src/test/java/org/springframework/data/aot/types/WithDeclaredClass.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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.types; + +/** + * @author Christoph Strobl + */ +public class WithDeclaredClass { + + public enum SomeEnum {}; +} diff --git a/src/test/java/org/springframework/data/repository/config/DummyConfigurationExtension.java b/src/test/java/org/springframework/data/repository/config/DummyConfigurationExtension.java new file mode 100644 index 000000000..54384bbaa --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/DummyConfigurationExtension.java @@ -0,0 +1,34 @@ +/* + * 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.repository.config; + +import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +class DummyConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + public String getRepositoryFactoryBeanClassName() { + return DummyRepositoryFactoryBean.class.getName(); + } + + @Override + public String getModulePrefix() { + return "commons"; + } +} diff --git a/src/test/java/org/springframework/data/repository/config/DummyRegistrar.java b/src/test/java/org/springframework/data/repository/config/DummyRegistrar.java new file mode 100644 index 000000000..2c0f4f072 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/DummyRegistrar.java @@ -0,0 +1,41 @@ +/* + * 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.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.core.io.DefaultResourceLoader; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +class DummyRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + DummyRegistrar() { + setResourceLoader(new DefaultResourceLoader()); + } + + @Override + protected Class getAnnotation() { + return EnableRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new DummyConfigurationExtension(); + } +} diff --git a/src/test/java/org/springframework/data/repository/config/EnableReactiveRepositories.java b/src/test/java/org/springframework/data/repository/config/EnableReactiveRepositories.java new file mode 100644 index 000000000..817e832ed --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/EnableReactiveRepositories.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-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.repository.config; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.core.support.ReactiveDummyRepositoryFactoryBean; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; + +@Retention(RetentionPolicy.RUNTIME) +@Import(ReactiveDummyRegistrar.class) +@Inherited +public @interface EnableReactiveRepositories { + + String[] value() default {}; + + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; + + Filter[] includeFilters() default {}; + + Filter[] excludeFilters() default {}; + + Class repositoryFactoryBeanClass() default ReactiveDummyRepositoryFactoryBean.class; + + Class repositoryBaseClass() default ReactiveSortingRepository.class; + + String namedQueriesLocation() default ""; + + String repositoryImplementationPostfix() default "Impl"; + + boolean considerNestedRepositories() default false; + + boolean limitImplementationBasePackages() default true; + + BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; +} diff --git a/src/test/java/org/springframework/data/repository/config/EnableRepositories.java b/src/test/java/org/springframework/data/repository/config/EnableRepositories.java index 21f1219bc..a09c84e1f 100644 --- a/src/test/java/org/springframework/data/repository/config/EnableRepositories.java +++ b/src/test/java/org/springframework/data/repository/config/EnableRepositories.java @@ -22,7 +22,6 @@ import java.lang.annotation.RetentionPolicy; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupportUnitTests.DummyRegistrar; import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; @Retention(RetentionPolicy.RUNTIME) diff --git a/src/test/java/org/springframework/data/repository/config/ReactiveDummyConfigurationExtension.java b/src/test/java/org/springframework/data/repository/config/ReactiveDummyConfigurationExtension.java new file mode 100644 index 000000000..991ef4a36 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/ReactiveDummyConfigurationExtension.java @@ -0,0 +1,43 @@ +/* + * 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.repository.config; + +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.ReactiveDummyRepositoryFactoryBean; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +class ReactiveDummyConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + public String getRepositoryFactoryBeanClassName() { + return ReactiveDummyRepositoryFactoryBean.class.getName(); + } + + @Override + public String getModulePrefix() { + return "commons"; + } + + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + if(metadata.isReactiveRepository()) { + return true; + } + return false; + } +} diff --git a/src/test/java/org/springframework/data/repository/config/ReactiveDummyRegistrar.java b/src/test/java/org/springframework/data/repository/config/ReactiveDummyRegistrar.java new file mode 100644 index 000000000..bac3c23c8 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/ReactiveDummyRegistrar.java @@ -0,0 +1,41 @@ +/* + * 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.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.core.io.DefaultResourceLoader; + +/** + * @author Christoph Strobl + * @since 2022/04 + */ +class ReactiveDummyRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + ReactiveDummyRegistrar() { + setResourceLoader(new DefaultResourceLoader()); + } + + @Override + protected Class getAnnotation() { + return EnableReactiveRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new ReactiveDummyConfigurationExtension(); + } +} diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java index 8dce63256..2b19adf5d 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportIntegrationTests.java @@ -25,7 +25,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; -import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupportUnitTests.DummyConfigurationExtension; import org.springframework.util.ClassUtils; /** diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java index 52c81e64e..f7ee0e598 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java @@ -18,8 +18,6 @@ package org.springframework.data.repository.config; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.lang.annotation.Annotation; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,7 +31,6 @@ import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.mapping.Person; diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java index b6b9a833b..67230778b 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java @@ -110,7 +110,7 @@ class RepositoryConfigurationExtensionSupportUnitTests { } @Override - protected Collection> getIdentifyingAnnotations() { + public Collection> getIdentifyingAnnotations() { return Collections.singleton(Primary.class); } diff --git a/src/test/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadataUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadataUnitTests.java index 939fea220..ade0325a7 100755 --- a/src/test/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadataUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadataUnitTests.java @@ -19,8 +19,10 @@ import static org.assertj.core.api.Assertions.*; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Page; diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java index b0dcd5e8c..71845b0b8 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java @@ -16,6 +16,7 @@ package org.springframework.data.repository.core.support; import java.lang.reflect.Method; +import java.util.Collections; import java.util.Set; import org.springframework.data.repository.core.CrudMethods; @@ -103,4 +104,9 @@ public final class DummyRepositoryInformation implements RepositoryInformation { public boolean isReactiveRepository() { return metadata.isReactiveRepository(); } + + @Override + public Set> getFragments() { + return Collections.emptySet(); + } } diff --git a/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactory.java new file mode 100644 index 000000000..b6b47e4d1 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactory.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-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.repository.core.support; + +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.function.Supplier; + +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; + +/** + * Dummy implementation for {@link RepositoryFactorySupport} that is equipped with mocks to simulate behavior for test + * cases. + * + * @author Oliver Gierke + * @author Christoph Strobl + */ +public class ReactiveDummyRepositoryFactory extends ReactiveRepositoryFactorySupport { + + public final MyRepositoryQuery queryOne = mock(MyRepositoryQuery.class); + public final RepositoryQuery queryTwo = mock(RepositoryQuery.class); + public final QueryLookupStrategy strategy = mock(QueryLookupStrategy.class); + + private final ApplicationStartup applicationStartup; + + @SuppressWarnings("unchecked") private final QuerydslPredicateExecutor querydsl = mock( + QuerydslPredicateExecutor.class); + private final Object repository; + + public ReactiveDummyRepositoryFactory(Object repository) { + + this.repository = repository; + + when(strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), + Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(queryOne); + + this.applicationStartup = mock(ApplicationStartup.class); + var startupStep = mock(StartupStep.class); + when(applicationStartup.start(anyString())).thenReturn(startupStep); + when(startupStep.tag(anyString(), anyString())).thenReturn(startupStep); + when(startupStep.tag(anyString(), ArgumentMatchers.> any())).thenReturn(startupStep); + + var beanFactory = Mockito.mock(BeanFactory.class); + when(beanFactory.getBean(ApplicationStartup.class)).thenReturn(applicationStartup); + setBeanFactory(beanFactory); + } + + @Override + @SuppressWarnings("unchecked") + public EntityInformation getEntityInformation(Class domainClass) { + return mock(EntityInformation.class); + } + + @Override + protected Object getTargetRepository(RepositoryInformation information) { + return repository; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return repository.getClass(); + } + + @Override + protected Optional getQueryLookupStrategy(Key key, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + return Optional.of(strategy); + } + + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + + var fragments = super.getRepositoryFragments(metadata); + + return QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()) // + ? fragments.append(RepositoryFragments.just(querydsl)) // + : fragments; + } + + ApplicationStartup getApplicationStartup() { + return this.applicationStartup; + } + + /** + * @author Mark Paluch + */ + public interface MyRepositoryQuery extends RepositoryQuery { + + } + +} diff --git a/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactoryBean.java b/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactoryBean.java new file mode 100644 index 000000000..b0fe4fe9c --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/ReactiveDummyRepositoryFactoryBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-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.repository.core.support; + +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.springframework.data.mapping.context.SampleMappingContext; +import org.springframework.data.repository.Repository; + +/** + * @author Oliver Gierke + */ +public class ReactiveDummyRepositoryFactoryBean, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport { + + private final T repository; + + public ReactiveDummyRepositoryFactoryBean(Class repositoryInterface) { + + super(repositoryInterface); + + this.repository = mock(repositoryInterface); + setMappingContext(new SampleMappingContext()); + } + + @Override + protected RepositoryFactorySupport createRepositoryFactory() { + return new ReactiveDummyRepositoryFactory(repository); + } +}