You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/08/29 16:33:57 UTC
(grails-core) 01/01: Register a late ConfigurationClassPostProcessor for resources.groovy/xml
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch fix/resources-groovy-configuration-class-8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 22fa457ea922d75948e07e4aa0ba470cdc947a95
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Fri Aug 28 20:27:24 2026 -0500
Register a late ConfigurationClassPostProcessor for resources.groovy/xml
GrailsApplicationPostProcessor -- which drains resources.groovy,
resources.xml, and the application's own doWithSpring/beanRegistrar() --
is discovered as an ordinary registry bean, so Spring always invokes it
after its own ConfigurationClassPostProcessor has already completed its
one and only pass. @Configuration classes contributed there were
therefore added as plain, unexpanded bean definitions.
CoreGrailsPlugin used to unconditionally register a second processor
that happened to catch this late content, but a later change made it
stand down whenever Spring's own processor is already present -- true
in the early phase where CoreGrailsPlugin's registrar now runs (before
Spring's processor executes), but also true in the late phase where
resources.groovy is loaded (after it has already run), leaving nothing
to parse the late arrivals.
Register a second ConfigurationClassPostProcessor at the end of
GrailsApplicationPostProcessor's own postProcessBeanDefinitionRegistry
instead. Spring marks every bean definition a processor has already
parsed, so this second pass only expands what is genuinely new; it is
skipped under AOT, matching CoreGrailsPlugin's existing guard.
Fixes #16029
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
.../config/GrailsApplicationPostProcessor.groovy | 37 ++++++++
.../LateConfigurationClassPostProcessorSpec.groovy | 100 +++++++++++++++++++++
2 files changed, 137 insertions(+)
diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy b/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy
index c37840bfb5..8ed776f162 100644
--- a/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy
+++ b/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy
@@ -30,6 +30,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
import org.springframework.context.annotation.AnnotationConfigUtils
+import org.springframework.context.annotation.ConfigurationClassPostProcessor
import org.springframework.aot.AotDetector
import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.beans.factory.config.BeanDefinition
@@ -82,6 +83,13 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces
static final boolean RELOADING_ENABLED = Environment.isReloadingAgentEnabled()
+ /**
+ * Name under which a second {@link ConfigurationClassPostProcessor} is registered to parse
+ * {@code @Configuration} classes contributed after Spring's own processor has already run.
+ * See {@link #registerLateConfigurationClassPostProcessor}.
+ */
+ private static final String LATE_CONFIGURATION_CLASS_POST_PROCESSOR_BEAN_NAME = 'grailsLateConfigurationClassPostProcessor'
+
final GrailsApplication grailsApplication
final GrailsApplicationLifeCycle lifeCycle
final GrailsApplicationClass applicationClass
@@ -313,6 +321,35 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces
.register(registrar)
}
}
+
+ registerLateConfigurationClassPostProcessor(registry)
+ }
+
+ /**
+ * Registers a second {@link ConfigurationClassPostProcessor} so that {@code @Configuration}
+ * classes contributed by {@code resources.groovy}, {@code resources.xml}, or the application's
+ * own {@code doWithSpring}/{@code beanRegistrar()} -- all drained above -- get parsed.
+ *
+ * <p>This class is discovered as an ordinary registry bean (see
+ * {@link GrailsAutoConfiguration#grailsApplicationPostProcessor}), not added manually like
+ * {@link GrailsEarlyPluginRegistrationPostProcessor}, so Spring always invokes it after its own
+ * {@code ConfigurationClassPostProcessor} has already completed its one and only pass over the
+ * registry. Without a second pass, the {@code @Configuration} classes registered above are added
+ * as plain bean definitions but never expanded. Spring marks every bean definition a processor
+ * has already parsed with {@code CONFIGURATION_CLASS_ATTRIBUTE}, so this second pass skips
+ * everything the first one handled and only expands what arrived here.</p>
+ *
+ * <p>Skipped for an AOT-optimized context, matching {@link org.grails.plugins.CoreGrailsPlugin}:
+ * the configuration classes it would otherwise re-parse were already parsed at build time, and
+ * {@code loadExternalBeans} above skips {@code resources.groovy}/{@code resources.xml} entirely
+ * in that case.</p>
+ */
+ private static void registerLateConfigurationClassPostProcessor(BeanDefinitionRegistry registry) {
+ if (AotDetector.useGeneratedArtifacts()) {
+ return
+ }
+ registerInfrastructureBean(registry, LATE_CONFIGURATION_CLASS_POST_PROCESSOR_BEAN_NAME,
+ ConfigurationClassPostProcessor)
}
/**
diff --git a/grails-core/src/test/groovy/grails/boot/config/LateConfigurationClassPostProcessorSpec.groovy b/grails-core/src/test/groovy/grails/boot/config/LateConfigurationClassPostProcessorSpec.groovy
new file mode 100644
index 0000000000..45c29b773f
--- /dev/null
+++ b/grails-core/src/test/groovy/grails/boot/config/LateConfigurationClassPostProcessorSpec.groovy
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 grails.boot.config
+
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+import grails.core.GrailsApplicationLifeCycleAdapter
+import grails.util.Environment
+import grails.util.Holders
+import org.apache.grails.core.plugins.DefaultPluginDiscovery
+import org.apache.grails.core.plugins.PluginDiscovery
+import spock.lang.Specification
+
+/**
+ * Reproduces apache/grails-core#16029: an {@code @Configuration} class contributed to the registry
+ * by {@code GrailsApplicationPostProcessor.postProcessBeanDefinitionRegistry} -- standing in for one
+ * declared in an application's {@code resources.groovy}, which drains through that same method --
+ * must still be parsed, even though it arrives strictly after Spring's own
+ * {@code ConfigurationClassPostProcessor} has already completed its one and only pass.
+ *
+ * <p>{@code GrailsApplicationPostProcessor} is wired here exactly as {@code GrailsAutoConfiguration}
+ * wires it in production (see {@code GrailsAutoConfiguration#grailsApplicationPostProcessor}): as an
+ * ordinary, non-{@code PriorityOrdered} {@code @Bean}, so Spring only discovers and invokes it after
+ * expanding the primary source's own configuration -- the same round in which Spring's processor
+ * finishes running.</p>
+ */
+class LateConfigurationClassPostProcessorSpec extends Specification {
+
+ void 'a @Configuration class registered after Boot auto-configuration is still parsed'() {
+ given: 'a context wiring GrailsApplicationPostProcessor the same way GrailsAutoConfiguration does'
+ def ctx = new AnnotationConfigApplicationContext()
+ def discovery = new DefaultPluginDiscovery([] as Class<?>[])
+ discovery.loadPluginsFromClasspath = false
+ discovery.init(ctx.environment)
+ ctx.beanFactory.registerSingleton(PluginDiscovery.BEAN_NAME, discovery)
+ ctx.register(LateConfigPostProcessorWiring)
+
+ when: 'the context refreshes'
+ ctx.refresh()
+
+ then: 'the @Bean method on the late @Configuration class was expanded into a real bean'
+ ctx.getBean('lateBean') instanceof LateConfigBean
+
+ cleanup:
+ ctx.close()
+ Holders.clear()
+ Environment.setInitializing(false)
+ }
+}
+
+@Configuration
+class LateConfigPostProcessorWiring {
+
+ @Bean
+ GrailsApplicationPostProcessor grailsApplicationPostProcessor(
+ ApplicationContext applicationContext, PluginDiscovery pluginDiscovery) {
+ new GrailsApplicationPostProcessor(new LateConfigLifeCycle(), applicationContext, pluginDiscovery)
+ }
+}
+
+class LateConfigLifeCycle extends GrailsApplicationLifeCycleAdapter {
+
+ @Override
+ Closure doWithSpring() {
+ { -> delegate.lateConfig(LateConfig) }
+ }
+}
+
+/** Stands in for an {@code @Configuration} class declared in an application's resources.groovy. */
+@Configuration
+class LateConfig {
+
+ @Bean
+ LateConfigBean lateBean() {
+ new LateConfigBean()
+ }
+}
+
+class LateConfigBean {
+
+}