diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 9a7ac3241..92d163d14 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -21,6 +21,7 @@ * xref:custom-conversions.adoc[] * xref:entity-callbacks.adoc[] * xref:is-new-state-detection.adoc[] +* xref:aot.adoc[] * xref:kotlin.adoc[] ** xref:kotlin/requirements.adoc[] ** xref:kotlin/null-safety.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/aot.adoc b/src/main/antora/modules/ROOT/pages/aot.adoc new file mode 100644 index 000000000..60d02d77c --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/aot.adoc @@ -0,0 +1,71 @@ += Ahead of Time Optimizations + +This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations]. + +[[aot.bestpractices]] +== Best Practices + +=== Annotate your Domain Types + +During application startup, Spring scans the classpath for domain classes for early processing of entities. +By annotating your domain types with Spring Data Store specific `@Table`, `@Document` or `@Entity` annotations you can aid initial entity scanning and ensure that those types are registered with `ManagedTypes` for Runtime Hints. +Classpath scanning is not possible in native image arrangements and so Spring has to use `ManagedTypes` for the initial entity set. + +[[aot.code-gen]] +== Ahead of Time Code Generation + +Ahead of time code generation is not limited to usage with GraalVM Native Image but also offers benefits when working with regular deployments and can help optimize startup performance on the jvm. + +If Ahead of Time compilation is enabled Spring Data can (depending on the actual Module in use) contribute several components during the AOT phase of your build. + +* Bytecode for generated Type/Property Accessors +* Sourcecode for the defined Repository Interfaces +* Repository Metadata in JSON format + +Each of the above is enabled by default. +However there users may fine tune the configuration with following options. + +[options = "autowidth",cols="1,1"] +|=== +|spring.aot.data.accessors.enabled +|boolean flag to control contribution of Bytecode for generated Type/Property Accessors + +|spring.aot.data.accessors.exclude +|comma separated list of FQCN for which to skip contribution of Bytecode for generated Type/Property Accessors + +|spring.aot.data.accessors.include +|comma separated list of FQCN for which to contribute Bytecode for generated Type/Property Accessors + +|spring.aot.repositories.enabled +|boolean flag to control contribution of Source Code for Repository Interfaces + +|spring.aot.[module-name].repositories.enabled +|boolean flag to control contribution of Source Code for Repository Interfaces for a certain module (eg. jdbc) +|=== + +[[aot.repositories]] +== Ahead of Time Repositories + +AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations. +Query methods are opaque to developers regarding their underlying queries being executed in a query method call. +AOT repositories contribute query method implementations based on derived, annotated, and named queries that are known at build-time. +This optimization moves query method processing from runtime to build-time, which can lead to a significant performance improvement as query methods do not need to be analyzed reflectively upon each application start. + +The resulting AOT repository fragment follows the naming scheme of `Impl_AotRepository` and is placed in the same package as the repository interface. + +[[aot.hints]] +== Native Image Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage. +These are in particular hints for: + +* Auditing +* `ManagedTypes` to capture the outcome of class-path scans +* Repositories +** Reflection hints for entities, return types, and Spring Data annotations +** Repository fragments +** Querydsl `Q` classes +** Kotlin Coroutine support +* Web support (Jackson Hints for `PagedModel`) + diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc index 3407e0754..b3d204e39 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -362,7 +362,6 @@ The `exposeMetadata` flag can be set directly on the repository factory bean via import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Configuration; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; -import org.springframework.lang.Nullable; @Configuration class MyConfiguration { diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index fc9fa96df..4c2247a01 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -24,7 +24,6 @@ import java.util.Set; import java.util.function.Consumer; import org.jspecify.annotations.Nullable; -import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java index 73d8c136d..3671a4684 100644 --- a/src/main/java/org/springframework/data/aot/AotMappingContext.java +++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java @@ -28,7 +28,6 @@ import org.springframework.data.mapping.model.EntityInstantiatorSource; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.util.TypeInformation; /** @@ -70,14 +69,12 @@ class AotMappingContext extends @Override protected BasicPersistentEntity createPersistentEntity( TypeInformation typeInformation) { - logger.debug("I hate gradle: create persistent entity for type: " + typeInformation); return new BasicPersistentEntity<>(typeInformation); } @Override protected AotPersistentProperty createPersistentProperty(Property property, BasicPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { - logger.info("creating property: " + property.getName()); return new AotPersistentProperty(property, owner, simpleTypeHolder); } diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java index 5510df924..e9953e9b4 100644 --- a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java +++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java @@ -1,11 +1,11 @@ /* - * Copyright 2025. the original author or authors. + * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,7 +18,6 @@ package org.springframework.data.aot; import java.io.Serializable; import java.util.List; -import java.util.function.Predicate; import java.util.stream.Stream; import org.springframework.aop.SpringProxy; @@ -31,27 +30,70 @@ import org.springframework.core.env.Environment; import org.springframework.data.projection.TargetAware; /** + * Configuration object that captures various AOT configuration aspects of types within the data context by offering + * predefined methods to register native configuration necessary for data binding, projection proxy definitions, AOT + * cglib bytecode generation and other common tasks. + *

+ * On {@link #contribute(Environment, GenerationContext)} the configuration is added to the {@link GenerationContext}. + * * @author Christoph Strobl + * @since 4.0 */ public interface AotTypeConfiguration { + /** + * Configure the referenced type for data binding. In case of {@link java.lang.annotation.Annotation} only data ones + * are considered. For more fine grained control use {@link #forReflectiveAccess(MemberCategory...)}. + * + * @return this. + */ AotTypeConfiguration forDataBinding(); + /** + * Configure the referenced type for reflective access by providing at least one {@link MemberCategory}. + * + * @param categories must not contain {@literal null}. + * @return this. + */ AotTypeConfiguration forReflectiveAccess(MemberCategory... categories); + /** + * Contribute generated cglib accessors for the referenced type. + *

+ * Can be disabled by user configuration ({@code spring.aot.data.accessors.enabled}). Honors in/exclusions set by user + * configuration {@code spring.aot.data.accessors.include} / {@code spring.aot.data.accessors.exclude} + * + * @return this. + */ AotTypeConfiguration contributeAccessors(); - // TODO: ? should this be a global condition for the entire configuration or do we need it for certain aspects ? - AotTypeConfiguration filter(Predicate> filter); - + /** + * Configure the referenced type as a projection interface returned by eg. a query method. + *

+ * Shortcut for {@link #proxyInterface(Class[]) proxyInterface(TargetAware, SpringProxy, DecoratingProxy)} + * + * @return this. + */ default AotTypeConfiguration usedAsProjectionInterface() { return proxyInterface(TargetAware.class, SpringProxy.class, DecoratingProxy.class); } + /** + * Configure the referenced type as a spring proxy interface. + *

+ * Shortcut for {@link #proxyInterface(Class[]) proxyInterface(SpringProxy, Advised, DecoratingProxy)} + * + * @return this. + */ default AotTypeConfiguration springProxy() { return proxyInterface(SpringProxy.class, Advised.class, DecoratingProxy.class); } + /** + * Configure the referenced type as a repository proxy. + * + * @return this. + */ default AotTypeConfiguration repositoryProxy() { springProxy(); @@ -67,14 +109,36 @@ public interface AotTypeConfiguration { return this; } + /** + * Register a proxy for the referenced type that also implements the given proxyInterfaces. + * + * @param proxyInterfaces additional interfaces the proxy implements. Order matters! + * @return this. + */ AotTypeConfiguration proxyInterface(List proxyInterfaces); + /** + * Register a proxy for the referenced type that also implements the given proxyInterfaces. + * + * @param proxyInterfaces additional interfaces the proxy implements. Order matters! + * @return this. + */ default AotTypeConfiguration proxyInterface(Class... proxyInterfaces) { return proxyInterface(Stream.of(proxyInterfaces).map(TypeReference::of).toList()); } + /** + * Configure the referenced type for usage with Querydsl by registering hints for potential {@code Q} types. + * + * @return this. + */ AotTypeConfiguration forQuerydsl(); + /** + * Write the configuration to the given {@link GenerationContext}. + * + * @param environment must not be {@literal null}. + * @param generationContext must not be {@literal null}. + */ void contribute(Environment environment, GenerationContext generationContext); - } diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index 7d6bcc2f3..f2e6bb26d 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -26,11 +26,9 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; @@ -41,8 +39,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; -import org.springframework.core.env.Environment; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; import org.springframework.util.AntPathMatcher; @@ -53,22 +49,27 @@ import org.springframework.util.StringUtils; * Default {@link AotContext} implementation. * * @author Mark Paluch + * @author Christoph Strobl * @since 3.0 */ class DefaultAotContext implements AotContext { - private final AotMappingContext mappingContext = new AotMappingContext();; + private final AotMappingContext mappingContext; private final ConfigurableListableBeanFactory factory; - // TODO: should we reuse the config or potentially have multiple ones with different settings - somehow targets the - // filtering issue + // TODO: should we reuse the config or potentially have multiple ones with different settings for the same type private final Map, AotTypeConfiguration> typeConfigurations = new HashMap<>(); - private final Environment environment; + private final Environment environment; public DefaultAotContext(BeanFactory beanFactory, Environment environment) { + this(beanFactory, environment, new AotMappingContext()); + } + + DefaultAotContext(BeanFactory beanFactory, Environment environment, AotMappingContext mappingContext) { this.factory = beanFactory instanceof ConfigurableListableBeanFactory cbf ? cbf : new DefaultListableBeanFactory(beanFactory); this.environment = environment; + this.mappingContext = mappingContext; } @Override @@ -188,7 +189,6 @@ class DefaultAotContext implements AotContext { private boolean contributeAccessors = false; private boolean forQuerydsl = false; private final List> proxies = new ArrayList<>(); - private Predicate> filter; ContextualTypeConfiguration(Class type) { this.type = type; @@ -224,20 +224,9 @@ class DefaultAotContext implements AotContext { return this; } - @Override - public AotTypeConfiguration filter(Predicate> filter) { - - this.filter = filter; - return this; - } - @Override public void contribute(Environment environment, GenerationContext generationContext) { - if (filter != null && !filter.test(this.type)) { - return; - } - if (!this.categories.isEmpty()) { generationContext.getRuntimeHints().reflection().registerType(this.type, categories.toArray(MemberCategory[]::new)); @@ -255,11 +244,7 @@ class DefaultAotContext implements AotContext { } if (forDataBinding) { - TypeContributor.contribute(type, Set.of(TypeContributor.DATA_NAMESPACE), generationContext); - - generationContext.getRuntimeHints().reflection().registerType(type, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS); } if (forQuerydsl) { @@ -268,9 +253,8 @@ class DefaultAotContext implements AotContext { if (!proxies.isEmpty()) { for (List proxyInterfaces : proxies) { - generationContext.getRuntimeHints().proxies() - .registerJdkProxy(Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()) - .toArray(TypeReference[]::new)); + generationContext.getRuntimeHints().proxies().registerJdkProxy( + Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()).toArray(TypeReference[]::new)); } } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 49e0f7475..e6935c5bb 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -53,7 +52,6 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio private final Log logger = LogFactory.getLog(getClass()); private @Nullable String moduleIdentifier; - private AotContext aotContext; private Lazy environment = Lazy.of(StandardEnvironment::new); public void setModuleIdentifier(@Nullable String moduleIdentifier) { @@ -77,8 +75,8 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio return null; } - this.aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get()); - return contribute(this.aotContext, resolveManagedTypes(registeredBean), registeredBean); + DefaultAotContext aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get()); + return contribute(aotContext, resolveManagedTypes(registeredBean), registeredBean); } private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { @@ -119,7 +117,7 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio /** * Hook to provide a customized flavor of {@link BeanRegistrationAotContribution}. By overriding this method calls to - * {@link #contributeType(ResolvableType, GenerationContext)} might no longer be issued. + * {@link #contributeType(ResolvableType, GenerationContext, AotContext)} might no longer be issued. * * @param aotContext never {@literal null}. * @param managedTypes never {@literal null}. @@ -136,7 +134,7 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio * @param type never {@literal null}. * @param generationContext never {@literal null}. */ - protected void contributeType(ResolvableType type, GenerationContext generationContext) { + protected void contributeType(ResolvableType type, GenerationContext generationContext, AotContext aotContext) { if (logger.isDebugEnabled()) { logger.debug(String.format("Contributing type information for [%s]", type.getType())); @@ -148,7 +146,7 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio .contributeAccessors() // .forQuerydsl().contribute(environment.get(), generationContext)); - TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( + TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index ed7d32dc0..e86f8c161 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -23,7 +23,6 @@ import java.util.function.BiConsumer; import javax.lang.model.element.Modifier; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; @@ -77,11 +76,11 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContri private final AotContext aotContext; private final ManagedTypes managedTypes; private final Lazy>> sourceTypes; - private final BiConsumer contributionAction; + private final TypeRegistration contributionAction; private final RegisteredBean source; - public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean, - BiConsumer contributionAction) { + public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, + RegisteredBean registeredBean, TypeRegistration contributionAction) { this.aotContext = aotContext; this.managedTypes = managedTypes; @@ -96,7 +95,7 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContri List> types = sourceTypes.get(); if (!types.isEmpty()) { - TypeCollector.inspect(types).forEach(type -> contributionAction.accept(type, generationContext)); + TypeCollector.inspect(types).forEach(type -> contributionAction.register(type, generationContext, aotContext)); } } @@ -118,6 +117,10 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContri return source; } + interface TypeRegistration { + void register(ResolvableType type, GenerationContext generationContext, AotContext aotContext); + } + /** * Class used to generate the fragment of code needed to define a {@link ManagedTypes} bean from previously discovered * managed types. @@ -145,7 +148,8 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContri } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("Instance", this::generateInstanceFactory); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java index e39c9ffd4..367162f87 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -123,6 +123,8 @@ public class AotRepositoryBeanDefinitionPropertiesDecorator { private CodeBlock buildCallbackBody() { + Assert.state(repositoryContributor.getContributedTypeName() != null, "ContributedTypeName must not be null"); + CodeBlock.Builder callback = CodeBlock.builder(); List arguments = new ArrayList<>(); @@ -146,7 +148,7 @@ public class AotRepositoryBeanDefinitionPropertiesDecorator { List args = new ArrayList<>(); args.add(RepositoryComposition.RepositoryFragments.class); - args.add(repositoryContributor.getContributedTypeName().getCanonicalName()); + args.add(repositoryContributor.getContributedTypeName().getName()); args.addAll(arguments); callback.addStatement("return $T.just(new $L(%s%s))".formatted("$L".repeat(arguments.isEmpty() ? 0 : 1), diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java index 3193aa556..8f28e0b50 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java @@ -48,8 +48,11 @@ import org.springframework.util.Assert; */ class RepositoryConstructorBuilder implements AotRepositoryConstructorBuilder { + @SuppressWarnings("NullAway") private final String beanFactory = AotRepositoryBeanDefinitionPropertiesDecorator.RESERVED_TYPES .get(ResolvableType.forClass(BeanFactory.class)); + + @SuppressWarnings("NullAway") private final String fragmentCreationContext = AotRepositoryBeanDefinitionPropertiesDecorator.RESERVED_TYPES .get(ResolvableType.forClass(RepositoryFactoryBeanSupport.FragmentCreationContext.class)); diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java index cdebdfa1d..0d4ebbd05 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -15,12 +15,10 @@ */ package org.springframework.data.repository.config; -import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -29,8 +27,6 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; -import org.springframework.aop.SpringProxy; -import org.springframework.aop.framework.Advised; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; @@ -39,13 +35,10 @@ import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.DecoratingProxy; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.projection.EntityProjectionIntrospector; @@ -55,9 +48,6 @@ import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.util.Lazy; -import org.springframework.data.util.Predicates; -import org.springframework.data.util.QTypeContributor; -import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.util.Assert; @@ -81,7 +71,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo private final AotRepositoryContext repositoryContext; - private @Nullable RepositoryContributor repositoryContributor; + private @Nullable RepositoryContributor repositoryContributor; private Lazy environment = Lazy.of(StandardEnvironment::new); private @Nullable BiFunction moduleContribution; @@ -238,7 +228,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo } } getRepositoryContext().typeConfigurations() - .forEach(typeConfiguration -> typeConfiguration.contribute(environment.get(), generationContext)); + .forEach(typeConfiguration -> typeConfiguration.contribute(environment.get(), generationContext)); } @Override @@ -308,7 +298,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional> implementation = fragment.getImplementationClass(); + Optional> implementation = fragment.getImplementationClass(); contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java index 108b44fd8..c94b64852 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -25,7 +25,6 @@ import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.annotation.Reflective; @@ -41,10 +40,10 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -89,9 +88,9 @@ public class RepositoryRegistrationAotProcessor return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null; } - @Nullable - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { repositoryContext.getResolvedTypes().stream() .filter(it -> !RepositoryRegistrationAotContribution.isJavaOrPrimitiveType(it)) @@ -145,7 +144,7 @@ public class RepositoryRegistrationAotProcessor return null; } - //TODO: add the hook for customizing bean initialization code here! + // TODO: add the hook for customizing bean initialization code here! return contribution.withModuleContribution((repositoryContext, generationContext) -> { registerReflectiveForAggregateRoot(repositoryContext, generationContext); @@ -196,7 +195,6 @@ public class RepositoryRegistrationAotProcessor protected void contributeType(Class type, GenerationContext generationContext) { TypeContributor.contribute(type, it -> true, generationContext); - } protected Log getLogger() { diff --git a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index 2ece7e020..d16477954 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -28,8 +28,6 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoSettings; - -import org.springframework.aot.hint.TypeReference; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -43,136 +41,135 @@ import org.springframework.util.StringUtils; /** * Unit tests for {@link AotContext}. * - * * @author Mark Paluch * @author Christoph Strobl */ @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) class AotContextUnitTests { - @Mock BeanFactory beanFactory; + @Mock BeanFactory beanFactory; - @Mock AotMappingContext mappingContext; + @Mock AotMappingContext mappingContext; - MockEnvironment mockEnvironment = new MockEnvironment(); + MockEnvironment mockEnvironment = new MockEnvironment(); - @Test // GH-2595 - void shouldContributeAccessorByDefault() { + @Test // GH-2595 + void shouldContributeAccessorByDefault() { - contributeAccessor(Address.class); - verify(mappingContext).contribute(Address.class); - } + contributeAccessor(Address.class); + verify(mappingContext).contribute(Address.class); + } - @Test // GH-2595 - void shouldConsiderDisabledAccessors() { + @Test // GH-2595 + void shouldConsiderDisabledAccessors() { - mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); + mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); - contributeAccessor(Address.class); + contributeAccessor(Address.class); - verifyNoInteractions(mappingContext); - } + verifyNoInteractions(mappingContext); + } - @Test // GH-2595 - void shouldApplyExcludeFilters() { + @Test // GH-2595 + void shouldApplyExcludeFilters() { - mockEnvironment.setProperty("spring.aot.data.accessors.exclude", - Customer.class.getName() + " , " + EmptyType1.class.getName()); + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", + Customer.class.getName() + " , " + EmptyType1.class.getName()); - contributeAccessor(Address.class, Customer.class, EmptyType1.class); + contributeAccessor(Address.class, Customer.class, EmptyType1.class); - verify(mappingContext).contribute(Address.class); - verifyNoMoreInteractions(mappingContext); - } + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } - @Test // GH-2595 - void shouldApplyIncludeExcludeFilters() { + @Test // GH-2595 + void shouldApplyIncludeExcludeFilters() { - mockEnvironment.setProperty("spring.aot.data.accessors.include", Customer.class.getPackageName() + ".Add*"); - mockEnvironment.setProperty("spring.aot.data.accessors.exclude", Customer.class.getPackageName() + ".**"); + mockEnvironment.setProperty("spring.aot.data.accessors.include", Customer.class.getPackageName() + ".Add*"); + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", Customer.class.getPackageName() + ".**"); - contributeAccessor(Address.class, Customer.class, EmptyType1.class); + contributeAccessor(Address.class, Customer.class, EmptyType1.class); - verify(mappingContext).contribute(Address.class); - verifyNoMoreInteractions(mappingContext); - } + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } - private void contributeAccessor(Class... classes) { + private void contributeAccessor(Class... classes) { - DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment); + DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment, mappingContext); - for (Class aClass : classes) { - context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); - } + for (Class aClass : classes) { + context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); + } - context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); - } + context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); + } - @ParameterizedTest // GH-3322 - @CsvSource({ // - "'spring.aot.repositories.enabled', '', '', '', true", // - "'spring.aot.repositories.enabled', 'true', '', '', true", // - "'spring.aot.repositories.enabled', 'false', '', '', false", // - "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // - "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // - }) - void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, - String storeValue, boolean enabled) { + @ParameterizedTest // GH-3322 + @CsvSource({ // + "'spring.aot.repositories.enabled', '', '', '', true", // + "'spring.aot.repositories.enabled', 'true', '', '', true", // + "'spring.aot.repositories.enabled', 'false', '', '', false", // + "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // + "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // + }) + void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, + String storeValue, boolean enabled) { - MockAotContext ctx = new MockAotContext(); - if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { - ctx.withProperty(generalFlag, generalValue); - } - if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { - ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); - } + MockAotContext ctx = new MockAotContext(); + if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { + ctx.withProperty(generalFlag, generalValue); + } + if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { + ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); + } - Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); - } + Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); + } - static class MockAotContext implements AotContext { + static class MockAotContext implements AotContext { - private final MockEnvironment environment; + private final MockEnvironment environment; - public MockAotContext() { - this.environment = new MockEnvironment(); - } + public MockAotContext() { + this.environment = new MockEnvironment(); + } - MockAotContext withProperty(String key, String value) { - environment.setProperty(key, value); - return this; - } + MockAotContext withProperty(String key, String value) { + environment.setProperty(key, value); + return this; + } - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return Mockito.mock(ConfigurableListableBeanFactory.class); - } + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return Mockito.mock(ConfigurableListableBeanFactory.class); + } - @Override - public TypeIntrospector introspectType(String typeName) { - return Mockito.mock(TypeIntrospector.class); - } + @Override + public TypeIntrospector introspectType(String typeName) { + return Mockito.mock(TypeIntrospector.class); + } - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return Mockito.mock(IntrospectedBeanDefinition.class); - } + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return Mockito.mock(IntrospectedBeanDefinition.class); + } - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { - } + } - @Override - public Collection typeConfigurations() { - return List.of(); - } + @Override + public Collection typeConfigurations() { + return List.of(); + } - @Override - public Environment getEnvironment() { - return environment; - } - } + @Override + public Environment getEnvironment() { + return environment; + } + } } diff --git a/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java b/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java index 5ffa62b5e..e531c3404 100755 --- a/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java @@ -15,20 +15,21 @@ */ package org.springframework.data.projection; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.Configuration.ConfigurationBuilder; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; -import org.springframework.lang.Nullable; /** * Integration tests for projections. * * @author Oliver Gierke + * @author Christoph Strobl */ class ProjectionIntegrationTests { @@ -44,7 +45,26 @@ class ProjectionIntegrationTests { assertThat(json.read("$.decoratedClass", String.class)).isNull(); } + @Test // GH-3170 + void jacksonSerializationConsidersJspecifyNullableAnnotations() throws Exception { + + var factory = new ProxyProjectionFactory(); + var projection = factory.createProjection(SampleProjectionJSpecify.class); + + var context = JsonPath.using(new ConfigurationBuilder().options(Option.SUPPRESS_EXCEPTIONS).build()); + var json = context.parse(new ObjectMapper().writeValueAsString(projection)); + + assertThat(json.read("$.decoratedClass", String.class)).isNull(); + } + + @SuppressWarnings("deprecation") interface SampleProjection { - @Nullable String getName(); + @Nullable + String getName(); + } + + interface SampleProjectionJSpecify { + @org.jspecify.annotations.Nullable + String getName(); } } diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java index b6afe7a3f..a9de3afb3 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -27,7 +27,6 @@ import java.util.function.Supplier; import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.function.ThrowingConsumer; -import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.aot.BeanRegistrationCode; diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java index 1226c1640..c01d50310 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -15,12 +15,11 @@ */ package org.springframework.data.repository.aot; -import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; +import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.assertThatContribution; import java.io.Serializable; import org.junit.jupiter.api.Test; - import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; import org.springframework.context.annotation.ComponentScan.Filter; @@ -109,8 +108,8 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { void simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class).forRepository( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class) + .forRepository(ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs( @@ -176,7 +175,7 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomImplementation.class) - .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); + .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class) // @@ -225,15 +224,15 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomRepositoryBaseClass.class) - .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); + .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // .hasFragments() // .codeContributionSatisfies(contribution -> { // // interface - contribution - .contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural fragment + contribution.contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural + // fragment .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type @@ -308,7 +307,7 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { RepositoryRegistrationAotContribution contribution = computeAotConfiguration( InheritedEventPublicationConfiguration.class) - .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); + .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); assertThatContribution(contribution).codeContributionSatisfies(it -> { it.contributesReflectionFor(AbstractAggregateRoot.class); @@ -335,10 +334,8 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { interface SampleRepository extends Repository {} } - @EnableRepositories( - includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, - value = InheritedEventPublicationConfiguration.SampleRepository.class) }, - considerNestedRepositories = true) + @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, + value = InheritedEventPublicationConfiguration.SampleRepository.class) }, considerNestedRepositories = true) public class InheritedEventPublicationConfiguration { static class Sample extends AbstractAggregateRoot {}