Add AOT repository support

We now use the AOT infrastructure of Spring Framework 6 and data commons to provide AOT support building the foundation for native image compilation.
Additionally we register hints for GraalVM native image.

See: #4022
Original Pull Request: #4093
This commit is contained in:
Christoph Strobl
2022-04-12 13:55:51 +02:00
parent 079c5a95aa
commit cfd55be95b
24 changed files with 932 additions and 19 deletions

View File

@@ -0,0 +1,50 @@
/*
* 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.mongodb;
import java.util.function.Consumer;
import org.springframework.data.domain.ManagedTypes;
/**
* @author Christoph Strobl
* @since 4.0
*/
public final class MongoManagedTypes implements ManagedTypes {
private final ManagedTypes delegate;
public MongoManagedTypes(ManagedTypes types) {
this.delegate = types;
}
public static MongoManagedTypes from(ManagedTypes managedTypes) {
return new MongoManagedTypes(managedTypes);
}
public static MongoManagedTypes of(Iterable<? extends Class<?>> types) {
return from(ManagedTypes.fromIterable(types));
}
public static MongoManagedTypes none() {
return from(ManagedTypes.empty());
}
@Override
public void forEach(Consumer<Class<?>> action) {
delegate.forEach(action);
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.mongodb.aot;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.data.aot.AotRepositoryContext;
import org.springframework.data.aot.RepositoryRegistrationAotProcessor;
import org.springframework.data.aot.TypeContributor;
import org.springframework.data.aot.TypeUtils;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
/**
* @author Christoph Strobl
*/
public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotProcessor {
private LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
@Override
protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
// do some custom type registration here
super.contribute(repositoryContext, generationContext);
repositoryContext.getResolvedTypes().stream().filter(MongoAotPredicates.IS_SIMPLE_TYPE.negate()).forEach(type -> {
TypeContributor.contribute(type, it -> true, generationContext);
lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext);
});
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.mongodb.aot;
import java.util.Arrays;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback;
import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 4.0
*/
public class DataMongoRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.reflection().registerTypes(
Arrays.asList(TypeReference.of(SimpleMongoRepository.class), TypeReference.of(BeforeConvertCallback.class),
TypeReference.of(BeforeSaveCallback.class), TypeReference.of(AfterConvertCallback.class),
TypeReference.of(AfterSaveCallback.class)),
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS));
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.mongodb.aot;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.Reference;
import org.springframework.data.aot.TypeUtils;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
/**
* @author Christoph Strobl
* @since 4.0
*/
public class LazyLoadingProxyAotProcessor {
private boolean generalLazyLoadingProxyContributed = false;
void registerLazyLoadingProxyIfNeeded(Class<?> type, GenerationContext generationContext) {
Set<Field> refFields = getFieldsWithAnnotationPresent(type, Reference.class);
if (refFields.isEmpty()) {
return;
}
refFields.stream() //
.filter(LazyLoadingProxyAotProcessor::isLazyLoading) //
.forEach(field -> {
if (!generalLazyLoadingProxyContributed) {
generationContext.getRuntimeHints().proxies().registerJdkProxy(
TypeReference.of(org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class),
TypeReference.of(org.springframework.aop.SpringProxy.class),
TypeReference.of(org.springframework.aop.framework.Advised.class),
TypeReference.of(org.springframework.core.DecoratingProxy.class));
generalLazyLoadingProxyContributed = true;
}
if (field.getType().isInterface()) {
List<Class<?>> interfaces = new ArrayList<>(
TypeUtils.resolveTypesInSignature(ResolvableType.forField(field, type)));
interfaces.add(0, org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class);
interfaces.add(org.springframework.aop.SpringProxy.class);
interfaces.add(org.springframework.aop.framework.Advised.class);
interfaces.add(org.springframework.core.DecoratingProxy.class);
generationContext.getRuntimeHints().proxies().registerJdkProxy(interfaces.toArray(Class[]::new));
} else {
generationContext.getRuntimeHints().proxies().registerClassProxy(field.getType(), builder -> {
builder.proxiedInterfaces(org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class);
});
}
});
}
private static boolean isLazyLoading(Field field) {
if (AnnotatedElementUtils.isAnnotated(field, DBRef.class)) {
return AnnotatedElementUtils.findMergedAnnotation(field, DBRef.class).lazy();
}
if (AnnotatedElementUtils.isAnnotated(field, DocumentReference.class)) {
return AnnotatedElementUtils.findMergedAnnotation(field, DocumentReference.class).lazy();
}
return false;
}
private static Set<Field> getFieldsWithAnnotationPresent(Class<?> type, Class<? extends Annotation> annotation) {
Set<Field> fields = new LinkedHashSet<>();
for (Field field : type.getDeclaredFields()) {
if (MergedAnnotations.from(field).get(annotation).isPresent()) {
fields.add(field);
}
}
return fields;
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.mongodb.aot;
import java.util.function.Predicate;
import org.springframework.data.aot.TypeUtils;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
/**
* @author Christoph Strobl
* @since 4.0
*/
class MongoAotPredicates {
static final Predicate<Class<?>> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type) || TypeUtils.type(type).isPartOf("org.bson");
}

View File

@@ -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.mongodb.aot;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.core.ResolvableType;
import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor;
import org.springframework.data.mongodb.MongoManagedTypes;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
* @since 2022/06
*/
public class MongoManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor {
private LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
public MongoManagedTypesBeanRegistrationAotProcessor() {
setModuleIdentifier("mongo");
}
@Override
protected boolean isMatch(@Nullable Class<?> beanType, @Nullable String beanName) {
return isMongoManagedTypes(beanType) || super.isMatch(beanType, beanName);
}
protected boolean isMongoManagedTypes(@Nullable Class<?> beanType) {
return beanType != null && ClassUtils.isAssignable(MongoManagedTypes.class, beanType);
}
@Override
protected void contributeType(ResolvableType type, GenerationContext generationContext) {
if (MongoAotPredicates.IS_SIMPLE_TYPE.test(type.toClass())) {
return;
}
super.contributeType(type, generationContext);
lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type.toClass(), generationContext);
}
}

View File

@@ -23,6 +23,8 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.data.aot.hint.AuditingHints;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
@@ -37,6 +39,7 @@ import org.springframework.data.domain.AuditorAware;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MongoAuditingRegistrar.class)
@ImportRuntimeHints(AuditingHints.AuditingRuntimeHints.class)
public @interface EnableMongoAuditing {
/**

View File

@@ -18,18 +18,18 @@ package org.springframework.data.mongodb.config;
import java.lang.annotation.Annotation;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
import org.springframework.data.auditing.config.AuditingConfiguration;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback;
import org.springframework.util.Assert;
@@ -40,7 +40,7 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @author Mark Paluch
*/
class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport implements Ordered {
@Override
protected Class<? extends Annotation> getAnnotation() {
@@ -52,12 +52,34 @@ class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
return "mongoAuditingHandler";
}
String persistentEntitiesBeanName;
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
if(persistentEntitiesBeanName == null) {
if (registry instanceof DefaultListableBeanFactory beanFactory) {
for (String bn : beanFactory.getBeanNamesForType(PersistentEntities.class)) {
if (bn.startsWith("mongo")) {
persistentEntitiesBeanName = bn;
}
}
}
if(persistentEntitiesBeanName == null) {
persistentEntitiesBeanName = BeanDefinitionReaderUtils.uniqueBeanName("mongo.persistent-entities", registry);
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntities.class)
.setFactoryMethod("of")
//.addConstructorArgValue(new RuntimeBeanReference(MongoMappingContext.class))
.addConstructorArgReference("mongoMappingContext");
registry.registerBeanDefinition(persistentEntitiesBeanName, definition.getBeanDefinition());
}
}
super.registerBeanDefinitions(annotationMetadata, registry);
}
@@ -67,11 +89,7 @@ class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
Assert.notNull(configuration, "AuditingConfiguration must not be null");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class);
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(org.springframework.data.repository.config.PersistentEntitiesFactoryBean.class);
definition.addConstructorArgValue(new RuntimeBeanReference(MappingContext.class));
builder.addConstructorArgValue(definition.getBeanDefinition());
builder.addConstructorArgReference(persistentEntitiesBeanName);
return configureDefaultAuditHandlerAttributes(configuration, builder);
}
@@ -91,4 +109,9 @@ class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
AuditingEntityCallback.class.getName(), registry);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -27,9 +27,11 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mongodb.MongoManagedTypes;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.mapping.Document;
@@ -79,11 +81,11 @@ public abstract class MongoConfigurationSupport {
* @throws ClassNotFoundException
*/
@Bean
public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions)
public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions, ManagedTypes managedTypes)
throws ClassNotFoundException {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setManagedTypes(managedTypes);
mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
mappingContext.setAutoIndexCreation(autoIndexCreation());
@@ -91,6 +93,16 @@ public abstract class MongoConfigurationSupport {
return mappingContext;
}
/**
* @return new instance of {@link MongoManagedTypes}.
* @throws ClassNotFoundException
* @since 4.0
*/
@Bean
public MongoManagedTypes managedTypes() throws ClassNotFoundException {
return MongoManagedTypes.of(getInitialEntitySet());
}
/**
* Register custom {@link Converter}s in a {@link CustomConversions} object if required. These
* {@link CustomConversions} will be registered with the

View File

@@ -27,12 +27,12 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.NativeDetector;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException;
@@ -71,6 +71,16 @@ class LazyLoadingProxyFactory {
if (!propertyType.isInterface()) {
if (NativeDetector.inNativeImage()) {
ProxyFactory factory = new ProxyFactory();
factory.addAdvice(interceptor);
factory.addInterface(LazyLoadingProxy.class);
factory.setTargetClass(propertyType);
factory.setProxyTargetClass(true);
return factory.getProxy(propertyType.getClassLoader());
}
Factory factory = (Factory) objenesis.newInstance(getEnhancedTypeFor(propertyType));
factory.setCallbacks(new Callback[] { interceptor });

View File

@@ -75,6 +75,15 @@ public abstract class MongoSimpleTypes {
simpleTypes.add(UUID.class);
simpleTypes.add(Instant.class);
simpleTypes.add(BsonValue.class);
simpleTypes.add(BsonNumber.class);
simpleTypes.add(BsonType.class);
simpleTypes.add(BsonArray.class);
simpleTypes.add(BsonSymbol.class);
simpleTypes.add(BsonUndefined.class);
simpleTypes.add(BsonMinKey.class);
simpleTypes.add(BsonMaxKey.class);
simpleTypes.add(BsonNull.class);
simpleTypes.add(BsonBinary.class);
simpleTypes.add(BsonBoolean.class);
simpleTypes.add(BsonDateTime.class);

View File

@@ -19,9 +19,11 @@ import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.mongodb.aot.AotMongoRepositoryPostProcessor;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
@@ -29,6 +31,7 @@ import org.springframework.data.repository.config.AnnotationRepositoryConfigurat
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.lang.NonNull;
import org.w3c.dom.Element;
/**
@@ -48,7 +51,7 @@ public class MongoRepositoryConfigurationExtension extends RepositoryConfigurati
}
@Override
protected String getModulePrefix() {
public String getModulePrefix() {
return "mongo";
}
@@ -62,10 +65,15 @@ public class MongoRepositoryConfigurationExtension extends RepositoryConfigurati
}
@Override
protected Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
public Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
return Collections.singleton(Document.class);
}
@Override
public Class<? extends BeanRegistrationAotProcessor> getRepositoryAotProcessor() {
return AotMongoRepositoryPostProcessor.class;
}
@Override
protected Collection<Class<?>> getIdentifyingTypes() {
return Collections.singleton(MongoRepository.class);

View File

@@ -0,0 +1,4 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.data.mongodb.aot.DataMongoRuntimeHints
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.data.mongodb.aot.MongoManagedTypesBeanRegistrationAotProcessor

View File

@@ -0,0 +1,87 @@
/*
* 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.mongodb.aot;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.aot.RepositoryRegistrationAotContributionAssert.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.annotation.SynthesizedAnnotation;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Transient;
import org.springframework.data.aot.RepositoryRegistrationAotContribution;
import org.springframework.data.mongodb.aot.configs.ImperativeConfig;
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
import org.springframework.data.mongodb.core.mapping.Address;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author Christoph Strobl
* @since 2022/04
*/
public class AotMongoRepositoryPostProcessorUnitTests {
@Test
void contributesProxiesForDataAnnotations() {
RepositoryRegistrationAotContribution repositoryBeanContribution = computeConfiguration(ImperativeConfig.class)
.forRepository(ImperativeConfig.PersonRepository.class);
assertThatContribution(repositoryBeanContribution) //
.codeContributionSatisfies(contribution -> {
contribution.contributesJdkProxy(Transient.class, SynthesizedAnnotation.class);
contribution.contributesJdkProxy(LastModifiedDate.class, SynthesizedAnnotation.class);
contribution.contributesJdkProxy(Document.class, SynthesizedAnnotation.class);
contribution.contributesJdkProxy(DBRef.class, SynthesizedAnnotation.class);
// TODO: not supported yet contribution.contributesClassProxy(Address.class, LazyLoadingProxy.class);
});
}
BeanContributionBuilder computeConfiguration(Class<?> configuration) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
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];
AotMongoRepositoryPostProcessor postProcessor = ctx.getBean(AotMongoRepositoryPostProcessor.class);
postProcessor.setBeanFactory(ctx.getDefaultListableBeanFactory());
BeanRegistrationAotContribution beanRegistrationAotContribution = postProcessor
.processAheadOfTime(RegisteredBean.of(ctx.getBeanFactory(), beanName));
assertThat(beanRegistrationAotContribution).isInstanceOf(RepositoryRegistrationAotContribution.class);
return (RepositoryRegistrationAotContribution) beanRegistrationAotContribution;
};
}
interface BeanContributionBuilder {
RepositoryRegistrationAotContribution forRepository(Class<?> repositoryInterface);
}
}

View File

@@ -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.mongodb.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<ClassProxyAssert, ClassProxyHint> {
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<TypeReference> getProxiedInterfaces() {
return actual.getProxiedInterfaces();
}
}

View File

@@ -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.mongodb.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.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.ClassProxyHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.RuntimeHintsPredicates;
/**
* @author Christoph Strobl
* @since 2022/04
*/
public class CodeContributionAssert extends AbstractAssert<CodeContributionAssert, GenerationContext> {
public CodeContributionAssert(GenerationContext contribution) {
super(contribution, CodeContributionAssert.class);
}
public CodeContributionAssert contributesReflectionFor(Class<?>... types) {
for (Class<?> type : types) {
assertThat(this.actual.getRuntimeHints())
.describedAs("No reflection entry found for [%s]", type)
.matches(RuntimeHintsPredicates.reflection().onType(type));
}
return this;
}
public CodeContributionAssert doesNotContributeReflectionFor(Class<?>... types) {
for (Class<?> type : types) {
assertThat(this.actual.getRuntimeHints())
.describedAs("Reflection entry found for [%s]", type)
.matches(RuntimeHintsPredicates.reflection().onType(type).negate());
}
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 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;
}
public CodeContributionAssert doesNotContributeJdkProxy(Class<?>... proxyInterfaces) {
assertThat(jdkProxiesFor(proxyInterfaces[0]))
.describedAs("Found JDK proxy matching [%s] though it should not be present",
Arrays.asList(proxyInterfaces))
.noneSatisfy(it -> new JdkProxyAssert(it).matches(proxyInterfaces));
return this;
}
private Stream<JdkProxyHint> jdkProxiesFor(Class<?> entryPoint) {
return this.actual.getRuntimeHints().proxies().jdkProxies()
.filter(jdkProxyHint -> jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName()
.equals(entryPoint.getCanonicalName()));
}
public CodeContributionAssert contributesClassProxy(Class<?>... proxyInterfaces) {
assertThat(classProxiesFor(proxyInterfaces[0]))
.describedAs("Unable to find JDK proxy matching [%s]", Arrays.asList(proxyInterfaces))
.anySatisfy(it -> new ClassProxyAssert(it).matches(proxyInterfaces));
return this;
}
private Stream<ClassProxyHint> classProxiesFor(Class<?> entryPoint) {
return this.actual.getRuntimeHints().proxies().classProxies()
.filter(jdkProxyHint -> jdkProxyHint.getProxiedInterfaces().get(0).getCanonicalName()
.equals(entryPoint.getCanonicalName()));
}
}

View File

@@ -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.mongodb.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<JdkProxyAssert, JdkProxyHint> {
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<TypeReference> getProxiedInterfaces() {
return actual.getProxiedInterfaces();
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.mongodb.aot;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Consumer;
import org.assertj.core.api.AbstractAssert;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.data.aot.RepositoryRegistrationAotContribution;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.lang.NonNull;
/**
* @author Christoph Strobl
* @since 2022/04
*/
public class RepositoryRegistrationAotContributionAssert
extends AbstractAssert<RepositoryRegistrationAotContributionAssert, RepositoryRegistrationAotContribution> {
@NonNull
public static RepositoryRegistrationAotContributionAssert assertThatContribution(
@NonNull RepositoryRegistrationAotContribution actual) {
return new RepositoryRegistrationAotContributionAssert(actual);
}
public RepositoryRegistrationAotContributionAssert(@NonNull RepositoryRegistrationAotContribution actual) {
super(actual, RepositoryRegistrationAotContributionAssert.class);
}
public RepositoryRegistrationAotContributionAssert targetRepositoryTypeIs(Class<?> expected) {
assertThat(getRepositoryInformation().getRepositoryInterface()).isEqualTo(expected);
return this.myself;
}
public RepositoryRegistrationAotContributionAssert hasNoFragments() {
assertThat(getRepositoryInformation().getFragments()).isEmpty();
return this;
}
public RepositoryRegistrationAotContributionAssert hasFragments() {
assertThat(getRepositoryInformation().getFragments()).isNotEmpty();
return this;
}
public RepositoryRegistrationAotContributionAssert verifyFragments(Consumer<Set<RepositoryFragment<?>>> consumer) {
assertThat(getRepositoryInformation().getFragments())
.satisfies(it -> consumer.accept(new LinkedHashSet<>(it)));
return this;
}
public RepositoryRegistrationAotContributionAssert codeContributionSatisfies(
Consumer<CodeContributionAssert> assertWith) {
BeanRegistrationCode mockBeanRegistrationCode = mock(BeanRegistrationCode.class);
DefaultGenerationContext generationContext =
new DefaultGenerationContext(new ClassNameGenerator(), new InMemoryGeneratedFiles(), new RuntimeHints());
this.actual.applyTo(generationContext, mockBeanRegistrationCode);
assertWith.accept(new CodeContributionAssert(generationContext));
return this;
}
private RepositoryInformation getRepositoryInformation() {
assertThat(this.actual)
.describedAs("No repository interface found on null bean contribution")
.isNotNull();
assertThat(this.actual.getRepositoryInformation())
.describedAs("No repository interface found on null repository information")
.isNotNull();
return this.actual.getRepositoryInformation();
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.mongodb.aot.configs;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.mongodb.aot.domaintypes.Person;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.data.repository.CrudRepository;
/**
* @author Christoph Strobl
*/
@Configuration
@EnableMongoRepositories(considerNestedRepositories = true,
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*PersonRepository$") })
public class ImperativeConfig {
public interface PersonRepository extends CrudRepository<Person, String> {
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.mongodb.aot.domaintypes;
import org.springframework.data.mongodb.core.mapping.Field;
/**
* @author Christoph Strobl
* @since 2022/04
*/
public class Address {
@Field("the-street")
String street;
}

View File

@@ -0,0 +1,39 @@
/*
* 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.mongodb.aot.domaintypes;
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.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author Christoph Strobl
* @since 2022/04
*/
@Document("persons")
public class Person {
@Id String id;
@DBRef(lazy = true) Address address;
@Transient String transientProperty;
@LastModifiedDate Instant modifiedAt;
}

View File

@@ -31,7 +31,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoManagedTypes;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
@@ -91,7 +93,7 @@ public class AbstractMongoConfigurationUnitTests {
public void returnsUninitializedMappingContext() throws Exception {
SampleMongoConfiguration configuration = new SampleMongoConfiguration();
MongoMappingContext context = configuration.mongoMappingContext(configuration.customConversions());
MongoMappingContext context = configuration.mongoMappingContext(configuration.customConversions(), MongoManagedTypes.of(Collections.singleton(Entity.class)));
assertThat(context.getPersistentEntities()).isEmpty();
context.initialize();

View File

@@ -32,6 +32,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.data.mongodb.MongoManagedTypes;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -92,7 +93,7 @@ public class AbstractReactiveMongoConfigurationUnitTests {
public void returnsUninitializedMappingContext() throws Exception {
SampleMongoConfiguration configuration = new SampleMongoConfiguration();
MongoMappingContext context = configuration.mongoMappingContext(configuration.customConversions());
MongoMappingContext context = configuration.mongoMappingContext(configuration.customConversions(), MongoManagedTypes.of(Collections.singleton(Entity.class)));
assertThat(context.getPersistentEntities()).isEmpty();
context.initialize();

View File

@@ -105,9 +105,7 @@ public class MappingMongoConverterParserIntegrationTests {
@Test // DATAMONGO-892
void shouldThrowBeanDefinitionParsingExceptionIfConverterDefinedAsNestedBean() {
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(this::loadNestedBeanConfiguration);
}
@Test // DATAMONGO-925, DATAMONGO-928