From 48e5e0b883228779a815657f971a3e6d5f0c2a0a Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Fri, 30 Dec 2022 13:36:44 +0100 Subject: [PATCH] Introduce component decorators. You can now decorate `Component` instances by calling `Configurer.registerComponentDecorator`. Upon initialization of the component, the original component will be constructed and the decorators will be called in order. Decorators can return the existing component, a modified version of it or a completely different implementation, as long as it's the same type. --- .../org/axonframework/config/Component.java | 11 ++++ .../config/ComponentDecorator.java | 29 +++++++++ .../org/axonframework/config/Configurer.java | 17 ++++++ .../config/DefaultConfigurer.java | 29 +++++++-- ...aultConfigurerLifecycleOperationsTest.java | 59 +++++++++++++++++-- .../config/DefaultConfigurerTest.java | 32 +++++++--- .../axonframework/tracing/SpanFactory.java | 4 +- 7 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 config/src/main/java/org/axonframework/config/ComponentDecorator.java diff --git a/config/src/main/java/org/axonframework/config/Component.java b/config/src/main/java/org/axonframework/config/Component.java index efd2f9d732..d1887a3b31 100644 --- a/config/src/main/java/org/axonframework/config/Component.java +++ b/config/src/main/java/org/axonframework/config/Component.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -100,6 +101,16 @@ public void update(@Nonnull Function builderFunction this.builderFunction = builderFunction; } + public void decorate(@Nonnull BiFunction decoratingFunction) { + Assert.state(instance == null, () -> "Cannot change " + name + ": it is already in use"); + Function previous = builderFunction; + this.update((configuration) -> { + B wrappedComponent = previous.apply(configuration); + LifecycleHandlerInspector.registerLifecycleHandlers(configuration, wrappedComponent); + return decoratingFunction.apply(configuration, wrappedComponent); + }); + } + /** * Checks if the component is already initialized. * diff --git a/config/src/main/java/org/axonframework/config/ComponentDecorator.java b/config/src/main/java/org/axonframework/config/ComponentDecorator.java new file mode 100644 index 0000000000..8b62bee436 --- /dev/null +++ b/config/src/main/java/org/axonframework/config/ComponentDecorator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2022. 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 org.axonframework.config; + +/** + * Functional interface that is able to decorate a component. + * + * @author Mitchell Herrijgers + * @since 4.7.0 + */ +@FunctionalInterface +public interface ComponentDecorator { + + T decorate(Configuration configuration, T component); +} diff --git a/config/src/main/java/org/axonframework/config/Configurer.java b/config/src/main/java/org/axonframework/config/Configurer.java index 7d68c81de8..11738585a2 100644 --- a/config/src/main/java/org/axonframework/config/Configurer.java +++ b/config/src/main/java/org/axonframework/config/Configurer.java @@ -212,6 +212,23 @@ Configurer configureCorrelationDataProviders( Configurer registerComponent(@Nonnull Class componentType, @Nonnull Function componentBuilder); + + /** + * Registers a decorator for a component type. For any component that is created by the configuration that matches + * the {@code componentType}, the {@code decorator} will be called. It's up to the decorator to decide to return the + * original component, a wrapped version of it or something else entirely. + * + * @param componentType The declared type of the component, typically an interface + * @param decorator The decorator function for this component + * @param The type of component + * @return the current instance of the Configurer, for chaining purposes + */ + default Configurer registerComponentDecorator(@Nonnull Class componentType, + @Nonnull ComponentDecorator decorator) { + // Default implementation for backwards compatibility + return this; + } + /** * Registers a command handler bean with this {@link Configurer}. The bean may be of any type. The actual command * handler methods will be detected based on the annotations present on the bean's methods. Message handling diff --git a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java index 8e01b17b00..86283a993a 100644 --- a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java +++ b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java @@ -159,6 +159,7 @@ public class DefaultConfigurer implements Configurer { ); private final List> initHandlers = new ArrayList<>(); + private final List decoratorHandlers = new ArrayList<>(); private final TreeMap> startHandlers = new TreeMap<>(); private final TreeMap> shutdownHandlers = new TreeMap<>(Comparator.reverseOrder()); private final List modules = new ArrayList<>(); @@ -643,6 +644,20 @@ public Configurer registerComponent(@Nonnull Class componentType, return this; } + @SuppressWarnings("unchecked") + @Override + public Configurer registerComponentDecorator(@Nonnull Class componentType, + @Nonnull ComponentDecorator decorator) { + logger.debug("Registering decorator for component [{}]", componentType.getSimpleName()); + decoratorHandlers.add(() -> { + Component component = (Component) components.getOrDefault(componentType, null); + if (component != null) { + component.decorate(decorator::decorate); + } + }); + return this; + } + @Override public Configurer registerCommandHandler(@Nonnull Function commandHandlerBuilder) { messageHandlerRegistrars.add(new Component<>( @@ -683,13 +698,13 @@ public Configurer registerQueryHandler(@Nonnull Function public Configurer registerMessageHandler(@Nonnull Function messageHandlerBuilder) { Component messageHandler = new Component<>(() -> config, "", messageHandlerBuilder); Class handlerClass = messageHandler.get().getClass(); - if (isCommandHandler(handlerClass)){ + if (isCommandHandler(handlerClass)) { registerCommandHandler(c -> messageHandler.get()); } - if (isEventHandler(handlerClass)){ + if (isEventHandler(handlerClass)) { eventProcessing().registerEventHandler(c -> messageHandler.get()); } - if (isQueryHandler(handlerClass)){ + if (isQueryHandler(handlerClass)) { registerQueryHandler(c -> messageHandler.get()); } return this; @@ -749,6 +764,7 @@ public Configuration buildConfiguration() { if (!initialized) { verifyIdentifierFactory(); prepareModules(); + invokeDecoratorHandlers(); prepareMessageHandlerRegistrars(); invokeInitHandlers(); } @@ -792,6 +808,10 @@ protected void invokeInitHandlers() { initHandlers.forEach(h -> h.accept(config)); } + protected void invokeDecoratorHandlers() { + decoratorHandlers.forEach(h -> h.run()); + } + /** * Invokes all registered start handlers. */ @@ -865,7 +885,8 @@ private void invokeLifecycleHandlers(TreeMap> li lifecycleState.description, currentLifecyclePhase )); } catch (TimeoutException e) { - final long lifecyclePhaseTimeoutInSeconds = TimeUnit.SECONDS.convert(lifecyclePhaseTimeout, lifecyclePhaseTimeunit); + final long lifecyclePhaseTimeoutInSeconds = TimeUnit.SECONDS.convert(lifecyclePhaseTimeout, + lifecyclePhaseTimeunit); logger.warn(String.format( "Timed out during %s phase [%d] after %d second(s). Proceeding to following phase", lifecycleState.description, currentLifecyclePhase, lifecyclePhaseTimeoutInSeconds diff --git a/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java b/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java index 955d6ff840..492d762579 100644 --- a/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java +++ b/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java @@ -17,6 +17,7 @@ package org.axonframework.config; import org.axonframework.common.AxonConfigurationException; +import org.axonframework.lifecycle.Lifecycle; import org.axonframework.lifecycle.LifecycleHandlerInvocationException; import org.junit.jupiter.api.*; import org.mockito.*; @@ -25,15 +26,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.Nonnull; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * Test class validating the workings of the lifecycle operations registered and invoked on the {@link Configurer}, - * {@link Configuration} and the {@link DefaultConfigurer} implementation. As such, operations like the {@link - * Configuration#onStart(int, LifecycleHandler)}, {@link Configuration#onShutdown(int, LifecycleHandler)}, {@link - * Configurer#start()}, {@link Configuration#start()} and {@link Configuration#shutdown()} will be tested. + * {@link Configuration} and the {@link DefaultConfigurer} implementation. As such, operations like the + * {@link Configuration#onStart(int, LifecycleHandler)}, {@link Configuration#onShutdown(int, LifecycleHandler)}, + * {@link Configurer#start()}, {@link Configuration#start()} and {@link Configuration#shutdown()} will be tested. * * @author Steven van Beelen */ @@ -468,6 +470,40 @@ void configureLifecyclePhaseTimeoutWithNullTimeUnitThrowsAxonConfigurationExcept ); } + @Test + void decoratedComponentsHaveLifeCycleHandlersRegistered() { + + LifeCycleComponent original = spy(new LifeCycleComponent()); + LifeCycleComponent decorator1 = spy(new LifeCycleComponent()); + LifeCycleComponent decorator2 = spy(new LifeCycleComponent()); + LifeCycleComponent decorator3 = spy(new LifeCycleComponent()); + + Configuration testSubject = DefaultConfigurer + .defaultConfiguration() + .registerComponent(LifeCycleComponent.class, config -> original) + .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator1) + .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator2) + .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator3) + .buildConfiguration(); + + testSubject.getComponent(LifeCycleComponent.class); + + testSubject.start(); + + InOrder lifecycleOrder = + inOrder(original, decorator1, decorator2, decorator3); + lifecycleOrder.verify(original).registerLifecycleHandlers(any()); + lifecycleOrder.verify(decorator1).registerLifecycleHandlers(any()); + lifecycleOrder.verify(decorator2).registerLifecycleHandlers(any()); + lifecycleOrder.verify(decorator3).registerLifecycleHandlers(any()); + + assertTrue(original.isInvoked()); + assertTrue(decorator1.isInvoked()); + assertTrue(decorator2.isInvoked()); + assertTrue(decorator3.isInvoked()); + } + + private static class LifecycleManagedInstance { private final ReentrantLock lock; @@ -534,9 +570,24 @@ public void failingStart() { } } + static class LifeCycleComponent implements Lifecycle { + private AtomicBoolean invoked = new AtomicBoolean(false); + + @Override + public void registerLifecycleHandlers(@Nonnull LifecycleRegistry lifecycle) { + lifecycle.onStart(0, () -> { + invoked.set(true); + }); + } + + public boolean isInvoked() { + return invoked.get(); + } + } + @FunctionalInterface private interface LifecycleRegistration { void registerLifecycleHandler(Configuration configuration, int phase, Runnable lifecycleHandler); } -} \ No newline at end of file +} diff --git a/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java b/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java index 925d684f38..9d7baf847f 100644 --- a/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java +++ b/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java @@ -620,13 +620,31 @@ void customConfiguredSpanFactory() { SpanFactory custom = mock(SpanFactory.class); SpanFactory result = DefaultConfigurer.defaultConfiguration() - .configureSpanFactory((config) -> custom) - .buildConfiguration() - .spanFactory(); + .configureSpanFactory((config) -> custom) + .buildConfiguration() + .spanFactory(); assertSame(custom, result); } + @Test + void canDecorateComponentsSuchAsSpanFactory() { + SpanFactory custom = mock(SpanFactory.class); + SpanFactory decorated = mock(SpanFactory.class); + + SpanFactory result = DefaultConfigurer.defaultConfiguration() + .configureSpanFactory((config) -> custom) + .registerComponentDecorator(SpanFactory.class, + (configuration, component) -> { + assertSame(custom, component); + return decorated; + }) + .buildConfiguration() + .spanFactory(); + + assertSame(decorated, result); + } + @Test void defaultConfiguredScopeAwareProvider() { ScopeAwareProvider result = DefaultConfigurer.defaultConfiguration() @@ -637,8 +655,8 @@ void defaultConfiguredScopeAwareProvider() { } @Test - void whenStubAggregateRegisteredWithRegisterMessageHandler_thenRightThingsCalled(){ - Configurer configurer = spy(DefaultConfigurer.defaultConfiguration()); + void whenStubAggregateRegisteredWithRegisterMessageHandler_thenRightThingsCalled() { + Configurer configurer = spy(DefaultConfigurer.defaultConfiguration()); configurer.registerMessageHandler(c -> new StubAggregate()); verify(configurer, times(1)).registerCommandHandler(any()); @@ -647,8 +665,8 @@ void whenStubAggregateRegisteredWithRegisterMessageHandler_thenRightThingsCalled } @Test - void whenQueryHandlerRegisteredWithRegisterMessageHandler_thenRightThingsCalled(){ - Configurer configurer = spy(DefaultConfigurer.defaultConfiguration()); + void whenQueryHandlerRegisteredWithRegisterMessageHandler_thenRightThingsCalled() { + Configurer configurer = spy(DefaultConfigurer.defaultConfiguration()); configurer.registerMessageHandler(c -> new StubQueryHandler()); verify(configurer, never()).registerCommandHandler(any()); diff --git a/messaging/src/main/java/org/axonframework/tracing/SpanFactory.java b/messaging/src/main/java/org/axonframework/tracing/SpanFactory.java index d2d05e00da..fc34e97083 100644 --- a/messaging/src/main/java/org/axonframework/tracing/SpanFactory.java +++ b/messaging/src/main/java/org/axonframework/tracing/SpanFactory.java @@ -132,11 +132,11 @@ Span createHandlerSpan(Supplier operationNameSupplier, Message parent * {@link SpanUtils#determineMessageName(Message)}. * * @param operationNameSupplier Supplier of the operation's name. - * @param parentMessage The message that is being handled. + * @param message The message that is being sent. * @param linkedSiblings Optional parameter, providing this will link the provided messages to the current. * @return The created {@link Span}. */ - Span createDispatchSpan(Supplier operationNameSupplier, Message parentMessage, Message... linkedSiblings); + Span createDispatchSpan(Supplier operationNameSupplier, Message message, Message... linkedSiblings); /** * Creates a new {@link Span} linked to the currently active span. This is useful for tracing different parts of