Basic information
-
Axon Framework version: 5.1.1-SNAPSHOT (also reproduced against axon-spring:5.1.0)
-
JDK version: 25
-
Spring Boot version: 3.5.14 (Spring Framework 6.2.18)
-
Complete executable reproducer if available (e.g. GitHub Repo):
MateuszNaKodach/HeroesOfDomainDrivenDesign.EventSourcing.Java.Axon.Spring,
branch af5-migration/test1. The failure surfaces on top of commit
d3319d9
("fix(af5-migration): enable Spring Boot test context for AF5 JPA event store"), which got the
Spring test context loading under AF5; with the JPA/@EntityScan blockers out of the way, the
next layer — @MockitoSpyBean CommandGateway — fails as described below. The three affected
tests in that repo are:
src/test/java/com/dddheroes/heroesofddd/astrologers/automation/whenweekstartedthenproclaimweeksymbol/WhenWeekStartedThenProclaimWeekSymbolTest.java
src/test/java/com/dddheroes/heroesofddd/astrologers/automation/whenweeksymbolproclaimedthenincreasedwellingavailablecreatures/WhenWeekSymbolProclaimedThenIncreaseDwellingAvailableCreaturesTest.java
src/test/java/com/dddheroes/heroesofddd/creaturerecruitment/automation/WhenCreatureRecruitedThenAddToArmyTest.java
Run ./mvnw test -Dtest='WhenWeekStartedThenProclaimWeekSymbolTest,WhenWeekSymbolProclaimedThenIncreaseDwellingAvailableCreaturesTest,WhenCreatureRecruitedThenAddToArmyTest'
on top of commit d3319d9 (and before the workaround commit
0d22913
— see the "Practical workaround" section — has been applied) to reproduce the failure on the
unmodified tests.
Steps to reproduce
-
Add axon-spring-boot-starter (or axoniq-spring-boot-starter) to a Spring Boot 3.x project.
-
Write a @SpringBootTest with a @MockitoSpyBean (or @MockitoBean) field of an AF5 framework
component, e.g.:
@SpringBootTest
class WhenSomethingThenSendCommandTest {
@MockitoSpyBean
private CommandGateway commandGateway;
@Test
void verifies_command_dispatch() {
// ...
}
}
-
Run the test.
Expected behaviour
The Spring test context loads, the @MockitoSpyBean wraps the AF5-registered CommandGateway
bean, and verify(commandGateway, ...).send(...) works as it would for any other Spring bean.
Actual behaviour
Context refresh is aborted before any bean is instantiated:
java.lang.IllegalStateException: Unable to select a bean to wrap: there are no beans of type
org.axonframework.messaging.commandhandling.gateway.CommandGateway (as required by field
'WhenSomethingThenSendCommandTest.commandGateway'). If the bean is defined in a @Bean method,
make sure the return type is the most specific type possible (for example, the concrete
implementation type).
at org.springframework.test.context.bean.override.BeanOverrideRegistrar ...
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization ...
The exact same symptom occurs with @MockitoBean and with every other framework component
registered through AF5's component registry (EventGateway, QueryGateway, EventSink,
CommandBus, EventBus, QueryBus, etc.).
Root cause
SpringComponentRegistry registers AF5 components as Spring BeanDefinitions lazily, from
inside SpringComponentRegistry#initialize(). That method is invoked from
postProcessAfterInitialization (SpringComponentRegistry.java:300-363) — i.e. it only runs once
the first non-infrastructure bean is being post-processed during the bean instantiation phase.
The actual registration happens in registerLocalComponentsWithApplicationContext
(SpringComponentRegistry.java:566-585).
Spring Boot's @MockitoSpyBean / @MockitoBean is installed by BeanOverrideContextCustomizer,
which contributes a BeanFactoryPostProcessor (BeanOverrideRegistrar). BFPPs run before any
bean is instantiated, and BeanOverrideRegistrar.applyBeanOverrides requires a BeanDefinition
of the overridden type to already be present at that moment — otherwise it raises
Unable to select a bean to wrap.
The two phases never line up:
[ BFPP phase ] BeanOverrideRegistrar looks up CommandGateway -> not registered yet -> FAIL
[ BPP phase ] SpringComponentRegistry.initialize() would have
registered CommandGateway as a BeanDefinition
(never reached because refresh was aborted)
This means @MockitoSpyBean / @MockitoBean cannot wrap any AF5 component out of the box.
Why the obvious workaround does not work
A user-level @Bean method that exposes the framework component via AxonConfiguration:
@Configuration
class AxonGatewaysBeanConfiguration {
@Bean
CommandGateway commandGateway(AxonConfiguration axonConfiguration) {
return axonConfiguration.getComponent(CommandGateway.class);
}
}
…causes a BeanCurrentlyInCreationException. The cycle is in
SpringComponentRegistry$SpringConfiguration.getComponent (SpringComponentRegistry.java:640):
@Override
public <C> C getComponent(Class<C> type) {
try {
return beanFactory.getBean(type); // <-- delegates back to Spring
} ...
}
SpringConfiguration is a thin proxy over Spring (not a separate registry), so the user
@Bean resolves itself through beanFactory.getBean(CommandGateway.class) while it is still
in creation:
Spring creates `commandGateway` (factory method)
-> axonConfiguration.getComponent(CommandGateway.class)
-> SpringConfiguration.getComponent
-> beanFactory.getBean(CommandGateway.class)
-> "Requested bean is currently in creation"
Practical workaround (consumer side)
Wrap the AF5-registered bean with Mockito.spy(...) from a BeanPostProcessor and use
@Autowired in the test instead of @MockitoSpyBean:
@TestConfiguration
public class CommandGatewaySpyConfiguration {
@Bean
static BeanPostProcessor commandGatewaySpyPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof CommandGateway gateway
&& CommandGateway.class.getName().equals(beanName)) {
return Mockito.spy(gateway);
}
return bean;
}
};
}
}
@SpringBootTest
@Import(CommandGatewaySpyConfiguration.class)
class WhenSomethingThenSendCommandTest {
@Autowired
private CommandGateway commandGateway;
// verify(commandGateway, times(1)).send(eq(cmd), eq(meta), any());
}
By the time this BPP runs on the bean named
org.axonframework.messaging.commandhandling.gateway.CommandGateway (the FQCN under which
SpringComponentRegistry.registerLocalComponentsWithApplicationContext registers it),
SpringComponentRegistry.initialize() has already produced the fully-decorated
ConvertingCommandGateway. Replacing the singleton-cache entry with Mockito.spy(real) makes
the spy visible to every subsequent getBean(CommandGateway.class) lookup — including the
parameter-injection performed by AF5 for automation processors — so verify(...) works as it
would for @MockitoSpyBean.
The drawback is that consumers have to roll their own BPP per spied/mocked component, and have
to abandon Spring Boot's standard @MockitoSpyBean / @MockitoBean annotations.
Suggested fix
Move framework-component BeanDefinition registration out of the BPP phase into a
BeanDefinitionRegistryPostProcessor's postProcessBeanDefinitionRegistry callback (or any
other hook that runs before regular BeanFactoryPostProcessors). The BeanDefinition already
uses a Supplier factory (() -> component.resolve(configuration)) in
registerLocalComponentsWithApplicationContext, so only the definition needs to move earlier;
the actual Component.resolve(...) call can still happen lazily when Spring needs an instance.
Roughly:
// SpringComponentRegistry implements BeanDefinitionRegistryPostProcessor
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
initializeIfNeeded(); // run enhancers, build Components, decorate
components.postProcessComponents(component -> {
String name = nameFor(component);
if (registry.containsBeanDefinition(name)) return;
AbstractBeanDefinition def = BeanDefinitionBuilder
.rootBeanDefinition(
ResolvableType.forType(component.identifier().type().getType()),
() -> component.resolve(configuration))
.getBeanDefinition();
registry.registerBeanDefinition(name, def);
});
}
With that change in place:
@MockitoSpyBean / @MockitoBean works against CommandGateway, EventGateway,
QueryGateway, EventSink, CommandBus, EventBus, QueryBus, and every other framework
component, without per-project bridging.
- The existing BPP-side decoration logic in
postProcessAfterInitialization keeps working for
user-defined beans that should be wrapped/decorated by AF5.
- The Spring Boot 4 infrastructure-bean hazard noted in
SpringComponentRegistry#isInfrastructureBean
remains addressable: skipping initialization while only ROLE_INFRASTRUCTURE beans exist is
not relevant during the BDRPP phase since no beans are instantiated there.
Additional context
- Affected AF artifacts:
org.axonframework.extension.spring:axon-spring,
org.axonframework.extension.spring:axon-spring-boot-starter,
io.axoniq.framework:axoniq-spring-boot-starter.
- The bug report uses
@MockitoSpyBean / @MockitoBean as the concrete example because that is
the first place real-world consumers hit it, but the root cause — framework BeanDefinitions
being registered only during the bean-instantiation phase — generalises to any Spring
facility that needs to see those BeanDefinitions at the BeanFactoryPostProcessor phase or
earlier. Other consumers that are likely affected by the same race:
- The Spring Framework Bean Override API in general —
@TestBean (Spring Framework 6.2+),
@MockitoSpyBean, @MockitoBean, custom BeanOverrideProcessor implementations
(BeanOverrideContextCustomizer / BeanOverrideRegistrar).
- Spring Boot's
ApplicationContextRunner.withBean(...) / withUserConfiguration(...) when a
user configuration redeclares a framework component.
- Conditional-on-bean evaluation in user
@Configuration classes that runs before any framework
bean is instantiated (e.g. @ConditionalOnMissingBean(CommandGateway.class) on a user-supplied
fallback).
- Spring Boot AOT/native image generation, which traverses
BeanDefinitions ahead-of-time.
- Any user
BeanDefinitionRegistryPostProcessor / BeanFactoryPostProcessor that inspects or
decorates framework BeanDefinitions.
Basic information
Axon Framework version:
5.1.1-SNAPSHOT(also reproduced againstaxon-spring:5.1.0)JDK version: 25
Spring Boot version: 3.5.14 (Spring Framework 6.2.18)
Complete executable reproducer if available (e.g. GitHub Repo):
MateuszNaKodach/HeroesOfDomainDrivenDesign.EventSourcing.Java.Axon.Spring,branch
af5-migration/test1. The failure surfaces on top of commitd3319d9("fix(af5-migration): enable Spring Boot test context for AF5 JPA event store"), which got the
Spring test context loading under AF5; with the JPA/
@EntityScanblockers out of the way, thenext layer —
@MockitoSpyBean CommandGateway— fails as described below. The three affectedtests in that repo are:
src/test/java/com/dddheroes/heroesofddd/astrologers/automation/whenweekstartedthenproclaimweeksymbol/WhenWeekStartedThenProclaimWeekSymbolTest.javasrc/test/java/com/dddheroes/heroesofddd/astrologers/automation/whenweeksymbolproclaimedthenincreasedwellingavailablecreatures/WhenWeekSymbolProclaimedThenIncreaseDwellingAvailableCreaturesTest.javasrc/test/java/com/dddheroes/heroesofddd/creaturerecruitment/automation/WhenCreatureRecruitedThenAddToArmyTest.javaRun
./mvnw test -Dtest='WhenWeekStartedThenProclaimWeekSymbolTest,WhenWeekSymbolProclaimedThenIncreaseDwellingAvailableCreaturesTest,WhenCreatureRecruitedThenAddToArmyTest'on top of commit
d3319d9(and before the workaround commit0d22913— see the "Practical workaround" section — has been applied) to reproduce the failure on the
unmodified tests.
Steps to reproduce
Add
axon-spring-boot-starter(oraxoniq-spring-boot-starter) to a Spring Boot 3.x project.Write a
@SpringBootTestwith a@MockitoSpyBean(or@MockitoBean) field of an AF5 frameworkcomponent, e.g.:
Run the test.
Expected behaviour
The Spring test context loads, the
@MockitoSpyBeanwraps the AF5-registeredCommandGatewaybean, and
verify(commandGateway, ...).send(...)works as it would for any other Spring bean.Actual behaviour
Context refresh is aborted before any bean is instantiated:
The exact same symptom occurs with
@MockitoBeanand with every other framework componentregistered through AF5's component registry (
EventGateway,QueryGateway,EventSink,CommandBus,EventBus,QueryBus, etc.).Root cause
SpringComponentRegistryregisters AF5 components as SpringBeanDefinitions lazily, frominside
SpringComponentRegistry#initialize(). That method is invoked frompostProcessAfterInitialization(SpringComponentRegistry.java:300-363) — i.e. it only runs oncethe first non-infrastructure bean is being post-processed during the bean instantiation phase.
The actual registration happens in
registerLocalComponentsWithApplicationContext(
SpringComponentRegistry.java:566-585).Spring Boot's
@MockitoSpyBean/@MockitoBeanis installed byBeanOverrideContextCustomizer,which contributes a
BeanFactoryPostProcessor(BeanOverrideRegistrar). BFPPs run before anybean is instantiated, and
BeanOverrideRegistrar.applyBeanOverridesrequires aBeanDefinitionof the overridden type to already be present at that moment — otherwise it raises
Unable to select a bean to wrap.The two phases never line up:
This means
@MockitoSpyBean/@MockitoBeancannot wrap any AF5 component out of the box.Why the obvious workaround does not work
A user-level
@Beanmethod that exposes the framework component viaAxonConfiguration:…causes a
BeanCurrentlyInCreationException. The cycle is inSpringComponentRegistry$SpringConfiguration.getComponent(SpringComponentRegistry.java:640):SpringConfigurationis a thin proxy over Spring (not a separate registry), so the user@Beanresolves itself throughbeanFactory.getBean(CommandGateway.class)while it is stillin creation:
Practical workaround (consumer side)
Wrap the AF5-registered bean with
Mockito.spy(...)from aBeanPostProcessorand use@Autowiredin the test instead of@MockitoSpyBean:By the time this BPP runs on the bean named
org.axonframework.messaging.commandhandling.gateway.CommandGateway(the FQCN under whichSpringComponentRegistry.registerLocalComponentsWithApplicationContextregisters it),SpringComponentRegistry.initialize()has already produced the fully-decoratedConvertingCommandGateway. Replacing the singleton-cache entry withMockito.spy(real)makesthe spy visible to every subsequent
getBean(CommandGateway.class)lookup — including theparameter-injection performed by AF5 for automation processors — so
verify(...)works as itwould for
@MockitoSpyBean.The drawback is that consumers have to roll their own BPP per spied/mocked component, and have
to abandon Spring Boot's standard
@MockitoSpyBean/@MockitoBeanannotations.Suggested fix
Move framework-component
BeanDefinitionregistration out of the BPP phase into aBeanDefinitionRegistryPostProcessor'spostProcessBeanDefinitionRegistrycallback (or anyother hook that runs before regular
BeanFactoryPostProcessors). TheBeanDefinitionalreadyuses a
Supplierfactory (() -> component.resolve(configuration)) inregisterLocalComponentsWithApplicationContext, so only the definition needs to move earlier;the actual
Component.resolve(...)call can still happen lazily when Spring needs an instance.Roughly:
With that change in place:
@MockitoSpyBean/@MockitoBeanworks againstCommandGateway,EventGateway,QueryGateway,EventSink,CommandBus,EventBus,QueryBus, and every other frameworkcomponent, without per-project bridging.
postProcessAfterInitializationkeeps working foruser-defined beans that should be wrapped/decorated by AF5.
SpringComponentRegistry#isInfrastructureBeanremains addressable: skipping initialization while only
ROLE_INFRASTRUCTUREbeans exist isnot relevant during the BDRPP phase since no beans are instantiated there.
Additional context
org.axonframework.extension.spring:axon-spring,org.axonframework.extension.spring:axon-spring-boot-starter,io.axoniq.framework:axoniq-spring-boot-starter.@MockitoSpyBean/@MockitoBeanas the concrete example because that isthe first place real-world consumers hit it, but the root cause — framework
BeanDefinitionsbeing registered only during the bean-instantiation phase — generalises to any Spring
facility that needs to see those
BeanDefinitions at theBeanFactoryPostProcessorphase orearlier. Other consumers that are likely affected by the same race:
@TestBean(Spring Framework 6.2+),@MockitoSpyBean,@MockitoBean, customBeanOverrideProcessorimplementations(
BeanOverrideContextCustomizer/BeanOverrideRegistrar).ApplicationContextRunner.withBean(...)/withUserConfiguration(...)when auser configuration redeclares a framework component.
@Configurationclasses that runs before any frameworkbean is instantiated (e.g.
@ConditionalOnMissingBean(CommandGateway.class)on a user-suppliedfallback).
BeanDefinitions ahead-of-time.BeanDefinitionRegistryPostProcessor/BeanFactoryPostProcessorthat inspects ordecorates framework
BeanDefinitions.