Skip to content

Commit

Permalink
Merge pull request #197 from AxonFramework/fix-native-reachability-hints
Browse files Browse the repository at this point in the history
Added native-compilation hints for reflection
  • Loading branch information
smcvb authored Nov 21, 2024
2 parents 8e356c2 + 571298a commit 97906d4
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 27 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- test -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -50,19 +65,59 @@ public class MessageHandlerRuntimeHintsRegistrar implements BeanFactoryInitializ

@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
List<Class<?>> messageHandlingClasses =
Set<Class<?>> messageHandlingClasses =
MessageHandlerLookup.messageHandlerBeans(messageType(), beanFactory, true)
.stream()
.map(beanFactory::getType)
.distinct()
.collect(Collectors.toList());
List<MessageHandlingMember<?>> messageHandlingMembers = messageHandlingClasses
.collect(Collectors.toSet());

Set<Class<?>> detectedClasses = new HashSet<>();
messageHandlingClasses.forEach(c -> registerAggregateMembers(c, detectedClasses));

List<MessageHandlingMember<?>> 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<Class<?>> reflectiveClasses) {
if (!reflectiveClasses.add(entityType)) {
return;
}

ReflectionUtils.fieldsOf(entityType).forEach(field -> {
Optional<Map<String, Object>> 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<Class<?>> type = ReflectionUtils.resolveMemberGenericType(field, 1);
type.ifPresent(t -> registerAggregateMembers(t, reflectiveClasses));
} else if (Collection.class.isAssignableFrom(field.getType())) {
Optional<Class<?>> type = ReflectionUtils.resolveMemberGenericType(field, 0);
type.ifPresent(t -> registerAggregateMembers(t, reflectiveClasses));
} else {
registerAggregateMembers(field.getType(), reflectiveClasses);
}
}
});
}

/**
Expand All @@ -80,14 +135,13 @@ private static class MessageHandlerContribution implements BeanFactoryInitializa

private final BindingReflectionHintsRegistrar registrar = new BindingReflectionHintsRegistrar();

private final List<Class<?>> messageHandlingClasses;
private final Set<Class<?>> messageHandlingClasses;

private final List<MessageHandlingMember<?>> messageHandlingMembers;

public MessageHandlerContribution(
List<Class<?>> messageHandlingClasses,
List<MessageHandlingMember<?>> messageHandlingMembers
) {
Set<Class<?>> messageHandlingClasses,
List<MessageHandlingMember<?>> messageHandlingMembers) {
this.messageHandlingClasses = messageHandlingClasses;
this.messageHandlingMembers = messageHandlingMembers;
}
Expand All @@ -108,4 +162,26 @@ public void applyTo(GenerationContext generationContext,
});
}
}

@Priority(Priority.LAST)
private static class LenientParameterResolver implements ParameterResolverFactory, ParameterResolver<Object> {

@Override
public ParameterResolver<Object> 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;
}
}
}
6 changes: 6 additions & 0 deletions src/test/java/com/axoniq/someproject/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
21 changes: 21 additions & 0 deletions src/test/java/com/axoniq/someproject/SomeBean.java
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@

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(
@EntityId String id,
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
}
}
34 changes: 23 additions & 11 deletions src/test/java/com/axoniq/someproject/something/SomeAggregate.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,24 +46,35 @@
@AggregateRoot(type = "some_aggregate")
public class SomeAggregate {

@AggregateMember(eventForwardingMode = ForwardMatchingInstances.class)
private final List<SomeAggregateChild> childList = new ArrayList<>();
@AggregateMember
private final Map<String, SomeAggregateChild> childMap = new HashMap<>();
@AggregateIdentifier
private String id;
private String status;

@AggregateMember
private SingleAggregateChild child;

@AggregateMember
private final List<SomeAggregateChild> childList = new ArrayList<>();

@AggregateMember
private final Map<String, SomeAggregateChild> 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())) {
Expand Down Expand Up @@ -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.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 97906d4

Please sign in to comment.