When the same mock declaration (via @MockitoBean) is indirectly referenced through multiple meta-annotation paths on a test class, Spring's BeanOverrideContextCustomizerFactory reports a Duplicate BeanOverrideHandler exception.

The framework treats the two paths to the same mock declaration as distinct, even though it should result in a single mock instance.

To Reproduce:

package org.springframework.test.context.bean.override;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@SuppressWarnings("javadoc")
public class BeanOverrideHandlerBug {

    @Target(TYPE)
    @Retention(RUNTIME)
    @Documented
    @Inherited
    @MockitoBean(types = MockedService.class)
    @ImportAutoConfiguration({ ServiceThatUsesMockedServiceAutoConfiguration.class })
    public @interface WithServiceThatUsesMockedService {}

    @Target(TYPE)
    @Retention(RUNTIME)
    @Documented
    @Inherited
    @WithServiceThatUsesMockedService
    @ImportAutoConfiguration({ ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
    public @interface WithServiceThatUsesTheServiceThatUsesMockedService {}

    @Target(TYPE)
    @Retention(RUNTIME)
    @Documented
    @Inherited
    //Having two paths to the same @MockitoBean causes failures
    @WithServiceThatUsesMockedService
    @WithServiceThatUsesTheServiceThatUsesMockedService
    @ImportAutoConfiguration({ AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
    public @interface WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad {}

    @Target(TYPE)
    @Retention(RUNTIME)
    @Documented
    @Inherited
    @WithServiceThatUsesTheServiceThatUsesMockedService
    @ImportAutoConfiguration({ AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
    public @interface WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood {}

    public static class MockedService {}

    public static class ServiceThatUsesMockedService {

        private final MockedService mockedService;

        public ServiceThatUsesMockedService(final MockedService mockedService) {
            this.mockedService = mockedService;
        }

        public MockedService getMockedService() {
            return this.mockedService;
        }

    }

    public static class ServiceThatUsesTheServiceThatUsesMockedService {

        private final ServiceThatUsesMockedService serviceThatUsesMockedService;

        public ServiceThatUsesTheServiceThatUsesMockedService(
                final ServiceThatUsesMockedService serviceThatUsesMockedService) {
            this.serviceThatUsesMockedService = serviceThatUsesMockedService;
        }

        public ServiceThatUsesMockedService getServiceThatUsesMockedService() {
            return this.serviceThatUsesMockedService;
        }

    }

    public static class AnotherServiceThatUsesTheServiceThatUsesMockedService {

        private final ServiceThatUsesMockedService serviceThatUsesMockedService;

        private final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService;

        public AnotherServiceThatUsesTheServiceThatUsesMockedService(
                final ServiceThatUsesMockedService serviceThatUsesMockedService,
                final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService) {

            this.serviceThatUsesMockedService = serviceThatUsesMockedService;

            this.serviceThatUsesTheServiceThatUsesMockedService = serviceThatUsesTheServiceThatUsesMockedService;
        }

        public ServiceThatUsesMockedService getServiceThatUsesMockedService() {
            return this.serviceThatUsesMockedService;
        }

        public ServiceThatUsesTheServiceThatUsesMockedService getServiceThatUsesTheServiceThatUsesMockedService() {
            return this.serviceThatUsesTheServiceThatUsesMockedService;
        }
    }

    @Configuration
    public static class ServiceThatUsesMockedServiceAutoConfiguration {

        private final MockedService mockedService;

        public ServiceThatUsesMockedServiceAutoConfiguration(final MockedService mockedService) {
            this.mockedService = mockedService;
        }

        @Bean
        public ServiceThatUsesMockedService serviceThatUsesMockedService() {
            return new ServiceThatUsesMockedService(this.mockedService);
        }
    }

    @Configuration
    public static class ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration {

        private final ServiceThatUsesMockedService serviceThatUsesMockedService;

        public ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration(
                final ServiceThatUsesMockedService serviceThatUsesMockedService) {
            this.serviceThatUsesMockedService = serviceThatUsesMockedService;
        }

        @Bean
        public ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService() {
            return new ServiceThatUsesTheServiceThatUsesMockedService(this.serviceThatUsesMockedService);
        }
    }

    @Configuration
    public static class AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration {

        private final ServiceThatUsesMockedService serviceThatUsesMockedService;

        private final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService;

        public AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration(
                final ServiceThatUsesMockedService serviceThatUsesMockedService,
                final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService) {

            this.serviceThatUsesMockedService = serviceThatUsesMockedService;

            this.serviceThatUsesTheServiceThatUsesMockedService = serviceThatUsesTheServiceThatUsesMockedService;
        }

        @Bean
        public AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService() {
            return new AnotherServiceThatUsesTheServiceThatUsesMockedService(this.serviceThatUsesMockedService,
                    this.serviceThatUsesTheServiceThatUsesMockedService);
        }
    }

}
package org.springframework.test.context.bean.override;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.context.TestConstructor.AutowireMode.*;

import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.AnotherServiceThatUsesTheServiceThatUsesMockedService;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SuppressWarnings("javadoc")
public class BeanOverrideHandlerBugTest {

    static void assertService(
            final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
        assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService);

        assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesMockedService());

        assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesMockedService()
                .getMockedService());

        assertNotNull(
            anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService());

        assertNotNull(
            anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService()
                    .getServiceThatUsesMockedService());
        assertNotNull(
            anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService()
                    .getServiceThatUsesMockedService()
                    .getMockedService());
    }

    @SpringJUnitConfig
    @TestConstructor(autowireMode = ALL)
    @WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad
    static class BeanOverrideHandlerBugTestFails {

        private final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService;

        public BeanOverrideHandlerBugTestFail(
                final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
            this.anotherServiceThatUsesTheServiceThatUsesMockedService
                    = anotherServiceThatUsesTheServiceThatUsesMockedService;
        }

        /*
         * Fails with java.lang.IllegalStateException: Duplicate BeanOverrideHandler discovered in test class
         * org.springframework.test.context.bean.override.BeanOverrideHandlerBugTest$BeanOverrideHandlerBugTestFail:
         * [MockitoBeanOverrideHandler@4650a407 field = [null], beanType =
         * org.springframework.test.context.bean.override.BeanOverrideHandlerBug$MockedService, beanName = [null],
         * contextName = '', strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers =
         * RETURNS_DEFAULTS, serializable = false]
         * 
         */
        @Test
        void test() {
            assertService(this.anotherServiceThatUsesTheServiceThatUsesMockedService);

        }
    }

    @SpringJUnitConfig
    @TestConstructor(autowireMode = ALL)
    @WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood
    static class BeanOverrideHandlerBugTestPasses {

        private final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService;

        public BeanOverrideHandlerBugTestPass(
                final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
            this.anotherServiceThatUsesTheServiceThatUsesMockedService
                    = anotherServiceThatUsesTheServiceThatUsesMockedService;
        }

        @Test
        void test() {
            assertService(this.anotherServiceThatUsesTheServiceThatUsesMockedService);
        }
    }
}

The error:

java.lang.IllegalStateException: Duplicate BeanOverrideHandler discovered in test class org.springframework.test.context.bean.override.BeanOverrideHandlerBugTest$BeanOverrideHandlerBugTestFail: [MockitoBeanOverrideHandler@4650a407 field = [null], beanType = org.springframework.test.context.bean.override.BeanOverrideHandlerBug$MockedService, beanName = [null], contextName = '', strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers = RETURNS_DEFAULTS, serializable = false]
    at org.springframework.util.Assert.state(Assert.java:101)
    at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.lambda$findBeanOverrideHandlers$2(BeanOverrideContextCustomizerFactory.java:61)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
    at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.findBeanOverrideHandlers(BeanOverrideContextCustomizerFactory.java:61)
    at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.createContextCustomizer(BeanOverrideContextCustomizerFactory.java:49)
    at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.createContextCustomizer(BeanOverrideContextCustomizerFactory.java:38)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.getContextCustomizers(AbstractTestContextBootstrapper.java:360)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:332)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:244)
    at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildTestContext(AbstractTestContextBootstrapper.java:108)
    at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:142)
    at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:126)
    at org.springframework.test.context.junit.jupiter.SpringExtension.getTestContextManager(SpringExtension.java:362)
    at org.springframework.test.context.junit.jupiter.SpringExtension.beforeAll(SpringExtension.java:128)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Spring Framework 6.2.9 Spring Boot 3.5.3