diff --git a/pom.xml b/pom.xml index 13143c9f6f..948f658b11 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-GH-2595-SNAPSHOT Spring Data Core Core 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 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 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); + } + + static class AotPersistentProperty extends AnnotationBasedPersistentProperty { + + public AotPersistentProperty(Property property, PersistentEntity 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

provider) { + EntityInstantiator instantiator = getEntityInstantiator(entity); + return instantiator.createInstance(entity, provider); + } + + @Override + public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { + return getEntityInstantiator(entity); + } + + private , P extends PersistentProperty

> EntityInstantiator getEntityInstantiator( + E entity) { + EntityInstantiator instantiator = this.entityInstantiators.get(entity.getTypeInformation()); if (instantiator == null) { instantiator = potentiallyCreateAndRegisterEntityInstantiator(entity); } - return instantiator.createInstance(entity, provider); + return instantiator; } /** @@ -170,10 +182,19 @@ protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity e */ boolean shouldUseReflectionEntityInstantiator(PersistentEntity entity) { + String accessorClassName = ObjectInstantiatorClassGenerator.generateClassName(entity); + + // already present in classloader + if (ClassUtils.isPresent(accessorClassName, entity.getType().getClassLoader())) { + return false; + } + if (NativeDetector.inNativeImage()) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("graalvm.nativeimage - fall back to reflection for %s", entity.getName())); + LOGGER.debug(String.format( + "[org.graalvm.nativeimage.imagecode=true] and no AOT-generated EntityInstantiator for %s. Falling back to reflection.", + entity.getName())); } return true; @@ -388,7 +409,7 @@ public , P extends PersistentPrope static class ObjectInstantiatorClassGenerator { private static final String INIT = ""; - private static final String TAG = "_Instantiator_"; + private static final String TAG = "__Instantiator_"; private static final String JAVA_LANG_OBJECT = Type.getInternalName(Object.class); private static final String CREATE_METHOD_NAME = "newInstance"; @@ -431,8 +452,8 @@ public Class generateCustomInstantiatorClass(PersistentEntity entity, * @param entity * @return */ - private String generateClassName(PersistentEntity entity) { - return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + static String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(Math.abs(entity.getType().getName().hashCode()), 36); } /** diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java index 0e6047d7aa..39f102a309 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -76,7 +76,8 @@ * @author Johannes Englmeier * @since 1.13 */ -public class ClassGeneratingPropertyAccessorFactory implements PersistentPropertyAccessorFactory { +public class ClassGeneratingPropertyAccessorFactory + implements PersistentPropertyAccessorFactory { // Pooling of parameter arrays to prevent excessive object allocation. private final ThreadLocal argumentCache = ThreadLocal.withInitial(() -> new Object[1]); @@ -91,18 +92,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert @Override public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { - Constructor constructor = constructorMap.get(entity); - - if (constructor == null) { - - Class> accessorClass = potentiallyCreateAndRegisterPersistentPropertyAccessorClass( - entity); - constructor = accessorClass.getConstructors()[0]; - - Map, Constructor> constructorMap = new HashMap<>(this.constructorMap); - constructorMap.put(entity, constructor); - this.constructorMap = constructorMap; - } + Constructor constructor = getPropertyAccessorConstructor(entity); Object[] args = argumentCache.get(); args[0] = bean; @@ -123,6 +113,24 @@ public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity getPropertyAccessorConstructor(PersistentEntity entity) { + + Constructor constructor = constructorMap.get(entity); + + if (constructor == null) { + + Class> accessorClass = potentiallyCreateAndRegisterPersistentPropertyAccessorClass( + entity); + constructor = accessorClass.getConstructors()[0]; + + Map, Constructor> constructorMap = new HashMap<>(this.constructorMap); + constructorMap.put(entity, constructor); + this.constructorMap = constructorMap; + } + + return constructor; + } + /** * Checks whether an accessor class can be generated. * @@ -136,6 +144,11 @@ public boolean isSupported(PersistentEntity entity) { Assert.notNull(entity, "PersistentEntity must not be null"); + // already present in classloader + if (findAccessorClass(entity) != null) { + return true; + } + return isClassLoaderDefineClassAvailable(entity) && isTypeInjectable(entity) && hasUniquePropertyHashCodes(entity); } @@ -184,7 +197,7 @@ private boolean hasUniquePropertyHashCodes(PersistentEntity entity) { /** * @param entity must not be {@literal null}. */ - private synchronized Class> potentiallyCreateAndRegisterPersistentPropertyAccessorClass( + protected synchronized Class> potentiallyCreateAndRegisterPersistentPropertyAccessorClass( PersistentEntity entity) { Map, Class>> map = this.propertyAccessorClasses; @@ -194,7 +207,7 @@ private synchronized Class> potentiallyCreateAndRe return propertyAccessorClass; } - propertyAccessorClass = createAccessorClass(entity); + propertyAccessorClass = loadOrCreateAccessorClass(entity); map = new HashMap<>(map); map.put(entity.getTypeInformation(), propertyAccessorClass); @@ -204,16 +217,29 @@ private synchronized Class> potentiallyCreateAndRe return propertyAccessorClass; } - @SuppressWarnings("unchecked") - private Class> createAccessorClass(PersistentEntity entity) { + @SuppressWarnings({ "unchecked" }) + private Class> loadOrCreateAccessorClass(PersistentEntity entity) { try { + + Class accessorClass = findAccessorClass(entity); + if (accessorClass != null) { + return (Class>) accessorClass; + } + return (Class>) PropertyAccessorClassGenerator.generateCustomAccessorClass(entity); } catch (Exception e) { throw new RuntimeException(e); } } + private static @Nullable Class findAccessorClass(PersistentEntity entity) { + + String accessorClassName = PropertyAccessorClassGenerator.generateClassName(entity); + + return org.springframework.data.util.ClassUtils.loadIfPresent(accessorClassName, entity.getType().getClassLoader()); + } + /** * Generates {@link PersistentPropertyAccessor} classes to access properties of a {@link PersistentEntity}. This code * uses {@code private static final} held method handles which perform about the speed of native method invocations @@ -306,7 +332,7 @@ static class PropertyAccessorClassGenerator { private static final String INIT = ""; private static final String CLINIT = ""; - private static final String TAG = "_Accessor_"; + private static final String TAG = "__Accessor_"; private static final String JAVA_LANG_OBJECT = "java/lang/Object"; private static final String JAVA_LANG_STRING = "java/lang/String"; private static final String JAVA_LANG_REFLECT_METHOD = "java/lang/reflect/Method"; @@ -347,7 +373,6 @@ static Class generateCustomAccessorClass(PersistentEntity entity) { try { return ReflectUtils.defineClass(className, bytecode, classLoader, type.getProtectionDomain(), type); - } catch (Exception o_O) { throw new IllegalStateException(o_O); } @@ -1372,8 +1397,8 @@ private static int classVariableIndex5(List> list, Class item) { return 5 + list.indexOf(item); } - private static String generateClassName(PersistentEntity entity) { - return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + static String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(Math.abs(entity.getType().getName().hashCode()), 36); } } diff --git a/src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java new file mode 100644 index 0000000000..b43afd2f8f --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java @@ -0,0 +1,36 @@ +/* + * 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.model; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * Interface declaring a source for {@link EntityInstantiator} objects. + * + * @author Mark Paluch + * @since 4.0 + */ +@FunctionalInterface +public interface EntityInstantiatorSource { + + /** + * Returns an {@link EntityInstantiator} for the given {@link PersistentEntity}. + * + * @return the {@link EntityInstantiator} for the given {@link PersistentEntity}. + */ + EntityInstantiator getInstantiatorFor(PersistentEntity entity); + +} diff --git a/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java index 4fd4f2cfad..71cfe042b6 100644 --- a/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java @@ -31,7 +31,7 @@ * @author Mark Paluch * @since 2.3 */ -public class EntityInstantiators { +public class EntityInstantiators implements EntityInstantiatorSource { private final EntityInstantiator fallback; private final Map, EntityInstantiator> customInstantiators; @@ -84,6 +84,7 @@ public EntityInstantiators(EntityInstantiator defaultInstantiator, * @param entity must not be {@literal null}. * @return will never be {@literal null}. */ + @Override public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { Assert.notNull(entity, "Entity must not be null"); 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 e39c9ffd4a..367162f879 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 CodeBlock decorate() { 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 @@ private CodeBlock buildCallbackBody() { 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 3193aa5563..8f28e0b506 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 @@ */ 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/AotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java index 0a89486886..c0eac7cf29 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -79,4 +79,6 @@ default Set getBasePackages() { */ Set> getResolvedTypes(); + Set> getUserDomainTypes(); + } diff --git a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java index ccf60a01e5..4c1d0e5384 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -27,9 +28,11 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeCollector; +import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; /** @@ -133,11 +136,29 @@ public Set> getResolvedTypes() { return managedTypes.get(); } + @Override + public Set> getUserDomainTypes() { + + return getResolvedTypes().stream() + .filter(it -> TypeContributor.isPartOf(it, Set.of(repositoryInformation.getDomainType().getPackageName()))) + .collect(Collectors.toSet()); + } + @Override public AotContext.TypeIntrospector introspectType(String typeName) { return aotContext.introspectType(typeName); } + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + aotContext.typeConfiguration(type, configurationConsumer); + } + + @Override + public Collection typeConfigurations() { + return aotContext.typeConfigurations(); + } + @Override public AotContext.IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return aotContext.introspectBeanDefinition(beanName); 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 bd2abb6706..0d4ebbd053 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -15,7 +15,6 @@ */ package org.springframework.data.repository.config; -import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,8 +27,6 @@ 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,23 +36,20 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.DecoratingProxy; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.projection.TargetAware; import org.springframework.data.repository.Repository; import org.springframework.data.repository.aot.generate.AotRepositoryBeanDefinitionPropertiesDecorator; 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.Predicates; -import org.springframework.data.util.QTypeContributor; -import org.springframework.data.util.TypeContributor; +import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeUtils; import org.springframework.javapoet.CodeBlock; -import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -67,7 +61,7 @@ * @author Mark Paluch * @since 3.0 */ -public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { +public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution, EnvironmentAware { private static final Log logger = LogFactory.getLog(RepositoryRegistrationAotContribution.class); @@ -78,6 +72,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo private final AotRepositoryContext repositoryContext; private @Nullable RepositoryContributor repositoryContributor; + private Lazy environment = Lazy.of(StandardEnvironment::new); private @Nullable BiFunction moduleContribution; @@ -213,6 +208,11 @@ public RepositoryRegistrationAotContribution withModuleContribution( return this; } + @Override + public void setEnvironment(Environment environment) { + this.environment = Lazy.of(environment); + } + @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { @@ -227,6 +227,8 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be this.repositoryContributor.contribute(generationContext); } } + getRepositoryContext().typeConfigurations() + .forEach(typeConfiguration -> typeConfiguration.contribute(environment.get(), generationContext)); } @Override @@ -255,27 +257,44 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener }; } - public Predicate> typeFilter() { // like only document ones. // TODO: As in MongoDB? - return Predicates.isTrue(); - } - private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) { RepositoryInformation repositoryInformation = getRepositoryInformation(); logTrace("Contributing repository information for [%s]", repositoryInformation.getRepositoryInterface()); - contribution.getRuntimeHints().reflection() - .registerType(repositoryInformation.getRepositoryInterface(), - hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)) - .registerType(repositoryInformation.getRepositoryBaseClass(), hint -> hint - .withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface(), + config -> config.forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS).repositoryProxy()); + + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass(), config -> config + .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); - TypeContributor.contribute(repositoryInformation.getDomainType(), contribution); - QTypeContributor.contributeEntityPath(repositoryInformation.getDomainType(), contribution, - repositoryContext.getClassLoader()); + repositoryContext.typeConfiguration(repositoryInformation.getDomainType(), + config -> config.forDataBinding().forQuerydsl()); + + // TODO: purposeful api for uses cases to have some internal logic + repositoryContext.getUserDomainTypes() // + .forEach(it -> repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors)); // Repository Fragments + contributeFragments(contribution); + + // Kotlin + if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { + contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), hint -> {}); + } + + // Repository query methods + repositoryInformation.getQueryMethods().stream().map(repositoryInformation::getReturnedDomainClass) + .filter(Class::isInterface).forEach(type -> { + if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, + repositoryInformation.getDomainType())) { + repositoryContext.typeConfiguration(type, AotTypeConfiguration::usedAsProjectionInterface); + } + }); + } + + private void contributeFragments(GenerationContext contribution) { for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { Class repositoryFragmentType = fragment.getSignatureContributor(); @@ -301,50 +320,6 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge }); }); } - - // Repository Proxy - contribution.getRuntimeHints().proxies().registerJdkProxy(repositoryInformation.getRepositoryInterface(), - SpringProxy.class, Advised.class, DecoratingProxy.class); - - // Transactional Repository Proxy - // repositoryContext.ifTransactionManagerPresent(transactionManagerBeanNames -> { - - // TODO: Is the following double JDK Proxy registration above necessary or would a single JDK Proxy - // registration suffice? - // In other words, simply having a single JDK Proxy registration either with or without - // the additional Serializable TypeReference? - // NOTE: Using a single JDK Proxy registration causes the - // simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() test case method to fail. - List transactionalRepositoryProxyTypeReferences = transactionalRepositoryProxyTypeReferences( - repositoryInformation); - - contribution.getRuntimeHints().proxies() - .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); - - if (isComponentAnnotatedRepository(repositoryInformation)) { - transactionalRepositoryProxyTypeReferences.add(TypeReference.of(Serializable.class)); - contribution.getRuntimeHints().proxies() - .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); - } - // }); - - // Kotlin - if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { - contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), hint -> {}); - } - - // Repository query methods - repositoryInformation.getQueryMethods().stream().map(repositoryInformation::getReturnedDomainClass) - .filter(Class::isInterface).forEach(type -> { - if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, - repositoryInformation.getDomainType())) { - contributeProjection(type, contribution); - } - }); - } - - private boolean isComponentAnnotatedRepository(RepositoryInformation repositoryInformation) { - return AnnotationUtils.findAnnotation(repositoryInformation.getRepositoryInterface(), Component.class) != null; } private boolean isKotlinCoroutineRepository(AotRepositoryContext repositoryContext, @@ -367,21 +342,6 @@ private List kotlinRepositoryReflectionTypeReferences() { TypeReference.of("kotlin.Boolean"))); } - private List transactionalRepositoryProxyTypeReferences(RepositoryInformation repositoryInformation) { - - return new ArrayList<>(Arrays.asList(TypeReference.of(repositoryInformation.getRepositoryInterface()), - TypeReference.of(Repository.class), // - TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"), // - TypeReference.of("org.springframework.aop.framework.Advised"), // - TypeReference.of(DecoratingProxy.class))); - } - - private void contributeProjection(Class type, GenerationContext generationContext) { - - generationContext.getRuntimeHints().proxies().registerJdkProxy(type, TargetAware.class, SpringProxy.class, - DecoratingProxy.class); - } - static boolean isJavaOrPrimitiveType(Class type) { return TypeUtils.type(type).isPartOf("java") // || ClassUtils.isPrimitiveOrWrapper(type) // 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 4fbb086106..c94b648522 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 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; @@ -44,6 +43,7 @@ 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,7 +89,8 @@ public class RepositoryRegistrationAotProcessor } @Nullable - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { repositoryContext.getResolvedTypes().stream() .filter(it -> !RepositoryRegistrationAotContribution.isJavaOrPrimitiveType(it)) @@ -109,6 +110,8 @@ protected RepositoryContributor contribute(AotRepositoryContext repositoryContex * @param repositoryContext must not be {@literal null}. * @param generationContext must not be {@literal null}. */ + // TODO: Can we merge #contribute, #registerReflectiveForAggregateRoot into RepositoryRegistrationAotContribution? + // hints and types are contributed from everywhere. private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -117,7 +120,13 @@ private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryC RuntimeHints hints = generationContext.getRuntimeHints(); Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) - .forEach(it -> registrar.registerRuntimeHints(hints, it)); + .forEach(it -> { + + // arent we already registering the types in RepositoryRegistrationAotContribution#contributeRepositoryInfo? + registrar.registerRuntimeHints(hints, it); + + repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors); + }); } private boolean isRepositoryBean(RegisteredBean bean) { @@ -135,7 +144,7 @@ private boolean isRepositoryBean(RegisteredBean bean) { 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); diff --git a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index 4c257440f1..a943aded02 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -15,22 +15,98 @@ */ package org.springframework.data.aot; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; 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.test.generate.TestGenerationContext; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.Environment; +import org.springframework.data.aot.types.Address; +import org.springframework.data.aot.types.Customer; +import org.springframework.data.aot.types.EmptyType1; import org.springframework.mock.env.MockEnvironment; import org.springframework.util.StringUtils; /** - * Tests for {@link AotContext}. + * Unit tests for {@link AotContext}. * + * @author Mark Paluch * @author Christoph Strobl */ +@MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) class AotContextUnitTests { + @Mock BeanFactory beanFactory; + + @Mock AotMappingContext mappingContext; + + MockEnvironment mockEnvironment = new MockEnvironment(); + + @Test // GH-2595 + void shouldContributeAccessorByDefault() { + + contributeAccessor(Address.class); + verify(mappingContext).contribute(Address.class); + } + + @Test // GH-2595 + void shouldConsiderDisabledAccessors() { + + mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); + + contributeAccessor(Address.class); + + verifyNoInteractions(mappingContext); + } + + @Test // GH-2595 + void shouldApplyExcludeFilters() { + + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", + Customer.class.getName() + " , " + EmptyType1.class.getName()); + + contributeAccessor(Address.class, Customer.class, EmptyType1.class); + + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } + + @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() + ".**"); + + contributeAccessor(Address.class, Customer.class, EmptyType1.class); + + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } + + private void contributeAccessor(Class... classes) { + + DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment, mappingContext); + + for (Class aClass : classes) { + context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); + } + + context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); + } + @ParameterizedTest // GH-3322 @CsvSource({ // "'spring.aot.repositories.enabled', '', '', '', true", // @@ -83,6 +159,16 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return Mockito.mock(IntrospectedBeanDefinition.class); } + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + @Override public Environment getEnvironment() { return environment; diff --git a/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java new file mode 100644 index 0000000000..4a165555d7 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java @@ -0,0 +1,44 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +public class AotMappingContextUnitTests { + + @Test // GH-2595 + void obtainEntityWithReference() { + new AotMappingContext().getPersistentEntity(TypeInformation.of(DemoEntity.class)); + } + + static class DemoEntity { + + @Id String id; + String name; + + @Reference ReferencedEntity referencedEntity; + } + + static class ReferencedEntity { + @Id String id; + } +} diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java index 1bf8817bb8..ac15597ed6 100644 --- a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -15,15 +15,18 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.assertj.core.api.AbstractAssert; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -33,6 +36,7 @@ * * @author Christoph Strobl * @author John Blum + * @author Mark Paluch * @since 3.0 */ @SuppressWarnings("UnusedReturnValue") @@ -52,6 +56,19 @@ public CodeContributionAssert contributesReflectionFor(Class... types) { return this; } + public CodeContributionAssert contributesReflectionFor(TypeReference typeReference) { + + assertThat(this.actual.getRuntimeHints()).describedAs(() -> { + + return "Existing hints: " + System.lineSeparator() + this.actual().getRuntimeHints().reflection().typeHints() + .map(TypeHint::toString).map(" - "::concat).collect(Collectors.joining(System.lineSeparator())); + + }).matches(RuntimeHintsPredicates.reflection().onType(typeReference), + String.format("No reflection entry found for [%s]", typeReference)); + + return this; + } + public CodeContributionAssert contributesReflectionFor(String... types) { for (String type : types) { diff --git a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java index 708139c04a..896abf21b5 100755 --- a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java @@ -15,14 +15,29 @@ */ package org.springframework.data.mapping.context; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import groovy.lang.MetaClass; import java.time.LocalDateTime; -import java.util.*; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java b/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java index 5ffa62b5e8..e531c3404b 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 @@ void jacksonSerializationDoesNotExposeDecoratedClass() throws Exception { 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/AotUtil.java b/src/test/java/org/springframework/data/repository/aot/AotUtil.java new file mode 100644 index 0000000000..042ada7c64 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/AotUtil.java @@ -0,0 +1,83 @@ +/* + * 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.repository.aot; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; +import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; + +/** + * Utility class to create {@link RepositoryRegistrationAotContribution} instances for a given configuration class. + * + * @author Mark Paluch + */ +class AotUtil { + + static RepositoryRegistrationAotContributionBuilder contributionFor(Class configuration) { + return contributionFor(configuration, new AnnotationConfigApplicationContext()); + } + + static RepositoryRegistrationAotContributionBuilder contributionFor(Class configuration, + AnnotationConfigApplicationContext applicationContext) { + + applicationContext.register(configuration); + applicationContext.refreshForAotProcessing(new RuntimeHints()); + + return repositoryTypes -> { + + BeanRegistrationAotContribution beanContribution = null; + + for (Class repositoryType : repositoryTypes) { + + String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); + + assertThat(repositoryBeanNames) + .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) + .hasSize(1); + + String repositoryBeanName = repositoryBeanNames[0]; + + ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); + + RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext + .getBean(RepositoryRegistrationAotProcessor.class); + + repositoryAotProcessor.setBeanFactory(beanFactory); + + RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); + beanContribution = repositoryAotProcessor.processAheadOfTime(bean); + } + + assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); + return (RepositoryRegistrationAotContribution) beanContribution; + }; + } + + @FunctionalInterface + interface RepositoryRegistrationAotContributionBuilder { + default RepositoryRegistrationAotContribution forRepository(Class repositoryInterface) { + return forRepositories(repositoryInterface); + } + + RepositoryRegistrationAotContribution forRepositories(Class... repositoryInterface); + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java new file mode 100644 index 0000000000..7d7c0aa670 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2022-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.repository.aot; + +import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.assertThatContribution; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.TypeReference; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.aot.types.BaseEntity; +import org.springframework.data.aot.types.CyclicPropertiesA; +import org.springframework.data.aot.types.CyclicPropertiesB; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo1; +import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo2; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; +import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; + +/** + * Integration Tests for {@link RepositoryRegistrationAotProcessor} to verify capturing generated instantiations and + * property accessors. + * + * @author Mark Paluch + * @author Christoph Strobl + */ +public class GeneratedClassesCaptureIntegrationTests { + + @Test // GH-2595 + void registersGeneratedPropertyAccessorsEntityInstantiators() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil.contributionFor(Config.class) + .forRepository(Config.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.BaseEntity__Accessor_m5hoaa")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.BaseEntity__Instantiator_m5hoaa")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.Address__Accessor_rf1iey")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.Address__Instantiator_rf1iey")); + }); + } + + @Test // GH-2595 + @Disabled("caching issue in ClassGeneratingEntityInstantiator") + void registersGeneratedPropertyAccessorsEntityInstantiatorsForCyclicProperties() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil + .contributionFor(ConfigWithCyclicReferences.class).forRepository(ConfigWithCyclicReferences.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Accessor_o13htw")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Instantiator_o13htw")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Accessor_o13htx")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Instantiator_o13htx")); + }); + } + + @Test // GH-2595 + void registersGeneratedPropertyAccessorsEntityInstantiatorsForMultipleRepositoriesReferencingEachOther() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil + .contributionFor(ConfigWithMultipleRepositories.class) + .forRepositories(ConfigWithMultipleRepositories.Repo1.class, ConfigWithMultipleRepositories.Repo2.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Accessor_o13htw")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Instantiator_o13htw")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Accessor_o13htx")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Instantiator_o13htx")); + }); + } + + @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = Config.MyRepo.class) }, + basePackageClasses = Config.class, considerNestedRepositories = true) + public static class Config { + + public interface MyRepo extends CrudRepository { + + } + } + + @EnableRepositories( + includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = ConfigWithCyclicReferences.MyRepo.class) }, + basePackageClasses = ConfigWithCyclicReferences.class, considerNestedRepositories = true) + public static class ConfigWithCyclicReferences { + + public interface MyRepo extends CrudRepository { + + } + } + + @EnableRepositories( + includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = { Repo1.class, Repo2.class }) }, + basePackageClasses = ConfigWithCyclicReferences.class, considerNestedRepositories = true) + public static class ConfigWithMultipleRepositories { + + public interface Repo1 extends CrudRepository { + + } + + public interface Repo2 extends CrudRepository { + + } + } + +} 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 55c2d86ea4..43b5ad85f7 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -15,18 +15,23 @@ */ package org.springframework.data.repository.aot; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Consumer; +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; +import org.springframework.context.aot.ApplicationContextAotGenerator; import org.springframework.data.aot.CodeContributionAssert; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.core.RepositoryInformation; @@ -111,11 +116,28 @@ public RepositoryRegistrationAotContributionAssert codeContributionSatisfies( BeanRegistrationCode mockBeanRegistrationCode = mock(BeanRegistrationCode.class); - GenerationContext generationContext = new TestGenerationContext(Object.class); + TestGenerationContext generationContext = new TestGenerationContext(Object.class); - this.actual.applyTo(generationContext, mockBeanRegistrationCode); + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); try { + Class handlerClass = Class.forName("org.springframework.context.aot.CglibClassHandler"); + Constructor constructor = handlerClass.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + Object handler = BeanUtils.instantiateClass(constructor, generationContext); + + Method withCglibClassHandler = generator.getClass().getDeclaredMethod("withCglibClassHandler", handlerClass, + Supplier.class); + withCglibClassHandler.setAccessible(true); + withCglibClassHandler.invoke(generator, handler, new Supplier() { + + @Override + public Object get() { + + actual.applyTo(generationContext, mockBeanRegistrationCode); + return null; + } + }); assertWith.accept(new CodeContributionAssert(generationContext)); } catch (Throwable o_O) { fail(o_O.getMessage(), o_O); 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 bb71245359..c01d50310f 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -15,20 +15,13 @@ */ package org.springframework.data.repository.aot; -import static org.assertj.core.api.Assertions.*; -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.aot.hint.RuntimeHints; -import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.core.DecoratingProxy; @@ -65,6 +58,7 @@ * @author Christoph Strobl * @author John Blum */ +// TODO: This is verifying repository.config code. Move to repository.config package? public class RepositoryRegistrationAotProcessorIntegrationTests { @Test // GH-2593 @@ -83,9 +77,7 @@ void simpleRepositoryNoTxManagerNoKotlinNoReactiveNoComponent() { .contributesJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, SpringProxy.class, Advised.class, DecoratingProxy.class) // .contributesJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class) - .doesNotContributeJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + TransactionalProxy.class, Advised.class, DecoratingProxy.class); }); } @@ -108,9 +100,7 @@ void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, SpringProxy.class, Advised.class, DecoratingProxy.class) .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class) - .doesNotContributeJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + TransactionalProxy.class, Advised.class, DecoratingProxy.class); }); } @@ -118,8 +108,8 @@ void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { void simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class).forRepository( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class) + .forRepository(ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs( @@ -185,7 +175,7 @@ void contributesCustomImplementationCorrectly() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomImplementation.class) - .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); + .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class) // @@ -234,15 +224,15 @@ void contributesRepositoryBaseClassCorrectly() { 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 @@ -296,8 +286,7 @@ void registersQTypeIfPresent() { assertThatContribution(repositoryBeanContribution) // .codeContributionSatisfies(contribution -> { contribution.contributesReflectionFor(Person.class); - contribution.contributesReflectionFor( - QConfigWithQuerydslPredicateExecutor_Person.class); + contribution.contributesReflectionFor(QConfigWithQuerydslPredicateExecutor_Person.class); }); } @@ -318,53 +307,15 @@ void registersReflectionForInheritedDomainPublicationAnnotations() { RepositoryRegistrationAotContribution contribution = computeAotConfiguration( InheritedEventPublicationConfiguration.class) - .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); + .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); assertThatContribution(contribution).codeContributionSatisfies(it -> { it.contributesReflectionFor(AbstractAggregateRoot.class); }); } - RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { - return computeAotConfiguration(configuration, new AnnotationConfigApplicationContext()); - } - - RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration, - AnnotationConfigApplicationContext applicationContext) { - - applicationContext.register(configuration); - applicationContext.refreshForAotProcessing(new RuntimeHints()); - - return repositoryType -> { - - String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); - - assertThat(repositoryBeanNames) - .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) - .hasSize(1); - - String repositoryBeanName = repositoryBeanNames[0]; - - ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); - - RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext - .getBean(RepositoryRegistrationAotProcessor.class); - - repositoryAotProcessor.setBeanFactory(beanFactory); - - RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); - - BeanRegistrationAotContribution beanContribution = repositoryAotProcessor.processAheadOfTime(bean); - - assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); - - return (RepositoryRegistrationAotContribution) beanContribution; - }; - } - - @FunctionalInterface - interface RepositoryRegistrationAotContributionBuilder { - RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); + AotUtil.RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { + return AotUtil.contributionFor(configuration); } @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = SampleRepository.class) }, @@ -383,10 +334,8 @@ void cleanup() {} 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 {} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java index 3d3b3ffc64..8cb172f1c4 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -17,16 +17,18 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; - import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.test.tools.ClassFile; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; @@ -75,6 +77,16 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + @Override public String getBeanName() { return "dummyRepository"; @@ -105,6 +117,11 @@ public Set> getResolvedTypes() { return Set.of(); } + @Override + public Set> getUserDomainTypes() { + return Set.of(); + } + public List getRequiredContextFiles() { return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); }