From 8328517cc9e7e46f78c0691e3f1f4984da5b83b5 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 21 Sep 2023 16:12:19 +0200 Subject: [PATCH] Fix detection of @DomainEvents and @AfterDomainEventPublication on native. We now unconditionally process the aggregate root types declared on repositories for @Reflective annotations, which @DE and @ADEP got meta-annotated with. Fixes #2939. --- .../domain/AfterDomainEventPublication.java | 7 +- .../data/domain/DomainEvents.java | 6 +- .../RepositoryRegistrationAotProcessor.java | 38 ++++++++-- .../data/aot/CodeContributionAssert.java | 11 +++ ...toryRegistrationAotContributionAssert.java | 9 ++- ...istrationAotProcessorIntegrationTests.java | 70 ++++++++++++++++--- 6 files changed, 119 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java b/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java index 6b481d9be..df14eea6b 100644 --- a/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java +++ b/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java @@ -20,6 +20,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * Annotation to be used on a method of a Spring Data managed aggregate to get invoked after the events of an aggregate * have been published. @@ -29,8 +31,7 @@ import java.lang.annotation.Target; * @since 1.13 * @soundtrack Benny Greb - September (Moving Parts Live) */ +@Reflective @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) -public @interface AfterDomainEventPublication { - -} +public @interface AfterDomainEventPublication {} diff --git a/src/main/java/org/springframework/data/domain/DomainEvents.java b/src/main/java/org/springframework/data/domain/DomainEvents.java index 9817b493d..99b650410 100644 --- a/src/main/java/org/springframework/data/domain/DomainEvents.java +++ b/src/main/java/org/springframework/data/domain/DomainEvents.java @@ -20,6 +20,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * {@link DomainEvents} can be used on methods of aggregate roots managed by Spring Data repositories to publish the * events returned by that method as Spring application events. @@ -30,7 +32,7 @@ import java.lang.annotation.Target; * @since 1.13 * @soundtrack Benny Greb - Soulfood (Moving Parts Live) */ +@Reflective @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) -public @interface DomainEvents { -} +public @interface DomainEvents {} 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 7b19a34fc..e8bd8531e 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -16,15 +16,20 @@ package org.springframework.data.repository.config; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Predicate; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -35,8 +40,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.data.util.TypeContributor; +import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.util.TypeContributor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -62,7 +68,6 @@ import org.springframework.util.StringUtils; * @author John Blum * @since 3.0 */ -@SuppressWarnings("unused") public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware { private ConfigurableListableBeanFactory beanFactory; @@ -74,7 +79,6 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr @Nullable @Override public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean bean) { - return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null; } @@ -89,6 +93,28 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr .forEach(it -> contributeType(it, generationContext)); } + /** + * Processes the repository's domain and alternative domain types to consider {@link Reflective} annotations used on + * it. + * + * @param repositoryContext must not be {@literal null}. + * @param generationContext must not be {@literal null}. + */ + private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { + + RepositoryInformation information = repositoryContext.getRepositoryInformation(); + ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + RuntimeHints hints = generationContext.getRuntimeHints(); + + List> aggregateRootTypes = new ArrayList<>(); + aggregateRootTypes.add(information.getDomainType()); + aggregateRootTypes.addAll(information.getAlternativeDomainTypes()); + + Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) + .forEach(it -> registrar.registerRuntimeHints(hints, it)); + } + private boolean isRepositoryBean(RegisteredBean bean) { return getConfigMap().containsKey(bean.getBeanName()); } @@ -99,7 +125,9 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.fromProcessor(this) .forBean(repositoryBean); - return contribution.withModuleContribution(this::contribute); + BiConsumer moduleContribution = this::registerReflectiveForAggregateRoot; + + return contribution.withModuleContribution(moduleContribution.andThen(this::contribute)); } @Override diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java index afa17d70e..02cd972a9 100644 --- a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -17,6 +17,7 @@ package org.springframework.data.aot; import static org.assertj.core.api.Assertions.*; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.stream.Stream; @@ -50,6 +51,16 @@ public class CodeContributionAssert extends AbstractAssert... types) { for (Class type : types) { 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 5bf448320..78dccedf8 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.function.Consumer; 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.factory.aot.BeanRegistrationCode; @@ -106,7 +107,7 @@ public class RepositoryRegistrationAotContributionAssert } public RepositoryRegistrationAotContributionAssert codeContributionSatisfies( - Consumer assertWith) { + ThrowingConsumer assertWith) { BeanRegistrationCode mockBeanRegistrationCode = mock(BeanRegistrationCode.class); @@ -114,7 +115,11 @@ public class RepositoryRegistrationAotContributionAssert this.actual.applyTo(generationContext, mockBeanRegistrationCode); - assertWith.accept(new CodeContributionAssert(generationContext)); + try { + assertWith.accept(new CodeContributionAssert(generationContext)); + } catch (Throwable o_O) { + fail(o_O.getMessage(), o_O); + } return this; } 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 ce6a8bd4c..540133a2b 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -28,22 +28,21 @@ 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; -import org.springframework.data.aot.sample.ConfigWithCustomImplementation; -import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass; -import org.springframework.data.aot.sample.ConfigWithFragments; -import org.springframework.data.aot.sample.ConfigWithQueryMethods; +import org.springframework.data.aot.sample.*; import org.springframework.data.aot.sample.ConfigWithQueryMethods.ProjectionInterface; -import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor; import org.springframework.data.aot.sample.ConfigWithQuerydslPredicateExecutor.Person; -import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository; -import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresent; -import org.springframework.data.aot.sample.ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository; -import org.springframework.data.aot.sample.QConfigWithQuerydslPredicateExecutor_Person; -import org.springframework.data.aot.sample.ReactiveConfig; +import org.springframework.data.domain.AbstractAggregateRoot; +import org.springframework.data.domain.AfterDomainEventPublication; +import org.springframework.data.domain.DomainEvents; import org.springframework.data.domain.Page; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.aot.RepositoryRegistrationAotProcessorIntegrationTests.EventPublicationConfiguration.Sample; +import org.springframework.data.repository.aot.RepositoryRegistrationAotProcessorIntegrationTests.EventPublicationConfiguration.SampleRepository; +import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -292,6 +291,30 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { }); } + @Test // GH-2939 + void registersReflectionForDomainPublicationAnnotations() { + + RepositoryRegistrationAotContribution contribution = computeAotConfiguration(EventPublicationConfiguration.class) + .forRepository(SampleRepository.class); + + assertThatContribution(contribution).codeContributionSatisfies(it -> { + it.contributesReflectionFor(Sample.class.getDeclaredMethod("publication")); + it.contributesReflectionFor(Sample.class.getDeclaredMethod("cleanup")); + }); + } + + @Test // GH-2939 + void registersReflectionForInheritedDomainPublicationAnnotations() { + + RepositoryRegistrationAotContribution contribution = computeAotConfiguration( + InheritedEventPublicationConfiguration.class) + .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); + + assertThatContribution(contribution).codeContributionSatisfies(it -> { + it.contributesReflectionFor(AbstractAggregateRoot.class); + }); + } + RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { return computeAotConfiguration(configuration, new AnnotationConfigApplicationContext()); } @@ -333,4 +356,31 @@ public class RepositoryRegistrationAotProcessorIntegrationTests { interface RepositoryRegistrationAotContributionBuilder { RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); } + + @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = SampleRepository.class) }, + considerNestedRepositories = true) + public class EventPublicationConfiguration { + + static class Sample { + + @DomainEvents + void publication() {} + + @AfterDomainEventPublication + void cleanup() {} + } + + interface SampleRepository extends Repository {} + } + + @EnableRepositories( + includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, + value = InheritedEventPublicationConfiguration.SampleRepository.class) }, + considerNestedRepositories = true) + public class InheritedEventPublicationConfiguration { + + static class Sample extends AbstractAggregateRoot {} + + interface SampleRepository extends Repository {} + } }