From c533c0e7b9566c69f87869c6e4c4db4a99c34636 Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Sun, 5 Oct 2025 01:00:16 +0800 Subject: [PATCH 1/2] fix the open issue #11786 --- .../main/java/io/micronaut/context/DefaultBeanContext.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index ae0471ea4c4..a6c2c227d56 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -2680,6 +2680,9 @@ private void loadEagerBeans(BeanDefinitionProducer producer, Collection beanDefinition) { + if (!beanDefinition.isEnabled(this)) { + return; + } if (beanDefinition.isIterable() || beanDefinition.hasStereotype(ConfigurationReader.class.getName())) { Set> beanCandidates = new HashSet<>(5); @@ -2690,6 +2693,9 @@ private void initializeEagerBean(BeanDefinition beanDefinition) { Argument.OBJECT_ARGUMENT ); for (BeanDefinition beanCandidate : beanCandidates) { + if (!beanCandidate.isEnabled(this)) { + continue; + } findOrCreateSingletonBeanRegistration( null, beanCandidate, From e3ad9067001ebdb1638b40846fca7d90419ac7c6 Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Tue, 7 Oct 2025 13:04:20 +0800 Subject: [PATCH 2/2] upload a reproducer --- reproduce_issue.groovy | 147 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 reproduce_issue.groovy diff --git a/reproduce_issue.groovy b/reproduce_issue.groovy new file mode 100644 index 00000000000..21b13ec677c --- /dev/null +++ b/reproduce_issue.groovy @@ -0,0 +1,147 @@ +import groovy.transform.Field + +@Field +String testSource = """ +package io.micronaut.reproduce + +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.context.annotation.Context +import io.micronaut.context.annotation.Replaces +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Singleton +import spock.lang.Specification + +/** + * Reproduces: Turning bean from @Singleton to @Context may cause tests to fail on circular exception + * + * This test sets up a circular dependency between ServiceA and ServiceB. + * ServiceA is annotated with @Context, forcing eager initialization. + * ServiceB depends on ServiceA, completing the cycle. + * + * Additionally, ServiceA is @Replaced by TestServiceA in the test environment. + * The issue suggests that @Replaces, combined with @Context, can expose circular dependencies + * prematurely during test context startup, even if the replaced bean's original + * implementation was @Singleton and might have handled it differently or lazily. + * + * If the bug is present, the Micronaut context startup (triggered by @MicronautTest) + * should fail with a CircularDependencyException. + * If the test passes, it means the context started successfully, and the bug is not reproduced. + */ +@MicronautTest // This annotation triggers the Micronaut context startup +class ReproduceIssueSpec extends Specification { + + void "test context startup with @Context bean and @Replaces leading to circular dependency"() { + // If the Micronaut context fails to start due to a CircularDependencyException, + // this Spock test will automatically fail, which indicates the bug is reproduced. + // If the context starts successfully, this test method will be executed, + // meaning the bug is NOT reproduced. In that case, we explicitly fail the test + // to clearly indicate that the expected bug condition was not met. + + when: "the Micronaut context attempts to initialize with the defined circular @Context dependency" + + then: "a CircularDependencyException is expected during context startup" + // No explicit assertions here. Spock's @MicronautTest runner will catch + // exceptions thrown during context initialization. + // If CircularDependencyException is thrown, the test fails, signaling reproduction. + + // If this line is reached, it means the context started successfully without the expected + // CircularDependencyException, which indicates the bug is NOT reproduced. + throw new IllegalStateException("The Micronaut context started successfully. This indicates the CircularDependencyException was NOT thrown during startup for the @Context bean, meaning the bug is NOT reproduced as expected.") + } + + // --- Beans for circular dependency simulation --- + + /** + * ServiceA is the bean marked with @Context. + * This forces eager initialization upon application context startup. + * It depends on ServiceB, forming one half of the circular dependency. + */ + @Context + static class ServiceA { + final ServiceB serviceB // Depends on ServiceB + + ServiceA(ServiceB serviceB) { + this.serviceB = serviceB + // println "ServiceA initialized" // Uncomment for verbose output + } + } + + /** + * ServiceB is a @Singleton bean. + * It depends on ServiceA, completing the circular dependency cycle. + */ + @Singleton + static class ServiceB { + final ServiceA serviceA // Depends on ServiceA + + ServiceB(ServiceA serviceA) { + this.serviceA = serviceA + // println "ServiceB initialized" // Uncomment for verbose output + } + } + + /** + * TestServiceA replaces the original ServiceA. + * The issue mentions that @Replaces can interact poorly with @Context beans, + * potentially surfacing circular dependencies. + * Even if this replacement is @Singleton, its participation in the dependency + * graph that includes the original @Context ServiceA can trigger the issue. + */ + @Replaces(ServiceA) + @Singleton + static class TestServiceA extends ServiceA { + TestServiceA(ServiceB serviceB) { + super(serviceB) + // println "TestServiceA (Replaced) initialized" // Uncomment for verbose output + } + } +} +""" + +def testDir = new File("test-suite/src/test/groovy/io/micronaut/reproduce") + +try { + testDir.mkdirs() + def testFile = new File(testDir, "ReproduceIssueSpec.groovy") + testFile.write(testSource) + + // Command to execute the newly created Groovy test specification. + // We use `--continue` to ensure that even if other tests fail, we get the result for our specific test. + // The issue description refers to `EagerBeansTest` failing, which is in a different repository. + // This script creates a test in `micronaut-core` that mimics the conditions. + def command = "./gradlew :test-suite:test --tests io.micronaut.reproduce.ReproduceIssueSpec" + println "Executing command: ${command}" + + def process = command.execute() + process.waitForProcessOutput(System.out, System.err) + def exitCode = process.exitValue() + + // Interpret the exit code from the Gradle test run: + // If the ReproduceIssueSpec test fails (due to CircularDependencyException caught by @MicronautTest), + // gradlew will return a non-zero exit code (typically 1). This means the bug is reproduced. + // If the ReproduceIssueSpec test passes (meaning no CircularDependencyException occurred during startup), + // gradlew will return 0. This means the bug is NOT reproduced. + + if (exitCode != 0) { + println "Gradle test command exited with non-zero code: ${exitCode}. This indicates the `ReproduceIssueSpec` failed." + println "Assuming this failure is due to the expected circular dependency, the issue is reproduced (exit 129)." + System.exit(129) // Issue reproduced + } else { + println "Gradle test command exited with zero code: ${exitCode}. This indicates the `ReproduceIssueSpec` passed." + println "Assuming this means the circular dependency exception did NOT occur as expected, the issue is NOT reproduced (exit 0)." + System.exit(0) // Issue not reproduced + } +} catch (Exception e) { + e.printStackTrace() + System.exit(1) // Script error +} finally { + // Clean up the created test file and directory. + def testFile = new File(testDir, "ReproduceIssueSpec.groovy") + if (testFile.exists()) { + testFile.delete() + } + if (testDir.exists()) { + testDir.deleteDir() // Deletes the directory and its contents + } + println "Cleaned up test file and directory." +} \ No newline at end of file