From c2328188f2ad5a87658e1dd0c3b0d7dddc6168ea Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Sun, 5 Oct 2025 00:56:45 +0800 Subject: [PATCH 1/2] fix the open issue #11754 --- .../runtime/context/scope/refresh/RefreshScope.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java b/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java index 81bd851c119..2e52638c9f4 100644 --- a/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java +++ b/context/src/main/java/io/micronaut/runtime/context/scope/refresh/RefreshScope.java @@ -176,7 +176,7 @@ private void refreshSubsetOfConfigurationProperties(Set keySet) { BeanDefinition definition = registration.getBeanDefinition(); Optional value = definition.stringValue(ConfigurationReader.class, "prefix"); if (value.isPresent()) { - String configPrefix = value.get(); + String configPrefix = io.micronaut.core.naming.NameUtils.hyphenate(value.get()); if (keySet.stream().anyMatch(key -> key.startsWith(configPrefix))) { beanContext.refreshBean(registration); } @@ -198,8 +198,9 @@ private void disposeOfBeanSubset(Collection keys) { String[] strings = definition.stringValues(Refreshable.class); if (!ArrayUtils.isEmpty(strings)) { for (String prefix : strings) { + String normalizedPrefix = io.micronaut.core.naming.NameUtils.hyphenate(prefix); for (String k : keys) { - if (k.startsWith(prefix)) { + if (k.startsWith(normalizedPrefix)) { disposeOfBean(entry.getKey()); } } From 92a38c2cb4128c89d01956f3ec99049e0b9b2fb6 Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Tue, 7 Oct 2025 13:01:37 +0800 Subject: [PATCH 2/2] upload a reproducer --- reproduce_issue.groovy | 102 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 reproduce_issue.groovy diff --git a/reproduce_issue.groovy b/reproduce_issue.groovy new file mode 100644 index 00000000000..95865e3d730 --- /dev/null +++ b/reproduce_issue.groovy @@ -0,0 +1,102 @@ +import groovy.transform.Field + +@Field +String testSource = """ +package io.micronaut.reproduce + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Refreshable +import io.micronaut.context.env.Environment +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class ReproduceIssueSpec extends Specification { + + // Define the configuration class that uses a camelCase name in @ConfigurationProperties + @ConfigurationProperties("myCamelCaseConfig") // Annotated with camelCase + @Refreshable // Make it refreshable + static class MyCamelCaseConfig { + String myProperty + } + + void "test @ConfigurationProperties and @Refreshable with camelCase annotated name does not refresh from kebab-case property source"() { + given: "an initial configuration with kebab-case property key that maps to the camelCase annotated bean name" + def initialProperties = [ + "my-camel-case-config.my-property": "initialValue" + ] + + def context = ApplicationContext.run(initialProperties) + def environment = context.environment + + def configBean = context.getBean(MyCamelCaseConfig) + + expect: "the bean is initially loaded correctly from the kebab-case property" + configBean.myProperty == "initialValue" + + when: "the configuration is changed with new kebab-case values and refresh is triggered" + def updatedProperties = [ + "my-camel-case-config.my-property": "updatedValue" + ] + // Add a new property source with higher precedence to simulate a change in configuration + environment.addPropertySource(PropertySource.of("test-update", updatedProperties)) + + // Trigger the refresh mechanism for @Refreshable beans + def diff = environment.refreshAndDiff() + + then: "the environment should detect the change for the kebab-case property" + // Verify that the environment itself registers the property change. + def changedPropertyNames = diff.getChanged().collect { it.name } + changedPropertyNames.contains("my-camel-case-config.my-property") + + and: "the config bean's property should NOT be updated if the bug is reproduced, otherwise it is fixed" + if (configBean.myProperty == "initialValue") { + // If the property remains "initialValue", it means the refresh did not occur as expected + // for the camelCase-named @ConfigurationProperties bean. + // This indicates the bug is reproduced. Throw an AssertionError to make the Gradle test task fail. + throw new AssertionError("BUG REPRODUCED: Property 'myCamelCaseConfig.myProperty' did not refresh from 'my-camel-case-config.my-property'. Expected 'updatedValue' but got 'initialValue'.") + } else { + // If the property updated to "updatedValue", it means the bug is fixed. + // The test passes in this case, indicating a fix. + configBean.myProperty == "updatedValue" + } + + cleanup: + context.close() + } +} +""" + +def testDir = new File("test-suite/src/test/groovy/io/micronaut/reproduce") +def testFile = new File(testDir, "ReproduceIssueSpec.groovy") + +try { + testDir.mkdirs() + testFile.write(testSource) + + // Command to execute the specific Groovy test using Gradle + def command = "./gradlew :test-suite:test --tests io.micronaut.reproduce.ReproduceIssueSpec" + def process = command.execute() + process.waitForProcessOutput(System.out, System.err) + def gradleExitCode = process.exitValue() + + // Map Gradle's exit code to the required script exit codes: + // If Gradle test fails (non-zero exit code), it means the bug was reproduced (AssertionError thrown). + // If Gradle test passes (zero exit code), it means the bug was fixed. + if (gradleExitCode != 0) { + System.exit(129) // Issue reproduced + } else { + System.exit(0) // Issue not reproduced (fixed) + } +} catch (Exception e) { + e.printStackTrace() + System.exit(1) // Script error (e.g., file operations, command execution issues) +} finally { + // Clean up the created test file and directory + if (testFile.exists()) { + testFile.delete() + } + if (testDir.exists()) { + testDir.deleteDir() + } +} \ No newline at end of file