diff --git a/pom.xml b/pom.xml
index 13143c9f6f..948f658b11 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-commons
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2595-SNAPSHOTSpring Data CoreCore Spring concepts underpinning every Spring Data module.
diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc
index 9a7ac3241f..92d163d147 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 0000000000..f60d10ece0
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/aot.adoc
@@ -0,0 +1,75 @@
+= 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.include`
+|Comma separated list of FQCN for which to contribute Bytecode for generated Type/Property Accessors.
+Ant-style include patterns matching package names (e.g. `com.acme.**`) or type names inclusion.
+Inclusion pattern matches are evaluated before exclusions for broad exclusion and selective inclusion.
+
+|`spring.aot.data.accessors.exclude`
+|Comma separated list of FQCN for which to skip contribution of Bytecode for generated Type/Property Accessors.
+Ant-style exclude patterns matching package names (e.g. `com.acme.**`) or type names exclusion.
+Exclusion pattern matches are evaluated after inclusions for broad exclusion and selective inclusion.
+
+|`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`, `jpa`, `mongodb`, `cassandra`)
+|===
+
+[[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 3407e07545..b3d204e393 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 67f423ae60..537f801354 100644
--- a/src/main/java/org/springframework/data/aot/AotContext.java
+++ b/src/main/java/org/springframework/data/aot/AotContext.java
@@ -30,6 +30,7 @@
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
@@ -213,6 +214,33 @@ default IntrospectedBeanDefinition introspectBeanDefinition(BeanReference refere
*/
IntrospectedBeanDefinition introspectBeanDefinition(String beanName);
+ /**
+ * Obtain a {@link AotTypeConfiguration} for the given {@link ResolvableType} to customize the AOT processing for the
+ * given type.
+ *
+ * @param resolvableType the resolvable type to configure.
+ * @param configurationConsumer configuration consumer function.
+ */
+ default void typeConfiguration(ResolvableType resolvableType, Consumer configurationConsumer) {
+ typeConfiguration(resolvableType.toClass(), configurationConsumer);
+ }
+
+ /**
+ * Obtain a {@link AotTypeConfiguration} for the given {@link ResolvableType} to customize the AOT processing for the
+ * given type.
+ *
+ * @param type the type to configure.
+ * @param configurationConsumer configuration consumer function.
+ */
+ void typeConfiguration(Class> type, Consumer configurationConsumer);
+
+ /**
+ * Return all type configurations registered with this {@link AotContext}.
+ *
+ * @return all type configurations registered with this {@link AotContext}.
+ */
+ Collection typeConfigurations();
+
/**
* Type-based introspector to resolve {@link Class} from a type name and to introspect the bean factory for presence
* of beans.
@@ -272,7 +300,6 @@ default void ifTypePresent(Consumer> action) {
* @return a {@link List} of bean names. The list is empty if the bean factory does not hold any beans of this type.
*/
List getBeanNames();
-
}
/**
@@ -326,7 +353,6 @@ interface IntrospectedBeanDefinition {
*/
@Nullable
Class> resolveType();
-
}
}
diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java
new file mode 100644
index 0000000000..01d023ca9f
--- /dev/null
+++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2025 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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.data.mapping.Association;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.context.AbstractMappingContext;
+import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
+import org.springframework.data.mapping.model.BasicPersistentEntity;
+import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
+import org.springframework.data.mapping.model.EntityInstantiator;
+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.util.TypeInformation;
+
+/**
+ * Simple {@link AbstractMappingContext} for processing of AOT contributions.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class AotMappingContext extends
+ AbstractMappingContext, AotMappingContext.AotPersistentProperty> {
+
+ private static final Log logger = LogFactory.getLog(AotMappingContext.class);
+
+ private final EntityInstantiators instantiators = new EntityInstantiators();
+ private final AotAccessorFactory propertyAccessorFactory = new AotAccessorFactory();
+
+ /**
+ * Contribute entity instantiators and property accessors for the given {@link PersistentEntity} that are captured
+ * through Spring's {@code CglibClassHandler}. Otherwise, this is a no-op if contributions are not ran through
+ * {@code CglibClassHandler}.
+ *
+ * @param entityType
+ */
+ public void contribute(Class> entityType) {
+
+ BasicPersistentEntity, AotPersistentProperty> entity = getPersistentEntity(entityType);
+
+ if (entity != null) {
+
+ EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
+ if (instantiator instanceof EntityInstantiatorSource source) {
+ source.getInstantiatorFor(entity);
+ }
+
+ propertyAccessorFactory.initialize(entity);
+ }
+ }
+
+ @Override
+ protected BasicPersistentEntity, AotPersistentProperty> 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, AotPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
+ logger.info("creating property: " + property.getName());
+ return new AotPersistentProperty(property, owner, simpleTypeHolder);
+ }
+
+ static class AotPersistentProperty extends AnnotationBasedPersistentProperty {
+
+ public AotPersistentProperty(Property property, PersistentEntity, AotPersistentProperty> owner,
+ SimpleTypeHolder simpleTypeHolder) {
+ super(property, owner, simpleTypeHolder);
+ }
+
+ @Override
+ public boolean isAssociation() {
+ return false;
+ }
+
+ @Override
+ protected Association createAssociation() {
+ return new Association<>(this, null);
+ }
+
+ @Override
+ public Association getAssociation() {
+ return new Association<>(this, null);
+ }
+
+ }
+
+ static class AotAccessorFactory extends ClassGeneratingPropertyAccessorFactory {
+
+ public void initialize(PersistentEntity, ?> entity) {
+ potentiallyCreateAndRegisterPersistentPropertyAccessorClass(entity);
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java
new file mode 100644
index 0000000000..e9953e9b43
--- /dev/null
+++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java
@@ -0,0 +1,144 @@
+/*
+ * 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
+ *
+ * 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.List;
+import java.util.stream.Stream;
+
+import org.springframework.aop.SpringProxy;
+import org.springframework.aop.framework.Advised;
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.core.DecoratingProxy;
+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();
+
+ /**
+ * 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();
+
+ List transactionalProxy = List.of(TypeReference.of("org.springframework.data.repository.Repository"),
+ TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"),
+ TypeReference.of("org.springframework.aop.framework.Advised"), TypeReference.of(DecoratingProxy.class));
+ proxyInterface(transactionalProxy);
+
+ proxyInterface(
+ Stream.concat(transactionalProxy.stream(), Stream.of(TypeReference.of(Serializable.class))).toList());
+
+ 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 65cb8ab544..fcffdaa0ae 100644
--- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java
+++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java
@@ -15,13 +15,24 @@
*/
package org.springframework.data.aot;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+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;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -29,24 +40,38 @@
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.env.Environment;
+import org.springframework.data.util.Lazy;
+import org.springframework.data.util.QTypeContributor;
+import org.springframework.data.util.TypeContributor;
+import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
+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;
private final ConfigurableListableBeanFactory factory;
+ // 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;
public DefaultAotContext(BeanFactory beanFactory, Environment environment) {
- factory = beanFactory instanceof ConfigurableListableBeanFactory cbf ? cbf
+ 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
@@ -69,6 +94,16 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) {
return new DefaultIntrospectedBeanDefinition(beanName);
}
+ @Override
+ public void typeConfiguration(Class> type, Consumer configurationConsumer) {
+ configurationConsumer.accept(typeConfigurations.computeIfAbsent(type, it -> new ContextualTypeConfiguration(type)));
+ }
+
+ @Override
+ public Collection typeConfigurations() {
+ return typeConfigurations.values();
+ }
+
class DefaultTypeIntrospector implements TypeIntrospector {
private final String typeName;
@@ -148,4 +183,150 @@ public RootBeanDefinition getRootBeanDefinition() throws NoSuchBeanDefinitionExc
}
}
+ class ContextualTypeConfiguration implements AotTypeConfiguration {
+
+ private final Class> type;
+ private boolean forDataBinding = false;
+ private final Set categories = new HashSet<>(5);
+ private boolean contributeAccessors = false;
+ private boolean forQuerydsl = false;
+ private final List> proxies = new ArrayList<>();
+
+ ContextualTypeConfiguration(Class> type) {
+ this.type = type;
+ }
+
+ @Override
+ public AotTypeConfiguration forDataBinding() {
+ this.forDataBinding = true;
+ return this;
+ }
+
+ @Override
+ public AotTypeConfiguration forReflectiveAccess(MemberCategory... categories) {
+ this.categories.addAll(Arrays.asList(categories));
+ return this;
+ }
+
+ @Override
+ public AotTypeConfiguration contributeAccessors() {
+ this.contributeAccessors = true;
+ return this;
+ }
+
+ @Override
+ public AotTypeConfiguration proxyInterface(List interfaces) {
+ this.proxies.add(interfaces);
+ return this;
+ }
+
+ @Override
+ public AotTypeConfiguration forQuerydsl() {
+ this.forQuerydsl = true;
+ return this;
+ }
+
+ @Override
+ public void contribute(Environment environment, GenerationContext generationContext) {
+
+ if (!this.categories.isEmpty()) {
+ generationContext.getRuntimeHints().reflection().registerType(this.type,
+ categories.toArray(MemberCategory[]::new));
+ }
+
+ if (contributeAccessors) {
+
+ AccessorContributionConfiguration configuration = AccessorContributionConfiguration.of(environment);
+ if (configuration.shouldContributeAccessors(type)) {
+ mappingContext.contribute(type);
+ }
+ }
+
+ if (forDataBinding) {
+ TypeContributor.contribute(type, Set.of(TypeContributor.DATA_NAMESPACE), generationContext);
+ }
+
+ if (forQuerydsl) {
+ QTypeContributor.contributeEntityPath(type, generationContext, factory.getBeanClassLoader());
+ }
+
+ if (!proxies.isEmpty()) {
+ for (List proxyInterfaces : proxies) {
+ generationContext.getRuntimeHints().proxies().registerJdkProxy(
+ Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()).toArray(TypeReference[]::new));
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Configuration for accessor to determine whether accessors should be contributed for a given type.
+ */
+ private record AccessorContributionConfiguration(boolean enabled, Lazy include, Lazy exclude) {
+
+ /**
+ * {@code boolean }Environment property to enable/disable accessor contribution. Enabled by default.
+ */
+ public static final String ACCESSORS_ENABLED = "spring.aot.data.accessors.enabled";
+
+ /**
+ * {@code String} Environment property to define Ant-style include patterns (comma-separated) matching package names
+ * (e.g. {@code com.acme.**}) or type names inclusion. Inclusion pattern matches are evaluated before exclusions for
+ * broad exclusion and selective inclusion.
+ */
+ public static final String INCLUDE_PATTERNS = "spring.aot.data.accessors.include";
+
+ /**
+ * {@code String} Environment property to define Ant-style exclude patterns (comma-separated) matching package names
+ * (e.g. {@code com.acme.**}) or type names exclusion. Exclusion pattern matches are evaluated after inclusions for
+ * broad exclusion and selective inclusion.
+ */
+ public static final String EXCLUDE_PATTERNS = "spring.aot.data.accessors.exclude";
+
+ private static final AntPathMatcher antPathMatcher = new AntPathMatcher(".");
+
+ private AccessorContributionConfiguration(boolean enabled, Supplier include, Supplier exclude) {
+ this(enabled, Lazy.of(include), Lazy.of(exclude));
+ }
+
+ public static AccessorContributionConfiguration of(Environment environment) {
+ return new AccessorContributionConfiguration(environment.getProperty(ACCESSORS_ENABLED, Boolean.class, true),
+ () -> environment.getProperty(INCLUDE_PATTERNS, String.class, ""),
+ () -> environment.getProperty(EXCLUDE_PATTERNS, String.class, ""));
+ }
+
+ boolean shouldContributeAccessors(Class> type) {
+
+ if (!enabled) {
+ return false;
+ }
+
+ if (StringUtils.hasText(include.get())) {
+
+ String[] includes = include.get().split(",");
+
+ for (String includePattern : includes) {
+ if (antPathMatcher.match(includePattern.trim(), type.getName())) {
+ return true;
+ }
+ }
+ }
+
+ if (StringUtils.hasText(exclude.get())) {
+
+ String[] excludes = exclude.get().split(",");
+
+ for (String excludePattern : excludes) {
+ if (antPathMatcher.match(excludePattern.trim(), type.getName())) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ }
+
}
diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
index 0bc6cd3ba6..e6935c5bb4 100644
--- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
+++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java
@@ -22,10 +22,8 @@
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.BeanFactory;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
@@ -37,7 +35,6 @@
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.util.Lazy;
-import org.springframework.data.util.QTypeContributor;
import org.springframework.data.util.TypeContributor;
import org.springframework.data.util.TypeUtils;
import org.springframework.util.ClassUtils;
@@ -68,7 +65,7 @@ public String getModuleIdentifier() {
@Override
public void setEnvironment(Environment environment) {
- this.environment = Lazy.of(() -> environment);
+ this.environment = Lazy.of(environment);
}
@Override
@@ -78,9 +75,8 @@ public void setEnvironment(Environment environment) {
return null;
}
- BeanFactory beanFactory = registeredBean.getBeanFactory();
- return contribute(AotContext.from(beanFactory, this.environment.get()), resolveManagedTypes(registeredBean),
- registeredBean);
+ DefaultAotContext aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get());
+ return contribute(aotContext, resolveManagedTypes(registeredBean), registeredBean);
}
private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) {
@@ -121,7 +117,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) {
/**
* 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}.
@@ -129,7 +125,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) {
*/
protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes,
RegisteredBean registeredBean) {
- return new ManagedTypesRegistrationAotContribution(managedTypes, registeredBean, this::contributeType);
+ return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, this::contributeType);
}
/**
@@ -138,7 +134,7 @@ protected BeanRegistrationAotContribution contribute(AotContext aotContext, Mana
* @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()));
@@ -146,11 +142,11 @@ protected void contributeType(ResolvableType type, GenerationContext generationC
Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE);
- Class> resolvedType = type.toClass();
- TypeContributor.contribute(resolvedType, annotationNamespaces, generationContext);
- QTypeContributor.contributeEntityPath(resolvedType, generationContext, resolvedType.getClassLoader());
+ aotContext.typeConfiguration(type, config -> config.forDataBinding() //
+ .contributeAccessors() //
+ .forQuerydsl().contribute(environment.get(), generationContext));
- TypeUtils.resolveUsedAnnotations(resolvedType).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 1e59e7e852..e86f8c161f 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 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;
@@ -74,14 +73,16 @@
*/
class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContribution {
+ 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(ManagedTypes managedTypes, RegisteredBean registeredBean,
- BiConsumer contributionAction) {
+ public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes,
+ RegisteredBean registeredBean, TypeRegistration contributionAction) {
+ this.aotContext = aotContext;
this.managedTypes = managedTypes;
this.sourceTypes = Lazy.of(managedTypes::toList);
this.contributionAction = contributionAction;
@@ -94,7 +95,7 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be
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));
}
}
@@ -116,6 +117,10 @@ public RegisteredBean getSource() {
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.
@@ -143,7 +148,8 @@ protected ManagedTypesInstanceCodeFragment(List> sourceTypes, Registere
}
@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/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
index 5ba05b4a02..feaedac470 100644
--- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
+++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
@@ -56,7 +56,6 @@
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.PropertyPath;
-import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mapping.model.InstantiationAwarePropertyAccessorFactory;
@@ -125,7 +124,7 @@ protected AbstractMappingContext() {
EntityInstantiators instantiators = new EntityInstantiators();
PersistentPropertyAccessorFactory accessorFactory = NativeDetector.inNativeImage()
- ? BeanWrapperPropertyAccessorFactory.INSTANCE
+ ? new ReflectionFallbackPersistentPropertyAccessorFactory()
: new ClassGeneratingPropertyAccessorFactory();
this.persistentPropertyAccessorFactory = new InstantiationAwarePropertyAccessorFactory(accessorFactory,
@@ -244,6 +243,7 @@ public Collection getPersistentEntities() {
@Override
@Nullable
public E getPersistentEntity(Class> type) {
+ LOGGER.info("obtain persistent entity for type: " + type);
return getPersistentEntity(TypeInformation.of(type));
}
diff --git a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java
new file mode 100644
index 0000000000..6640f925e3
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 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.mapping.context;
+
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
+import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
+import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
+
+/**
+ * {@link PersistentPropertyAccessorFactory} that uses {@link ClassGeneratingPropertyAccessorFactory} if
+ * {@link ClassGeneratingPropertyAccessorFactory#isSupported(PersistentEntity) supported} and falls back to reflection.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class ReflectionFallbackPersistentPropertyAccessorFactory implements PersistentPropertyAccessorFactory {
+
+ private final ClassGeneratingPropertyAccessorFactory accessorFactory = new ClassGeneratingPropertyAccessorFactory();
+
+ @Override
+ public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity, ?> entity, T bean) {
+
+ if (accessorFactory.isSupported(entity)) {
+ return accessorFactory.getPropertyAccessor(entity, bean);
+ }
+
+ return BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(entity, bean);
+ }
+
+ @Override
+ public boolean isSupported(PersistentEntity, ?> entity) {
+ return true;
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
index 952fa0e9a6..589e225f74 100644
--- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
@@ -51,7 +51,7 @@
* An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation. Uses the
* {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by dynamically
* generating factory methods with appropriate constructor invocations via ASM. If we cannot generate byte code for a
- * type, we gracefully fallback to the {@link ReflectionEntityInstantiator}.
+ * type, we gracefully fall back to the {@link ReflectionEntityInstantiator}.
*
* @author Thomas Darimont
* @author Oliver Gierke
@@ -60,7 +60,7 @@
* @author Mark Paluch
* @since 1.11
*/
-class ClassGeneratingEntityInstantiator implements EntityInstantiator {
+class ClassGeneratingEntityInstantiator implements EntityInstantiator, EntityInstantiatorSource {
private static final Log LOGGER = LogFactory.getLog(ClassGeneratingEntityInstantiator.class);
@@ -91,13 +91,25 @@ public ClassGeneratingEntityInstantiator() {
public , P extends PersistentProperty
> T createInstance(E entity,
ParameterValueProvider