diff --git a/pom.xml b/pom.xml index 8bb4250..b76eb19 100644 --- a/pom.xml +++ b/pom.xml @@ -211,6 +211,7 @@ ${maven.compiler.source} ${maven.compiler.target} + UTF-8 diff --git a/src/main/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrar.java b/src/main/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrar.java index 4dd257b..40fbf7a 100644 --- a/src/main/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrar.java +++ b/src/main/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrar.java @@ -16,9 +16,17 @@ package org.axonframework.springboot.aot; +import org.axonframework.common.Priority; +import org.axonframework.common.ReflectionUtils; +import org.axonframework.common.annotation.AnnotationUtils; import org.axonframework.messaging.Message; import org.axonframework.messaging.annotation.AnnotatedHandlerInspector; +import org.axonframework.messaging.annotation.ClasspathParameterResolverFactory; import org.axonframework.messaging.annotation.MessageHandlingMember; +import org.axonframework.messaging.annotation.MultiParameterResolverFactory; +import org.axonframework.messaging.annotation.ParameterResolver; +import org.axonframework.messaging.annotation.ParameterResolverFactory; +import org.axonframework.modelling.command.AggregateMember; import org.axonframework.queryhandling.annotation.QueryHandlingMember; import org.axonframework.spring.config.MessageHandlerLookup; import org.springframework.aot.generate.GenerationContext; @@ -31,10 +39,17 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * BeanFactoryInitializationAotProcessor that registers message handler methods declared on beans for reflection. This @@ -50,19 +65,59 @@ public class MessageHandlerRuntimeHintsRegistrar implements BeanFactoryInitializ @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { - List> messageHandlingClasses = + Set> messageHandlingClasses = MessageHandlerLookup.messageHandlerBeans(messageType(), beanFactory, true) .stream() .map(beanFactory::getType) - .distinct() - .collect(Collectors.toList()); - List> messageHandlingMembers = messageHandlingClasses + .collect(Collectors.toSet()); + + Set> detectedClasses = new HashSet<>(); + messageHandlingClasses.forEach(c -> registerAggregateMembers(c, detectedClasses)); + + List> messageHandlingMembers = detectedClasses .stream() - .flatMap(beanType -> AnnotatedHandlerInspector.inspectType(beanType).getAllHandlers().values() - .stream()) + .flatMap(beanType -> + { + AnnotatedHandlerInspector inspector = AnnotatedHandlerInspector.inspectType( + beanType, + MultiParameterResolverFactory.ordered( + ClasspathParameterResolverFactory.forClass(beanType), + new LenientParameterResolver() + )); + return Stream.concat(inspector.getAllHandlers().values().stream(), + inspector.getAllInterceptors().values().stream()); + }) .flatMap(Collection::stream) .collect(Collectors.toList()); - return new MessageHandlerContribution(messageHandlingClasses, messageHandlingMembers); + return new MessageHandlerContribution(detectedClasses, messageHandlingMembers); + } + + private void registerAggregateMembers(Class entityType, Set> reflectiveClasses) { + if (!reflectiveClasses.add(entityType)) { + return; + } + + ReflectionUtils.fieldsOf(entityType).forEach(field -> { + Optional> annotationAttributes = AnnotationUtils.findAnnotationAttributes(field, + AggregateMember.class); + if (annotationAttributes.isPresent()) { + Class declaredType = (Class) annotationAttributes.get().get("type"); + Class forwardingMode = (Class) annotationAttributes.get().get("eventForwardingMode"); + reflectiveClasses.add(forwardingMode); + + if (declaredType != Void.class) { + registerAggregateMembers(declaredType, reflectiveClasses); + } else if (Map.class.isAssignableFrom(field.getType())) { + Optional> type = ReflectionUtils.resolveMemberGenericType(field, 1); + type.ifPresent(t -> registerAggregateMembers(t, reflectiveClasses)); + } else if (Collection.class.isAssignableFrom(field.getType())) { + Optional> type = ReflectionUtils.resolveMemberGenericType(field, 0); + type.ifPresent(t -> registerAggregateMembers(t, reflectiveClasses)); + } else { + registerAggregateMembers(field.getType(), reflectiveClasses); + } + } + }); } /** @@ -80,14 +135,13 @@ private static class MessageHandlerContribution implements BeanFactoryInitializa private final BindingReflectionHintsRegistrar registrar = new BindingReflectionHintsRegistrar(); - private final List> messageHandlingClasses; + private final Set> messageHandlingClasses; private final List> messageHandlingMembers; public MessageHandlerContribution( - List> messageHandlingClasses, - List> messageHandlingMembers - ) { + Set> messageHandlingClasses, + List> messageHandlingMembers) { this.messageHandlingClasses = messageHandlingClasses; this.messageHandlingMembers = messageHandlingMembers; } @@ -108,4 +162,26 @@ public void applyTo(GenerationContext generationContext, }); } } + + @Priority(Priority.LAST) + private static class LenientParameterResolver implements ParameterResolverFactory, ParameterResolver { + + @Override + public ParameterResolver createInstance(Executable executable, + Parameter[] parameters, + int parameterIndex) { + return this; + } + + @Override + public Object resolveParameterValue(Message message) { + throw new UnsupportedOperationException( + "This parameter resolver is not mean for production use. Only for detecting handler methods."); + } + + @Override + public boolean matches(Message message) { + return true; + } + } } diff --git a/src/test/java/com/axoniq/someproject/App.java b/src/test/java/com/axoniq/someproject/App.java index c540357..34b4e97 100644 --- a/src/test/java/com/axoniq/someproject/App.java +++ b/src/test/java/com/axoniq/someproject/App.java @@ -17,8 +17,14 @@ package com.axoniq.someproject; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class App { + @Bean + public SomeBean springBean() { + return new SomeBean(); + } + } diff --git a/src/test/java/com/axoniq/someproject/SomeBean.java b/src/test/java/com/axoniq/someproject/SomeBean.java new file mode 100644 index 0000000..1e4bcb4 --- /dev/null +++ b/src/test/java/com/axoniq/someproject/SomeBean.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010-2024. Axon Framework + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.axoniq.someproject; + +public class SomeBean { + +} diff --git a/src/test/java/com/axoniq/someproject/something/SingleAggregateChild.java b/src/test/java/com/axoniq/someproject/something/SingleAggregateChild.java index 44dc274..9507def 100644 --- a/src/test/java/com/axoniq/someproject/something/SingleAggregateChild.java +++ b/src/test/java/com/axoniq/someproject/something/SingleAggregateChild.java @@ -16,9 +16,11 @@ package com.axoniq.someproject.something; +import com.axoniq.someproject.SomeBean; import com.axoniq.someproject.api.SingleChildCommand; -import com.axoniq.someproject.api.SomeChildCommand; import org.axonframework.commandhandling.CommandHandler; +import org.axonframework.messaging.InterceptorChain; +import org.axonframework.modelling.command.CommandHandlerInterceptor; import org.axonframework.modelling.command.EntityId; public record SingleAggregateChild( @@ -26,8 +28,13 @@ public record SingleAggregateChild( String property ) { + @CommandHandlerInterceptor + public Object intercept(InterceptorChain chain) throws Exception { + return chain.proceed(); + } + @CommandHandler - public void handle(SingleChildCommand command) { + public void handle(SingleChildCommand command, SomeBean someBean) { //left empty to not overcomplicate things } } diff --git a/src/test/java/com/axoniq/someproject/something/SomeAggregate.java b/src/test/java/com/axoniq/someproject/something/SomeAggregate.java index 930ec50..eb1de80 100644 --- a/src/test/java/com/axoniq/someproject/something/SomeAggregate.java +++ b/src/test/java/com/axoniq/someproject/something/SomeAggregate.java @@ -26,9 +26,14 @@ import com.axoniq.someproject.api.StatusChangedEvent; import org.axonframework.commandhandling.CommandHandler; import org.axonframework.eventsourcing.EventSourcingHandler; +import org.axonframework.messaging.InterceptorChain; +import org.axonframework.messaging.Message; +import org.axonframework.messaging.interceptors.ExceptionHandler; import org.axonframework.modelling.command.AggregateIdentifier; import org.axonframework.modelling.command.AggregateMember; import org.axonframework.modelling.command.AggregateRoot; +import org.axonframework.modelling.command.CommandHandlerInterceptor; +import org.axonframework.modelling.command.ForwardMatchingInstances; import java.util.ArrayList; import java.util.HashMap; @@ -41,24 +46,35 @@ @AggregateRoot(type = "some_aggregate") public class SomeAggregate { + @AggregateMember(eventForwardingMode = ForwardMatchingInstances.class) + private final List childList = new ArrayList<>(); + @AggregateMember + private final Map childMap = new HashMap<>(); @AggregateIdentifier private String id; private String status; - @AggregateMember private SingleAggregateChild child; - @AggregateMember - private final List childList = new ArrayList<>(); - - @AggregateMember - private final Map childMap = new HashMap<>(); - @CommandHandler public SomeAggregate(SomeCommand command) { apply(new SomeEvent(command.id())); } + public SomeAggregate() { + // Required by Axon to construct an empty instance to initiate Event Sourcing. + } + + @ExceptionHandler + public void exceptionHandler(Exception error) throws Exception { + throw error; + } + + @CommandHandlerInterceptor + public Object intercept(Message message, InterceptorChain chain) throws Exception { + return chain.proceed(); + } + @CommandHandler public void handle(ChangeStatusCommand command) { if (Objects.equals(status, command.newStatus())) { @@ -96,8 +112,4 @@ protected void onAddedToList(ChildAddedToListEvent event) { protected void onAddedToMap(ChildAddedToMapEvent event) { this.childMap.put(event.key(), new SomeAggregateChild(event.id(), event.property())); } - - public SomeAggregate() { - // Required by Axon to construct an empty instance to initiate Event Sourcing. - } } diff --git a/src/test/java/com/axoniq/someproject/something/SomeAggregateChild.java b/src/test/java/com/axoniq/someproject/something/SomeAggregateChild.java index 2e02d07..80630b8 100644 --- a/src/test/java/com/axoniq/someproject/something/SomeAggregateChild.java +++ b/src/test/java/com/axoniq/someproject/something/SomeAggregateChild.java @@ -16,6 +16,7 @@ package com.axoniq.someproject.something; +import com.axoniq.someproject.SomeBean; import com.axoniq.someproject.api.SomeChildCommand; import org.axonframework.commandhandling.CommandHandler; import org.axonframework.modelling.command.EntityId; @@ -26,7 +27,7 @@ public record SomeAggregateChild( ) { @CommandHandler - public void handle(SomeChildCommand command) { + public void handle(SomeChildCommand command, SomeBean someBean) { //left empty to not overcomplicate things } } diff --git a/src/test/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrarTest.java b/src/test/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrarTest.java index 8782a70..8a2ecbb 100644 --- a/src/test/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrarTest.java +++ b/src/test/java/org/axonframework/springboot/aot/MessageHandlerRuntimeHintsRegistrarTest.java @@ -30,6 +30,8 @@ import com.axoniq.someproject.something.SomeAggregateChild; import com.axoniq.someproject.something.SomeProjectionWithGroupAnnotation; import com.axoniq.someproject.something.SomeProjectionWithoutGroupAnnotation; +import org.axonframework.modelling.command.ForwardMatchingInstances; +import org.axonframework.modelling.command.ForwardToAll; import org.junit.jupiter.api.*; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; @@ -51,8 +53,6 @@ class MessageHandlerRuntimeHintsRegistrarTest { @BeforeEach void processAheadOfTime() { addClassToBeanFactory(SomeAggregate.class); - addClassToBeanFactory(SingleAggregateChild.class); - addClassToBeanFactory(SomeAggregateChild.class); addClassToBeanFactory(SomeProjectionWithGroupAnnotation.class); addClassToBeanFactory(SomeProjectionWithoutGroupAnnotation.class); new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext, this.generationContext); @@ -87,6 +87,22 @@ void handlerMethodsHaveReflectiveHints() { testReflectionMethod(SomeAggregateChild.class, "handle"); } + @Test + void handlerInterceptorsHaveReflectiveHints() { + testReflectionMethod(SomeAggregate.class, "intercept"); + testReflectionMethod(SomeAggregate.class, "exceptionHandler"); + testReflectionMethod(SingleAggregateChild.class, "intercept"); + } + + @Test + void childEntitiesHaveReflectiveHints() { + testReflectionMethod(SomeAggregateChild.class, "handle"); + testReflectionMethod(SingleAggregateChild.class, "intercept"); + testReflectionMethod(SingleAggregateChild.class, "handle"); + testForConstructor(ForwardMatchingInstances.class); + testForConstructor(ForwardToAll.class); + } + private void addClassToBeanFactory(Class clazz) { BeanDefinition definition = new RootBeanDefinition(clazz); beanFactory.registerBeanDefinition(clazz.getName(), definition);