From 48e5e0b883228779a815657f971a3e6d5f0c2a0a Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Fri, 30 Dec 2022 13:36:44 +0100 Subject: [PATCH 1/4] 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 From c838eba74977b6e73bee0269c6dc105cf8c0175f Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Mon, 9 Jan 2023 13:31:06 +0100 Subject: [PATCH 2/4] Improve javadoc for Component decorators --- .../java/org/axonframework/config/Component.java | 15 +++++++++++++-- .../axonframework/config/ComponentDecorator.java | 3 ++- .../java/org/axonframework/config/Configurer.java | 6 +++--- .../axonframework/config/DefaultConfigurer.java | 9 ++++++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/config/src/main/java/org/axonframework/config/Component.java b/config/src/main/java/org/axonframework/config/Component.java index d1887a3b31..dff22d6af9 100644 --- a/config/src/main/java/org/axonframework/config/Component.java +++ b/config/src/main/java/org/axonframework/config/Component.java @@ -75,8 +75,9 @@ public Component(@Nonnull Supplier config, /** * Retrieves the object contained in this component, triggering the builder function if the component hasn't been - * built yet. Upon initiation of the instance the {@link LifecycleHandlerInspector#registerLifecycleHandlers(Configuration, - * Object)} methods will be called to resolve and register lifecycle methods. + * built yet. Upon initiation of the instance the + * {@link LifecycleHandlerInspector#registerLifecycleHandlers(Configuration, Object)} methods will be called to + * resolve and register lifecycle methods. * * @return the initialized component contained in this instance */ @@ -101,6 +102,16 @@ public void update(@Nonnull Function builderFunction this.builderFunction = builderFunction; } + /** + * Updates the builder function for this component by creating a new builderFunction that uses the + * {@code decoratingFunction} to change the original component. This can wrap the original component. + *

+ * Lifecycle handlers are scanned for both the original component and the one created by the @code + * decoratingFunction}. + * + * @param decoratingFunction Function that takes the original component and can wrap or change it depending on + * requirements. + */ public void decorate(@Nonnull BiFunction decoratingFunction) { Assert.state(instance == null, () -> "Cannot change " + name + ": it is already in use"); Function previous = builderFunction; diff --git a/config/src/main/java/org/axonframework/config/ComponentDecorator.java b/config/src/main/java/org/axonframework/config/ComponentDecorator.java index 8b62bee436..545061682f 100644 --- a/config/src/main/java/org/axonframework/config/ComponentDecorator.java +++ b/config/src/main/java/org/axonframework/config/ComponentDecorator.java @@ -17,10 +17,11 @@ package org.axonframework.config; /** - * Functional interface that is able to decorate a component. + * Functional interface that is able to decorate a {@link Component}, wrapping the original component. * * @author Mitchell Herrijgers * @since 4.7.0 + * @param The Component's type */ @FunctionalInterface public interface ComponentDecorator { diff --git a/config/src/main/java/org/axonframework/config/Configurer.java b/config/src/main/java/org/axonframework/config/Configurer.java index 11738585a2..886ef1045c 100644 --- a/config/src/main/java/org/axonframework/config/Configurer.java +++ b/config/src/main/java/org/axonframework/config/Configurer.java @@ -214,9 +214,9 @@ Configurer registerComponent(@Nonnull Class componentType, /** - * 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. + * Registers a {@link ComponentDecorator 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 or a wrapped version of it. * * @param componentType The declared type of the component, typically an interface * @param decorator The decorator function for this component diff --git a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java index 86283a993a..9ce51f2c1c 100644 --- a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java +++ b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java @@ -808,8 +808,15 @@ protected void invokeInitHandlers() { initHandlers.forEach(h -> h.accept(config)); } + /** + * Calls all registered decorators of this configuration and updates the components where appropriate. Decorating + * components is postponed until building to make sure all {@link Component} definitions are present and can be + * decorated. + *

+ * Registration of decorates are ignore after initialization. + */ protected void invokeDecoratorHandlers() { - decoratorHandlers.forEach(h -> h.run()); + decoratorHandlers.forEach(Runnable::run); } /** From 2e89ec3a50a186c36160a82d14c88971550eb5f6 Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Fri, 13 Jan 2023 16:24:34 +0100 Subject: [PATCH 3/4] Improve the decorator pattern by calling the Configuration from the component when initializing --- .../org/axonframework/config/Component.java | 30 +++----- .../config/ComponentDecorator.java | 14 ++-- .../config/ComponentTypeSafeDecorator.java | 69 +++++++++++++++++++ .../axonframework/config/Configuration.java | 6 +- .../org/axonframework/config/Configurer.java | 34 +++++++-- .../config/DefaultConfigurer.java | 38 ++++------ ...aultConfigurerLifecycleOperationsTest.java | 8 +-- .../config/DefaultConfigurerTest.java | 2 +- 8 files changed, 141 insertions(+), 60 deletions(-) create mode 100644 config/src/main/java/org/axonframework/config/ComponentTypeSafeDecorator.java diff --git a/config/src/main/java/org/axonframework/config/Component.java b/config/src/main/java/org/axonframework/config/Component.java index dff22d6af9..0c604733dd 100644 --- a/config/src/main/java/org/axonframework/config/Component.java +++ b/config/src/main/java/org/axonframework/config/Component.java @@ -21,7 +21,8 @@ import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; -import java.util.function.BiFunction; +import java.util.Collections; +import java.util.List; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -78,6 +79,8 @@ public Component(@Nonnull Supplier config, * built yet. Upon initiation of the instance the * {@link LifecycleHandlerInspector#registerLifecycleHandlers(Configuration, Object)} methods will be called to * resolve and register lifecycle methods. + *

+ * After building the initial definition, the decorators contained in the {@link Configuration} will be applied. * * @return the initialized component contained in this instance */ @@ -85,6 +88,11 @@ public B get() { if (instance == null) { Configuration configuration = configSupplier.get(); instance = builderFunction.apply(configuration); + List decorators = configuration != null ? configuration.getDecorators() : Collections.emptyList(); + for (ComponentDecorator applicableDecorator : decorators) { + //noinspection unchecked + instance = (B) applicableDecorator.decorate(configuration, instance); + } logger.debug("Instantiated component [{}]: {}", name, instance); LifecycleHandlerInspector.registerLifecycleHandlers(configuration, instance); } @@ -102,26 +110,6 @@ public void update(@Nonnull Function builderFunction this.builderFunction = builderFunction; } - /** - * Updates the builder function for this component by creating a new builderFunction that uses the - * {@code decoratingFunction} to change the original component. This can wrap the original component. - *

- * Lifecycle handlers are scanned for both the original component and the one created by the @code - * decoratingFunction}. - * - * @param decoratingFunction Function that takes the original component and can wrap or change it depending on - * requirements. - */ - 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 index 545061682f..514f70f33b 100644 --- a/config/src/main/java/org/axonframework/config/ComponentDecorator.java +++ b/config/src/main/java/org/axonframework/config/ComponentDecorator.java @@ -21,10 +21,16 @@ * * @author Mitchell Herrijgers * @since 4.7.0 - * @param The Component's type */ -@FunctionalInterface -public interface ComponentDecorator { +public interface ComponentDecorator { - T decorate(Configuration configuration, T component); + /** + * Can decorate the given component, returning a decorated component or the original if there is no intention + * of decorating it. + * + * @param configuration The Axon Configuration + * @param component The component to be decorated + * @return The decorated component + */ + Object decorate(Configuration configuration, Object component); } diff --git a/config/src/main/java/org/axonframework/config/ComponentTypeSafeDecorator.java b/config/src/main/java/org/axonframework/config/ComponentTypeSafeDecorator.java new file mode 100644 index 0000000000..f6d0db843a --- /dev/null +++ b/config/src/main/java/org/axonframework/config/ComponentTypeSafeDecorator.java @@ -0,0 +1,69 @@ +/* + * 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; + +import java.util.function.BiFunction; + +/** + * Type-safe implementation of the {@link ComponentDecorator}. Will check whether the component matches the target type + * before executing the decorating function. Additionally, if wanted, it will call the lifecycle handlers on the + * original component. + * + * @author Mitchell Herrijgers + * @since 4.7.0 + */ +public class ComponentTypeSafeDecorator implements ComponentDecorator { + + private final Class componentClass; + private final boolean registerOriginalLifeCycleHandlers; + private final BiFunction decorator; + + /** + * Creates a new {@link ComponentTypeSafeDecorator} that can decorate a component of the {@param componentClass}. + * + * @param componentClass The component class which this decorator should decorate. + * @param registerOriginalLifeCycleHandlers Whether the lifecycle handlers of the wrapped component should be registered. + * @param decoratorFunction The function that returns the decorated component based on the given + * configuration and the original. + */ + public ComponentTypeSafeDecorator(Class componentClass, + boolean registerOriginalLifeCycleHandlers, + BiFunction decoratorFunction) { + this.componentClass = componentClass; + this.registerOriginalLifeCycleHandlers = registerOriginalLifeCycleHandlers; + this.decorator = decoratorFunction; + } + + /** + * Decorates the given component using the {@link #decorator} if it matches the {@link #componentClass}, otherwise + * returns the original. + * + * @param configuration The Axon Configuration + * @param component The component to be decorated + * @return The decorated component + */ + public Object decorate(Configuration configuration, Object component) { + if (componentClass.isAssignableFrom(component.getClass())) { + T result = decorator.apply(configuration, (T) component); + if (registerOriginalLifeCycleHandlers) { + LifecycleHandlerInspector.registerLifecycleHandlers(configuration, component); + } + return result; + } + return component; + } +} diff --git a/config/src/main/java/org/axonframework/config/Configuration.java b/config/src/main/java/org/axonframework/config/Configuration.java index 57ee123c3c..0ed74aae7f 100644 --- a/config/src/main/java/org/axonframework/config/Configuration.java +++ b/config/src/main/java/org/axonframework/config/Configuration.java @@ -44,10 +44,10 @@ import org.axonframework.serialization.upcasting.event.EventUpcasterChain; import org.axonframework.tracing.SpanFactory; -import javax.annotation.Nonnull; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; +import javax.annotation.Nonnull; /** * Interface describing the Global Configuration for Axon components. It provides access to the components configured, @@ -412,6 +412,10 @@ default ScopeAwareProvider scopeAwareProvider() { */ EventUpcasterChain upcasterChain(); + default List getDecorators() { + throw new AxonConfigurationException("The getDecorators has not been implemented by the Configuration!"); + } + /** * Returns the {@link SnapshotFilter} combining all defined filters per {@link AggregateConfigurer} in an {@link * SnapshotFilter#combine(SnapshotFilter)} operation. diff --git a/config/src/main/java/org/axonframework/config/Configurer.java b/config/src/main/java/org/axonframework/config/Configurer.java index 886ef1045c..ccb306c98d 100644 --- a/config/src/main/java/org/axonframework/config/Configurer.java +++ b/config/src/main/java/org/axonframework/config/Configurer.java @@ -218,15 +218,37 @@ Configurer registerComponent(@Nonnull Class componentType, * 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 or a wrapped version of it. * - * @param componentType The declared type of the component, typically an interface - * @param decorator The decorator function for this component - * @param The type of component + * @param componentType The declared type of the component, typically an interface + * @param decoratorFunction The decorator function for this component + * @param registerOriginalLifeCycleHandlers Whether the original component's lifecycle handlers should be + * registered. + * @param The deckared 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; + @Nonnull BiFunction decoratorFunction, + boolean registerOriginalLifeCycleHandlers + ) { + throw new AxonConfigurationException( + "The registerComponentDecorator function has not been implemented by the Configurer"); + } + + /** + * Registers a {@link ComponentDecorator 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 or a wrapped version of it. + *

+ * Will register the original's lifecycle handlers. + * + * @param componentType The declared type of the component, typically an interface + * @param decoratorFunction The decorator function for this component + * @param The deckared type of component + * @return the current instance of the Configurer, for chaining purposes + */ + default Configurer registerComponentDecorator(@Nonnull Class componentType, + @Nonnull BiFunction decoratorFunction + ) { + return registerComponentDecorator(componentType, decoratorFunction, true); } /** diff --git a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java index 9ce51f2c1c..cc0277802c 100644 --- a/config/src/main/java/org/axonframework/config/DefaultConfigurer.java +++ b/config/src/main/java/org/axonframework/config/DefaultConfigurer.java @@ -159,10 +159,10 @@ 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<>(); + private final List decorators = new ArrayList<>(); private long lifecyclePhaseTimeout = 5; private TimeUnit lifecyclePhaseTimeunit = TimeUnit.SECONDS; @@ -644,17 +644,16 @@ 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); - } - }); + public Configurer registerComponentDecorator( + @Nonnull Class componentType, + @Nonnull BiFunction decoratorFunction, + boolean registerOriginalLifeCycleHandlers + ) { + if(initialized) { + throw new AxonConfigurationException("Can not register decorators after configuration has been initialized."); + } + decorators.add(new ComponentTypeSafeDecorator<>(componentType, registerOriginalLifeCycleHandlers, decoratorFunction)); return this; } @@ -764,7 +763,6 @@ public Configuration buildConfiguration() { if (!initialized) { verifyIdentifierFactory(); prepareModules(); - invokeDecoratorHandlers(); prepareMessageHandlerRegistrars(); invokeInitHandlers(); } @@ -808,17 +806,6 @@ protected void invokeInitHandlers() { initHandlers.forEach(h -> h.accept(config)); } - /** - * Calls all registered decorators of this configuration and updates the components where appropriate. Decorating - * components is postponed until building to make sure all {@link Component} definitions are present and can be - * decorated. - *

- * Registration of decorates are ignore after initialization. - */ - protected void invokeDecoratorHandlers() { - decoratorHandlers.forEach(Runnable::run); - } - /** * Invokes all registered start handlers. */ @@ -1053,5 +1040,10 @@ public EventUpcasterChain upcasterChain() { public HandlerDefinition handlerDefinition(Class inspectedType) { return handlerDefinition.get().apply(inspectedType); } + + @Override + public List getDecorators() { + return decorators; + } } } diff --git a/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java b/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java index 492d762579..c211a81609 100644 --- a/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java +++ b/config/src/test/java/org/axonframework/config/DefaultConfigurerLifecycleOperationsTest.java @@ -482,8 +482,8 @@ void decoratedComponentsHaveLifeCycleHandlersRegistered() { .defaultConfiguration() .registerComponent(LifeCycleComponent.class, config -> original) .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator1) - .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator2) - .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator3) + .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator2 ) + .registerComponentDecorator(LifeCycleComponent.class, (config, o) -> decorator3, false) .buildConfiguration(); testSubject.getComponent(LifeCycleComponent.class); @@ -494,12 +494,12 @@ void decoratedComponentsHaveLifeCycleHandlersRegistered() { inOrder(original, decorator1, decorator2, decorator3); lifecycleOrder.verify(original).registerLifecycleHandlers(any()); lifecycleOrder.verify(decorator1).registerLifecycleHandlers(any()); - lifecycleOrder.verify(decorator2).registerLifecycleHandlers(any()); + lifecycleOrder.verify(decorator2, never()).registerLifecycleHandlers(any()); lifecycleOrder.verify(decorator3).registerLifecycleHandlers(any()); assertTrue(original.isInvoked()); assertTrue(decorator1.isInvoked()); - assertTrue(decorator2.isInvoked()); + assertFalse(decorator2.isInvoked()); assertTrue(decorator3.isInvoked()); } diff --git a/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java b/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java index 9d7baf847f..146e6e75a2 100644 --- a/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java +++ b/config/src/test/java/org/axonframework/config/DefaultConfigurerTest.java @@ -638,7 +638,7 @@ void canDecorateComponentsSuchAsSpanFactory() { (configuration, component) -> { assertSame(custom, component); return decorated; - }) + }, false) .buildConfiguration() .spanFactory(); From 7ec37019029da56f5e435903b4ef4900bb173951 Mon Sep 17 00:00:00 2001 From: Mitchell Herrijgers Date: Fri, 13 Jan 2023 16:31:04 +0100 Subject: [PATCH 4/4] Fix typo in javadoc of the Configurer --- config/src/main/java/org/axonframework/config/Configurer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/axonframework/config/Configurer.java b/config/src/main/java/org/axonframework/config/Configurer.java index ccb306c98d..f3fbbaa2e6 100644 --- a/config/src/main/java/org/axonframework/config/Configurer.java +++ b/config/src/main/java/org/axonframework/config/Configurer.java @@ -222,7 +222,7 @@ Configurer registerComponent(@Nonnull Class componentType, * @param decoratorFunction The decorator function for this component * @param registerOriginalLifeCycleHandlers Whether the original component's lifecycle handlers should be * registered. - * @param The deckared type of component + * @param The declared type of component * @return the current instance of the Configurer, for chaining purposes */ default Configurer registerComponentDecorator(@Nonnull Class componentType, @@ -242,7 +242,7 @@ default Configurer registerComponentDecorator(@Nonnull Class componentTyp * * @param componentType The declared type of the component, typically an interface * @param decoratorFunction The decorator function for this component - * @param The deckared type of component + * @param The declared type of component * @return the current instance of the Configurer, for chaining purposes */ default Configurer registerComponentDecorator(@Nonnull Class componentType,