From 76401d04d2dfcf7260b07c499dd86a16c18ef983 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 00:42:33 -0600 Subject: [PATCH 01/20] Stop @EnableWebMvc from being automatically added to Grails Applications @EnableWebMvc is a Spring Framework annotation that is not meant to be combined with Spring Boot. Importing it (via DelegatingWebMvcConfiguration) disables Boot's WebMvcAutoConfiguration MVC setup (which is @ConditionalOnMissingBean(WebMvcConfigurationSupport)) and registers MVC infrastructure beans in the user-configuration phase, ahead of all auto-configuration. A concrete symptom is the localeResolver bean: WebMvcConfigurationSupport registers an AcceptHeaderLocaleResolver in the user phase, so grails-i18n's @AutoConfigureBefore(WebMvcAutoConfiguration) SessionLocaleResolver can only ever override it (triggering a bean-definition-override) rather than supersede it cleanly. Remove the auto-injected @EnableWebMvc so Boot's WebMvcAutoConfiguration drives MVC setup. --- .../grails/compiler/injection/ApplicationClassInjector.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/grails-core/src/main/groovy/org/grails/compiler/injection/ApplicationClassInjector.groovy b/grails-core/src/main/groovy/org/grails/compiler/injection/ApplicationClassInjector.groovy index 763bdbe7611..0d748dd9a5b 100644 --- a/grails-core/src/main/groovy/org/grails/compiler/injection/ApplicationClassInjector.groovy +++ b/grails-core/src/main/groovy/org/grails/compiler/injection/ApplicationClassInjector.groovy @@ -143,7 +143,6 @@ class ApplicationClassInjector implements GrailsArtefactClassInjector { addAnnotation('org.springframework.boot.SpringBootConfiguration', classNode)?.with { GrailsASTUtils.addExpressionToAnnotationMember(it, 'proxyBeanMethods', constX(false)) } - addAnnotation('org.springframework.web.servlet.config.annotation.EnableWebMvc', classNode, 'jakarta.servlet.ServletContext') addAnnotation('org.springframework.boot.autoconfigure.EnableAutoConfiguration', classNode)?.with { annotation -> EXCLUDED_AUTO_CONFIGURE_CLASSES.each { GrailsASTUtils.addExpressionToAnnotationMember( From b05894e5e76650aa575696dab359d8df83c8c2ab Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 00:59:38 -0600 Subject: [PATCH 02/20] Keep Grails' GrailsWebRequest bound once WebMvcAutoConfiguration is active With @EnableWebMvc no longer auto-injected, Boot's WebMvcAutoConfiguration becomes active and registers an OrderedRequestContextFilter (order -105). That runs between GrailsWebRequestFilter (-150) and the Spring Security chain (-100) and rebinds a plain ServletRequestAttributes, so security-chain code that expects a GrailsWebRequest fails with a ClassCastException (HTTP 500 on every request). Boot's filter is @ConditionalOnMissingBean({RequestContextListener, RequestContextFilter}), so make GrailsWebRequestFilter extend RequestContextFilter (it already overrides shouldNotFilterAsyncDispatch/ErrorDispatch identically and binds a richer request context) and expose it as a bean that the FilterRegistrationBean wraps. Boot then backs off in favour of Grails' request-context binding. --- .../ControllersAutoConfiguration.java | 17 ++++++++++++++--- .../web/servlet/mvc/GrailsWebRequestFilter.java | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 9d560ae0b85..cd079b7def2 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -108,13 +108,24 @@ public FilterRegistrationBean hiddenHttpMethodFilter() { return registrationBean; } + // Exposed as a RequestContextFilter bean (GrailsWebRequestFilter extends it) so that Boot's + // WebMvcAutoConfiguration RequestContextFilter — @ConditionalOnMissingBean(RequestContextFilter) — + // backs off in favour of Grails' GrailsWebRequest binding. Without @EnableWebMvc, Boot's + // WebMvcAutoConfiguration is active and would otherwise register an OrderedRequestContextFilter + // (order -105) that rebinds a plain ServletRequestAttributes between GrailsWebRequestFilter (-150) + // and the Spring Security chain (-100), causing a GrailsWebRequest ClassCastException downstream. @Bean @ConditionalOnMissingBean(GrailsWebRequestFilter.class) - public FilterRegistrationBean grailsWebRequestFilter(ApplicationContext applicationContext) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + public GrailsWebRequestFilter grailsWebRequest(ApplicationContext applicationContext) { GrailsWebRequestFilter grailsWebRequestFilter = new GrailsWebRequestFilter(); grailsWebRequestFilter.setApplicationContext(applicationContext); - registrationBean.setFilter(grailsWebRequestFilter); + return grailsWebRequestFilter; + } + + @Bean + public FilterRegistrationBean grailsWebRequestFilter(GrailsWebRequestFilter grailsWebRequest) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(grailsWebRequest); registrationBean.setDispatcherTypes(EnumSet.of( DispatcherType.FORWARD, DispatcherType.INCLUDE, diff --git a/grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsWebRequestFilter.java b/grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsWebRequestFilter.java index e938f1a28ce..820ee1a00bc 100644 --- a/grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsWebRequestFilter.java +++ b/grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsWebRequestFilter.java @@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.filter.RequestContextFilter; import grails.web.mvc.FlashScope; import org.grails.web.util.WebUtils; @@ -41,7 +41,7 @@ * @author Graeme Rocher * @since 0.4 */ -public class GrailsWebRequestFilter extends OncePerRequestFilter implements ApplicationContextAware { +public class GrailsWebRequestFilter extends RequestContextFilter implements ApplicationContextAware { Collection paramListenerBeans; /* (non-Javadoc) From 0a49d580a04ca8e717184d00d8d8b7911ba0174a Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 09:43:11 -0600 Subject: [PATCH 03/20] Remove Boot's defaultViewResolver for all Grails servlet web apps With @EnableWebMvc no longer auto-injected, Spring Boot's WebMvcAutoConfiguration is active and contributes a defaultViewResolver (InternalResourceViewResolver). For a Grails app that does not use JSP/InternalResource views (e.g. a REST/JSON-views app) this catch-all resolves any unmatched view name to a servlet forward, yielding "Circular view path [index]: would dispatch back to the current handler URL" (HTTP 500). grails-gsp already strips this bean for GSP apps via its own registrar, but a JSON-only app never loads grails-gsp. Add a core auto-configuration (ordered after WebMvcAutoConfiguration) that removes defaultViewResolver for every Grails servlet web application, so Grails' own view resolution is used. Disable with grails.web.removeDefaultViewResolverBean=false. Fixes the graphql, issue-views-182 and views-functional-tests example apps. --- .../GrailsViewResolverAutoConfiguration.java | 69 +++++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + 2 files changed, 70 insertions(+) create mode 100644 grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java new file mode 100644 index 00000000000..5c6da543356 --- /dev/null +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * 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 org.grails.plugins.web.controllers; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Without the auto-injected {@code @EnableWebMvc}, Spring Boot's + * {@link WebMvcAutoConfiguration} is active and contributes a {@code defaultViewResolver} + * ({@code InternalResourceViewResolver}). For a Grails application that does not use + * JSP/InternalResource views (e.g. a REST/JSON-views app) this catch-all resolver resolves + * any unmatched view name to a servlet forward, producing + * {@code Circular view path [index]: would dispatch back to the current handler URL} errors. + * + *

{@code grails-gsp} already removes this bean for GSP applications via its own registrar, + * but a JSON-only application never loads {@code grails-gsp}. This core auto-configuration + * removes the {@code defaultViewResolver} for every Grails servlet web application so Grails' + * own view resolution (GSP, JSON/Markup views) is used instead. Ordered after + * {@link WebMvcAutoConfiguration} so the bean exists by the time the registrar runs. + * + *

Disable with {@code grails.web.removeDefaultViewResolverBean=false}. + */ +@AutoConfiguration(after = WebMvcAutoConfiguration.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@Import(GrailsViewResolverAutoConfiguration.RemoveDefaultViewResolverRegistrar.class) +public class GrailsViewResolverAutoConfiguration { + + static class RemoveDefaultViewResolverRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { + + private boolean removeDefaultViewResolverBean = true; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + if (removeDefaultViewResolverBean && registry.containsBeanDefinition("defaultViewResolver")) { + registry.removeBeanDefinition("defaultViewResolver"); + } + } + + @Override + public void setEnvironment(Environment environment) { + this.removeDefaultViewResolverBean = environment.getProperty( + "grails.web.removeDefaultViewResolverBean", Boolean.class, true); + } + } +} diff --git a/grails-controllers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/grails-controllers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 070ecd4bead..a0c2247bfdd 100644 --- a/grails-controllers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/grails-controllers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ org.grails.plugins.web.controllers.ControllersAutoConfiguration +org.grails.plugins.web.controllers.GrailsViewResolverAutoConfiguration From 04622e3a9cfbd920089329aa6436c6c90f050ca9 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 10:44:25 -0600 Subject: [PATCH 04/20] Consolidate defaultViewResolver removal into one place GrailsViewResolverAutoConfiguration (grails-controllers) already removes Boot's defaultViewResolver for every Grails servlet web app, so grails-gsp's registrar no longer needs to do it too. Drop the duplicate defaultViewResolver removal (and its spring.gsp.removeDefaultViewResolverBean flag) from grails-gsp, leaving only the GSP-specific behaviour: replacing the viewResolver bean with an alias to gspViewResolver. Rename the registrar to ReplaceViewResolverRegistrar to reflect its remaining role. No behaviour change: GSP apps still have defaultViewResolver removed (now via the core auto-configuration) and gspViewResolver aliased as viewResolver. --- .../GrailsViewResolverAutoConfiguration.java | 7 +++--- .../grails/gsp/boot/GspAutoConfiguration.java | 25 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java index 5c6da543356..49b127f5d26 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java @@ -36,10 +36,9 @@ * any unmatched view name to a servlet forward, producing * {@code Circular view path [index]: would dispatch back to the current handler URL} errors. * - *

{@code grails-gsp} already removes this bean for GSP applications via its own registrar, - * but a JSON-only application never loads {@code grails-gsp}. This core auto-configuration - * removes the {@code defaultViewResolver} for every Grails servlet web application so Grails' - * own view resolution (GSP, JSON/Markup views) is used instead. Ordered after + * This is the single place the {@code defaultViewResolver} is removed for every Grails servlet + * web application (GSP, JSON/Markup or plain), so Grails' own view resolution is used instead. + * grails-gsp only loads for GSP applications, so the removal cannot live there. Ordered after * {@link WebMvcAutoConfiguration} so the bean exists by the time the registrar runs. * *

Disable with {@code grails.web.removeDefaultViewResolverBean=false}. diff --git a/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java b/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java index 79ec59002fe..b5614a45b4e 100644 --- a/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java +++ b/grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java @@ -90,7 +90,7 @@ protected static abstract class AbstractGspConfig { } @Configuration - @Import({TagLibraryLookupRegistrar.class, RemoveDefaultViewResolverRegistrar.class}) + @Import({TagLibraryLookupRegistrar.class, ReplaceViewResolverRegistrar.class}) protected static class GspTemplateEngineAutoConfiguration extends AbstractGspConfig { private static final String LOCAL_DIRECTORY_TEMPLATE_ROOT = "./src/main/resources/templates"; private static final String CLASSPATH_TEMPLATE_ROOT = "classpath:/templates"; @@ -266,26 +266,22 @@ protected GenericBeanDefinition createBeanDefinition(Class beanClass) { } /** - * {@link WebMvcAutoConfiguration} adds defaultViewResolver and viewResolver beans. + * Makes the GSP view resolver the primary {@code viewResolver} for GSP applications by + * replacing the {@code viewResolver} bean ({@link WebMvcAutoConfiguration}'s + * {@code ContentNegotiatingViewResolver}) with an alias to {@code gspViewResolver}. * - * This ImportBeanDefinitionRegistrar removes the defaultViewResolver and replaces - * the viewResolver bean with GSP view resolver by default. - * - * The behavior of this class can be controlled with spring.gsp.removeDefaultViewResolver and - * spring.gsp.replaceViewResolverBean configuration properties. + *

Removal of {@link WebMvcAutoConfiguration}'s {@code defaultViewResolver} + * ({@code InternalResourceViewResolver}) is handled centrally for every Grails servlet web + * application by {@code GrailsViewResolverAutoConfiguration} (grails-controllers), so it is + * not repeated here. * + *

Controlled by the {@code spring.gsp.replaceViewResolverBean} configuration property. */ - protected static class RemoveDefaultViewResolverRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { - boolean removeDefaultViewResolverBean; + protected static class ReplaceViewResolverRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { boolean replaceViewResolverBean; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - if (removeDefaultViewResolverBean) { - if (registry.containsBeanDefinition("defaultViewResolver")) { - registry.removeBeanDefinition("defaultViewResolver"); - } - } if (replaceViewResolverBean) { if (registry.containsBeanDefinition("viewResolver")) { registry.removeBeanDefinition("viewResolver"); @@ -296,7 +292,6 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B @Override public void setEnvironment(Environment environment) { - removeDefaultViewResolverBean = environment.getProperty("spring.gsp.removeDefaultViewResolverBean", Boolean.class, true); replaceViewResolverBean = environment.getProperty("spring.gsp.replaceViewResolverBean", Boolean.class, true); } } From fd012ce7bfca7b739fe712d435a154aed3d97a30 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 13:25:56 -0600 Subject: [PATCH 05/20] Add doWithSpringBeforeAutoConfiguration plugin phase (prototype) Introduces a modern, Spring Boot-aligned plugin bean-registration phase that runs BEFORE auto-configuration, so Boot beans guarded by @ConditionalOnMissingBean defer to the plugin's bean instead of the plugin having to override or remove the Boot bean afterwards (the legacy behaviour of doWithSpring, which runs after auto-config via GrailsApplicationPostProcessor). - GrailsApplicationLifeCycle.doWithSpringBeforeAutoConfiguration() (default no-op) + Plugin override - GrailsPlugin/GrailsPluginManager drain plumbing for the new phase - GrailsBeforeAutoConfigurationPostProcessor: a BeanDefinitionRegistryPostProcessor added via addBeanFactoryPostProcessor (so it runs ahead of ConfigurationClassPostProcessor, which expands the @AutoConfiguration imports), draining the new closures into the registry - GrailsBeforeAutoConfigurationInitializer wires it in (spring.factories ApplicationContextInitializer) The legacy doWithSpring phase is unchanged; this is additive and opt-in. Compiles clean; runtime defer-behaviour validation is the next step. --- ...ilsBeforeAutoConfigurationInitializer.java | 40 +++++++ ...sBeforeAutoConfigurationPostProcessor.java | 100 ++++++++++++++++++ .../core/GrailsApplicationLifeCycle.groovy | 17 +++ .../groovy/grails/plugins/GrailsPlugin.java | 11 ++ .../grails/plugins/GrailsPluginManager.java | 12 +++ .../main/groovy/grails/plugins/Plugin.groovy | 11 ++ .../plugins/AbstractGrailsPluginManager.java | 17 +++ .../grails/plugins/DefaultGrailsPlugin.java | 23 ++++ .../main/resources/META-INF/spring.factories | 3 +- 9 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationInitializer.java create mode 100644 grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationInitializer.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationInitializer.java new file mode 100644 index 00000000000..fbfb326bdc7 --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationInitializer.java @@ -0,0 +1,40 @@ +/* + * 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.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Registers the {@link GrailsBeforeAutoConfigurationPostProcessor} on the context via + * {@code addBeanFactoryPostProcessor}, which guarantees it runs ahead of Spring Boot's + * registry-discovered post-processors (including {@code ConfigurationClassPostProcessor}, + * which expands auto-configuration). That ordering is what lets plugin beans contributed in the + * {@code doWithSpringBeforeAutoConfiguration} phase be present before Boot's + * {@code @ConditionalOnMissingBean} guards are evaluated. + * + * @since 8.0 + */ +public class GrailsBeforeAutoConfigurationInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + applicationContext.addBeanFactoryPostProcessor(new GrailsBeforeAutoConfigurationPostProcessor(applicationContext)); + } +} diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java new file mode 100644 index 00000000000..1777ed6bdd4 --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -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 grails.core.DefaultGrailsApplication; +import grails.core.GrailsApplication; +import grails.plugins.DefaultGrailsPluginManager; +import grails.plugins.GrailsPluginManager; +import org.apache.grails.core.plugins.PluginDiscovery; +import org.grails.config.PropertySourcesConfig; +import org.grails.spring.DefaultRuntimeSpringConfiguration; +import org.grails.spring.RuntimeSpringConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +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.ConfigurableApplicationContext; +import org.springframework.core.PriorityOrdered; + +/** + * Registers plugin beans contributed via + * {@link grails.core.GrailsApplicationLifeCycle#doWithSpringBeforeAutoConfiguration()} before + * Spring Boot's auto-configuration is processed. + * + *

It is added to the context programmatically (see {@code GrailsBeforeAutoConfigurationInitializer}), + * so its {@code postProcessBeanDefinitionRegistry} runs ahead of Boot's {@code ConfigurationClassPostProcessor} + * (which expands the {@code @AutoConfiguration} imports). Because the plugin beans are already in the + * registry, Boot auto-config beans guarded by {@code @ConditionalOnMissingBean} back off in favour of the + * plugin's bean — no removal or override needed afterwards. + * + *

This deliberately does the minimum needed to drain the modern phase: it builds a lightweight + * {@link GrailsApplication} backed by the already-populated {@code Environment} config and loads the plugin + * classes (cheap). The full plugin lifecycle (artefact discovery, dynamic methods, legacy {@code doWithSpring}) + * still runs later in {@code GrailsApplicationPostProcessor}; {@code loadPlugins()} is idempotent. + * + * @since 8.0 + */ +public class GrailsBeforeAutoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered { + + private static final Logger LOG = LoggerFactory.getLogger(GrailsBeforeAutoConfigurationPostProcessor.class); + + private final ConfigurableApplicationContext applicationContext; + + public GrailsBeforeAutoConfigurationPostProcessor(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + if (!applicationContext.containsBean(PluginDiscovery.BEAN_NAME)) { + // No plugin discovery promoted to the context (e.g. unit-test slice) — nothing to do. + return; + } + PluginDiscovery pluginDiscovery = applicationContext.getBean(PluginDiscovery.BEAN_NAME, PluginDiscovery.class); + + DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication(); + grailsApplication.setConfig(new PropertySourcesConfig(applicationContext.getEnvironment().getPropertySources())); + grailsApplication.setApplicationContext(applicationContext); + + DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); + pluginManager.setApplicationContext(applicationContext); + pluginManager.loadPlugins(); + + RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); + pluginManager.doRuntimeConfigurationBeforeAutoConfiguration(springConfig); + springConfig.registerBeansWithRegistry(registry); + + if (LOG.isDebugEnabled()) { + LOG.debug("Registered plugin beans from the before-auto-configuration phase"); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // no-op + } + + @Override + public int getOrder() { + return PriorityOrdered.HIGHEST_PRECEDENCE; + } +} diff --git a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy index 7f3ca551ec3..dc64b72d867 100644 --- a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy +++ b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy @@ -35,6 +35,23 @@ interface GrailsApplicationLifeCycle { */ Closure doWithSpring() + /** + * Sub classes should override to register Spring beans before Spring Boot + * auto-configuration runs. Beans registered here are placed in the bean registry ahead of + * auto-configuration, so Boot beans guarded by {@code @ConditionalOnMissingBean} back off in + * favour of the plugin's bean — instead of the plugin having to override or remove the Boot + * bean afterwards (the legacy behaviour of {@link #doWithSpring()}). + * + *

This is the modern, Boot-aligned registration phase. Use it for beans that need to win + * over a Spring Boot auto-configuration default. Beans that iterate Grails artefacts + * (controllers, services, taglibs) must stay in {@link #doWithSpring()}, which runs after + * artefact discovery. + * + * @return A closure that defines beans to be registered before auto-configuration + * @since 8.0 + */ + default Closure doWithSpringBeforeAutoConfiguration() { null } + /** * Invoked once the {@link org.springframework.context.ApplicationContext} has been refreshed in a phase where plugins can add dynamic methods. Subclasses should override */ diff --git a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java index 3dcac050977..d5cec47ec3d 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java @@ -193,6 +193,17 @@ public interface GrailsPlugin extends ApplicationContextAware, Comparable, Grail */ void doWithRuntimeConfiguration(RuntimeSpringConfiguration springConfig); + /** + * Executes the plugin code defined in the {@code doWithSpringBeforeAutoConfiguration} closure + * (registered before Spring Boot auto-configuration). Default no-op for legacy plugins. + * + * @param springConfig The RuntimeSpringConfiguration instance + * @since 8.0 + */ + default void doWithRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { + // no-op by default + } + /** * Makes the plugin excluded for a particular Environment * @param env The Environment diff --git a/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java b/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java index 0ed6ea174e8..f988d878e20 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java @@ -107,6 +107,18 @@ public interface GrailsPluginManager extends ApplicationContextAware { */ void doRuntimeConfiguration(RuntimeSpringConfiguration springConfig); + /** + * Executes the pre-auto-configuration phase of the loaded plugins + * ({@code doWithSpringBeforeAutoConfiguration}), so the resulting beans are registered ahead of + * Spring Boot auto-configuration. Default no-op. + * + * @param springConfig The {@link RuntimeSpringConfiguration} instance + * @since 8.0 + */ + default void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { + // no-op by default + } + /** * Performs post-initialization configuration for each plugin, * passing the built application context (e.g., {@code doWithApplicationContext}). diff --git a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy index b3ca44ddca1..54d6c73f26c 100644 --- a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy +++ b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy @@ -109,6 +109,17 @@ abstract class Plugin implements GrailsApplicationLifeCycle, GrailsApplicationAw @Override Closure doWithSpring() { null } + /** + * Sub classes should override to register beans before Spring Boot auto-configuration + * runs, so Boot beans guarded by {@code @ConditionalOnMissingBean} defer to the plugin's bean. + * See {@link grails.core.GrailsApplicationLifeCycle#doWithSpringBeforeAutoConfiguration()}. + * + * @return A closure that defines beans to be registered before auto-configuration + * @since 8.0 + */ + @Override + Closure doWithSpringBeforeAutoConfiguration() { null } + /** * Invoked in a phase where plugins can add dynamic methods. Subclasses should override */ diff --git a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java index 54ba5efa3fa..572983e29ea 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -176,6 +176,23 @@ public Object convert(NavigableMap.NullSafeNavigator source) { } } + /** + * Drains each plugin's {@code doWithSpringBeforeAutoConfiguration} closure. Invoked from the + * pre-auto-configuration phase so the resulting beans are registered ahead of Spring Boot's + * auto-configuration, letting Boot's {@code @ConditionalOnMissingBean} guards defer to them. + */ + @Override + public void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { + checkInitialised(); + for (PluginInfo pluginInfo : pluginDiscovery.getPluginsInTopologicalOrder()) { + GrailsPlugin grailsPlugin = plugins.get(pluginInfo.getName()); + // Only plugins that opt in (override the default no-op) register anything here. + if (grailsPlugin.supportsCurrentScopeAndEnvironment()) { + grailsPlugin.doWithRuntimeConfigurationBeforeAutoConfiguration(springConfig); + } + } + } + /** * Base implementation that will perform runtime configuration for the specified plugin name. */ diff --git a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java index a4fe202df21..7ef7918fca3 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java +++ b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java @@ -415,6 +415,29 @@ public void doWithRuntimeConfiguration(RuntimeSpringConfiguration springConfig) } + @Override + public void doWithRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { + // Only Plugin subclasses expose the modern before-auto-configuration phase. + if (!(plugin instanceof Plugin)) { + return; + } + Closure c = ((Plugin) plugin).doWithSpringBeforeAutoConfiguration(); + if (c == null) { + return; + } + Binding b = new Binding(); + b.setVariable("application", grailsApplication); + b.setVariable(GrailsApplication.APPLICATION_ID, grailsApplication); + b.setVariable("manager", getManager()); + b.setVariable("plugin", this); + b.setVariable("parentCtx", getParentCtx()); + b.setVariable("resolver", getResolver()); + BeanBuilder bb = new BeanBuilder(getParentCtx(), springConfig, grailsApplication.getClassLoader()); + bb.setBinding(b); + c.setDelegate(bb); + bb.invokeMethod("beans", new Object[]{c}); + } + @Override public String getName() { return pluginGrailsClass.getLogicalPropertyName(); diff --git a/grails-core/src/main/resources/META-INF/spring.factories b/grails-core/src/main/resources/META-INF/spring.factories index 824ecc7a0ea..470544d4bac 100644 --- a/grails-core/src/main/resources/META-INF/spring.factories +++ b/grails-core/src/main/resources/META-INF/spring.factories @@ -22,4 +22,5 @@ org.springframework.boot.env.PropertySourceLoader=\ org.grails.config.yaml.YamlPropertySourceLoader org.springframework.boot.EnvironmentPostProcessor=grails.boot.config.GrailsEnvironmentPostProcessor org.springframework.boot.SpringApplicationRunListener=grails.config.external.ExternalConfigRunListener -org.springframework.boot.bootstrap.BootstrapRegistryInitializer=org.apache.grails.core.GrailsBootstrapRegistryInitializer \ No newline at end of file +org.springframework.boot.bootstrap.BootstrapRegistryInitializer=org.apache.grails.core.GrailsBootstrapRegistryInitializer +org.springframework.context.ApplicationContextInitializer=grails.boot.config.GrailsBeforeAutoConfigurationInitializer \ No newline at end of file From 68cdf6227e7299d47e113082afb463de40981b0b Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 13:28:54 -0600 Subject: [PATCH 06/20] Test: prove early BDRPP makes @ConditionalOnMissingBean defer Self-contained proof of the ordering linchpin behind the doWithSpringBeforeAutoConfiguration phase: a BeanDefinitionRegistryPostProcessor added via addBeanFactoryPostProcessor registers beans before ConfigurationClassPostProcessor evaluates @ConditionalOnMissingBean, so a Boot auto-config bean defers to the plugin's bean. Includes a control showing the conditional bean is created when nothing registers it early. --- ...BeforeAutoConfigurationOrderingSpec.groovy | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 grails-core/src/test/groovy/grails/boot/config/BeforeAutoConfigurationOrderingSpec.groovy diff --git a/grails-core/src/test/groovy/grails/boot/config/BeforeAutoConfigurationOrderingSpec.groovy b/grails-core/src/test/groovy/grails/boot/config/BeforeAutoConfigurationOrderingSpec.groovy new file mode 100644 index 00000000000..aeb689b2db4 --- /dev/null +++ b/grails-core/src/test/groovy/grails/boot/config/BeforeAutoConfigurationOrderingSpec.groovy @@ -0,0 +1,96 @@ +/* + * 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.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import spock.lang.Specification + +/** + * Proves the architectural linchpin of {@link GrailsBeforeAutoConfigurationPostProcessor}: a + * {@code BeanDefinitionRegistryPostProcessor} added to the context via + * {@code addBeanFactoryPostProcessor} registers its beans before + * {@code ConfigurationClassPostProcessor} evaluates {@code @ConditionalOnMissingBean}. That is why + * a plugin registering a bean in {@code doWithSpringBeforeAutoConfiguration} makes the matching Boot + * auto-config bean back off — instead of the plugin having to override/remove it afterwards. + */ +class BeforeAutoConfigurationOrderingSpec extends Specification { + + void "an early BDRPP makes a @ConditionalOnMissingBean auto-config bean defer"() { + given: "a context whose configuration would, by itself, register a conditional 'myResolver'" + def ctx = new AnnotationConfigApplicationContext() + ctx.register(AutoConfigLikeConfig) + + and: "a registrar added the same way GrailsBeforeAutoConfigurationPostProcessor is added — ahead of ConfigurationClassPostProcessor" + ctx.addBeanFactoryPostProcessor(new PluginPhaseRegistrar()) + + when: "the context refreshes" + ctx.refresh() + + then: "the early (plugin) bean is the one named 'myResolver'..." + ctx.getBean('myResolver').class == PluginResolver + + and: "...and Boot's conditional default was never created" + ctx.getBeansOfType(BootDefaultResolver).isEmpty() + + cleanup: + ctx.close() + } + + void "without the early registrar, the conditional auto-config bean IS created (control)"() { + given: + def ctx = new AnnotationConfigApplicationContext() + ctx.register(AutoConfigLikeConfig) + + when: + ctx.refresh() + + then: "the conditional default wins when nothing registered the bean first" + ctx.getBean('myResolver').class == BootDefaultResolver + + cleanup: + ctx.close() + } + + /** Stands in for a Spring Boot auto-configuration: a name-guarded conditional bean. */ + @Configuration + static class AutoConfigLikeConfig { + @Bean + @ConditionalOnMissingBean(name = 'myResolver') + BootDefaultResolver myResolver() { new BootDefaultResolver() } + } + + /** Stands in for what a plugin's doWithSpringBeforeAutoConfiguration closure registers. */ + static class PluginPhaseRegistrar implements BeanDefinitionRegistryPostProcessor { + @Override + void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + registry.registerBeanDefinition('myResolver', new RootBeanDefinition(PluginResolver)) + } + @Override + void postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory beanFactory) {} + } + + static class BootDefaultResolver {} + static class PluginResolver {} +} From 84a451dfa84a1dcfa86a201b1cd62f5d87635dbe Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 18:19:22 -0500 Subject: [PATCH 07/20] Warn (narrowly) when doWithSpring overrides a deferrable conditional bean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds DeferrableOverrideWarner: when a late doWithSpring bean overrides a Spring Boot auto-configuration bean that is name-guarded @ConditionalOnMissingBean (one that would have deferred), emit a one-line nudge toward doWithSpringBeforeAutoConfiguration. Wired into GrailsApplicationPostProcessor before the late beans flush; gated on grails.plugins.warnOnDeferrableOverride (default true). Deliberately narrow — silent on the permanent, legitimate late-registration uses (decoration, aggregation, artefact-driven beans, intentional overrides of unconditional beans) and on type-guarded conditionals (override-by-name doesn't prove the type condition would have matched). So it flags only the one anti-pattern the modern phase removes. Test asserts: name-guarded override flagged; type-guarded and unconditional left alone. --- .../boot/config/DeferrableOverrideWarner.java | 99 +++++++++++++++++++ .../GrailsApplicationPostProcessor.groovy | 7 ++ .../DeferrableOverrideWarnerSpec.groovy | 72 ++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java create mode 100644 grails-core/src/test/groovy/grails/boot/config/DeferrableOverrideWarnerSpec.groovy diff --git a/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java b/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java new file mode 100644 index 00000000000..6eaf14149c4 --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java @@ -0,0 +1,99 @@ +/* + * 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 java.util.Arrays; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.type.MethodMetadata; + +import org.grails.spring.RuntimeSpringConfiguration; + +/** + * Emits a one-line warning when a bean registered in the late {@code doWithSpring} phase overrides a + * Spring Boot auto-configuration bean that is {@code @ConditionalOnMissingBean} on that very name — + * i.e. a bean that would have deferred had the plugin/app registered it in the modern + * {@link grails.core.GrailsApplicationLifeCycle#doWithSpringBeforeAutoConfiguration()} phase instead, + * avoiding both the override and the wasted bean creation. + * + *

The check is deliberately narrow. {@code doWithSpring} (after auto-configuration) has permanent, + * legitimate uses that this MUST stay silent on: + *

    + *
  • decorating/wrapping a bean that auto-config created (needs it to exist first);
  • + *
  • aggregating or inspecting the fully-populated registry;
  • + *
  • artefact-driven beans (one per controller/service/taglib) that need a loaded GrailsApplication;
  • + *
  • intentionally overriding an unconditional Boot bean (the only way to replace it).
  • + *
+ * So it fires only on the one anti-pattern the modern phase removes: overriding a name-guarded + * {@code @ConditionalOnMissingBean} bean. Type-guarded conditionals are left silent — overriding by name + * does not reliably prove the type condition would have matched, and a false nudge is worse than none. + */ +final class DeferrableOverrideWarner { + + private static final Logger LOG = LoggerFactory.getLogger(DeferrableOverrideWarner.class); + + private static final String CONDITIONAL_ON_MISSING_BEAN = + "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"; + + private DeferrableOverrideWarner() { + } + + static void warnOnDeferrableOverrides(RuntimeSpringConfiguration springConfig, BeanDefinitionRegistry registry) { + for (String name : springConfig.getBeanNames()) { + if (!registry.containsBeanDefinition(name)) { + continue; // a fresh bean, not an override + } + if (overridesDeferrableConditional(registry.getBeanDefinition(name), name)) { + LOG.warn("Bean '{}' registered in doWithSpring overrides a Spring Boot auto-configuration " + + "bean of the same name that is @ConditionalOnMissingBean(name=\"{}\") and would have " + + "deferred. Register it in doWithSpringBeforeAutoConfiguration() instead: the " + + "auto-configuration bean then backs off cleanly, with no override and no wasted bean " + + "creation. (doWithSpring stays correct for decoration, aggregation, artefact-driven " + + "beans, and intentional overrides of unconditional beans.)", name, name); + } + } + } + + /** + * True only when {@code existing} is an auto-configuration {@code @Bean} method annotated + * {@code @ConditionalOnMissingBean} whose {@code name} attribute names {@code beanName}. + */ + static boolean overridesDeferrableConditional(BeanDefinition existing, String beanName) { + if (!(existing instanceof AnnotatedBeanDefinition)) { + return false; + } + MethodMetadata factoryMethod = ((AnnotatedBeanDefinition) existing).getFactoryMethodMetadata(); + if (factoryMethod == null || !factoryMethod.isAnnotated(CONDITIONAL_ON_MISSING_BEAN)) { + return false; + } + Map attributes = factoryMethod.getAnnotationAttributes(CONDITIONAL_ON_MISSING_BEAN); + if (attributes == null) { + return false; + } + Object names = attributes.get("name"); + // Only the name-guarded case is unambiguous: the conditional explicitly guards this bean name, + // so registering it before auto-config would certainly make the conditional bean defer. + return names instanceof String[] && Arrays.asList((String[]) names).contains(beanName); + } +} 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 2734ce81fcb..c3db47f6be6 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy @@ -232,6 +232,13 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces } } + // Before flushing the late (doWithSpring) beans over the registry, nudge the narrow case + // where one overrides a name-guarded @ConditionalOnMissingBean auto-config bean that would + // have deferred had it been registered in doWithSpringBeforeAutoConfiguration instead. + if (application.config.getProperty('grails.plugins.warnOnDeferrableOverride', Boolean, true)) { + DeferrableOverrideWarner.warnOnDeferrableOverrides(springConfig, registry) + } + springConfig.registerBeansWithRegistry(registry) } diff --git a/grails-core/src/test/groovy/grails/boot/config/DeferrableOverrideWarnerSpec.groovy b/grails-core/src/test/groovy/grails/boot/config/DeferrableOverrideWarnerSpec.groovy new file mode 100644 index 00000000000..da027487ab3 --- /dev/null +++ b/grails-core/src/test/groovy/grails/boot/config/DeferrableOverrideWarnerSpec.groovy @@ -0,0 +1,72 @@ +/* + * 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.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import spock.lang.Specification + +/** + * Verifies the deprecation warning is precise: it fires only when a late doWithSpring bean would + * override a name-guarded {@code @ConditionalOnMissingBean} auto-config bean (one that would have + * deferred), and stays silent on the legitimate late-registration uses. + */ +class DeferrableOverrideWarnerSpec extends Specification { + + AnnotationConfigApplicationContext ctx + + def cleanup() { ctx?.close() } + + void "only a name-guarded @ConditionalOnMissingBean override is flagged"() { + given: "an auto-config-like @Configuration whose bean defs carry the real condition metadata" + ctx = new AnnotationConfigApplicationContext() + ctx.register(SampleAutoConfig) + ctx.refresh() + def registry = ctx.defaultListableBeanFactory + + expect: "name-guarded conditional -> would have deferred -> FLAGGED" + DeferrableOverrideWarner.overridesDeferrableConditional(registry.getBeanDefinition('nameGuarded'), 'nameGuarded') + + and: "type-guarded conditional (no name) -> conservatively NOT flagged" + !DeferrableOverrideWarner.overridesDeferrableConditional(registry.getBeanDefinition('typeGuarded'), 'typeGuarded') + + and: "unconditional bean -> intentional-override territory -> NOT flagged" + !DeferrableOverrideWarner.overridesDeferrableConditional(registry.getBeanDefinition('unconditional'), 'unconditional') + } + + @Configuration + static class SampleAutoConfig { + @Bean + @ConditionalOnMissingBean(name = 'nameGuarded') + MarkerA nameGuarded() { new MarkerA() } + + @Bean + @ConditionalOnMissingBean(MarkerB) + MarkerB typeGuarded() { new MarkerB() } + + @Bean + MarkerC unconditional() { new MarkerC() } + } + + static class MarkerA {} + static class MarkerB {} + static class MarkerC {} +} From 7044d95234a28ba61f4d91c7183bb6b973bf8538 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 19:08:09 -0500 Subject: [PATCH 08/20] Formalize the before-auto-configuration phase contract (no sharing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigated sharing one plugin manager + GrailsApplication across the early and late phases to avoid the second (lightweight) plugin instantiation. Rejected it: the late doWithSpring closures bind `application` from DefaultGrailsPlugin's own grailsApplication field, which GrailsPluginManager.setApplication() does not update (it sets AbstractGrailsPlugin.application) — so rebinding the early-loaded plugins to the full GrailsApplication leaves the closures pointing at the un-initialised minimal app (No such property: tagLibClasses). Plugins hold multiple GrailsApplication references; rebinding mid-lifecycle is fragile. The robust design is the prototype's: the early phase does an isolated, side-effect-free lightweight plugin load, fully decoupled from the main lifecycle (no Holders/global-state pollution). The second plugin instantiation is cheap and benign. Documents the contract on doWithSpringBeforeAutoConfiguration: define bean definitions only, no side effects. --- .../grails/core/GrailsApplicationLifeCycle.groovy | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy index dc64b72d867..11b0c8d86fb 100644 --- a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy +++ b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy @@ -47,6 +47,16 @@ interface GrailsApplicationLifeCycle { * (controllers, services, taglibs) must stay in {@link #doWithSpring()}, which runs after * artefact discovery. * + *

Contract: the returned closure must only define bean definitions + * and must be free of side effects. This phase runs before the main Grails lifecycle, in an + * isolated lightweight pass that instantiates plugins purely to collect their bean definitions; + * those plugin instances are then discarded and the real lifecycle re-instantiates them. A + * closure with side effects (mutating shared/static state, doing I/O, registering listeners) + * would therefore run twice. Read configuration and reference other beans freely — just do not + * do anything beyond declaring beans. Reading {@code grailsApplication.config} is safe; + * iterating {@code grailsApplication} artefacts is not (they are not discovered yet — use + * {@link #doWithSpring()} for those). + * * @return A closure that defines beans to be registered before auto-configuration * @since 8.0 */ From a727691ce561169fd08d958780d7c089f070e21c Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 19:13:22 -0500 Subject: [PATCH 09/20] Harden the early drain: profile/scope filtering + per-plugin error isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit doRuntimeConfigurationBeforeAutoConfiguration now (1) respects plugin enablement for the current scope/environment via isEnabled(activeProfiles) — matching the late doRuntimeConfiguration so a plugin disabled for the active profile does not contribute early beans — and (2) wraps each plugin's closure in try/catch, rethrowing as GrailsConfigurationException with the plugin name so a failure is attributable rather than aborting the boot opaquely. --- .../plugins/AbstractGrailsPluginManager.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java index 572983e29ea..4f2f9bd8918 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -56,6 +56,7 @@ import grails.plugins.GrailsPluginManager; import grails.plugins.Plugin; import grails.plugins.exceptions.PluginException; +import org.grails.core.exceptions.GrailsConfigurationException; import grails.util.Environment; import grails.util.GrailsNameUtils; import org.apache.grails.core.plugins.PluginDiscovery; @@ -184,11 +185,20 @@ public Object convert(NavigableMap.NullSafeNavigator source) { @Override public void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { checkInitialised(); + String[] activeProfiles = applicationContext != null + ? applicationContext.getEnvironment().getActiveProfiles() + : new String[0]; for (PluginInfo pluginInfo : pluginDiscovery.getPluginsInTopologicalOrder()) { GrailsPlugin grailsPlugin = plugins.get(pluginInfo.getName()); - // Only plugins that opt in (override the default no-op) register anything here. - if (grailsPlugin.supportsCurrentScopeAndEnvironment()) { - grailsPlugin.doWithRuntimeConfigurationBeforeAutoConfiguration(springConfig); + // Only plugins enabled for the current scope/environment, and that opt in by overriding + // the default no-op, register anything in this phase. + if (grailsPlugin.supportsCurrentScopeAndEnvironment() && grailsPlugin.isEnabled(activeProfiles)) { + try { + grailsPlugin.doWithRuntimeConfigurationBeforeAutoConfiguration(springConfig); + } catch (Throwable t) { + throw new GrailsConfigurationException("Error configuring before-auto-configuration beans for plugin " + + grailsPlugin.getName() + ": " + t.getMessage(), t); + } } } } From 86d2182019f3b0081b7282b121bde84b40517735 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 19:25:19 -0500 Subject: [PATCH 10/20] Config parity for early phase; document app classes use @Bean (#5, #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7: the before-auto-config phase now builds grailsApplication.config with the same conversion service + converters the main lifecycle registers (String->Resource, NullSafeNavigator->null x2), so early closures read config identically — null-safe navigation and resource coercion, not just scalar getProperty(). #5: documented that the early phase is plugin-scoped. An application class does not need it: it is a @Configuration, and Spring Boot processes user configuration before auto-configuration, so a plain @Bean already registers ahead of auto-config and @ConditionalOnMissingBean beans defer to it. Draining the app class early is also unsafe (its grailsApplication is not available yet). --- ...sBeforeAutoConfigurationPostProcessor.java | 31 ++++++++++++++++++- .../core/GrailsApplicationLifeCycle.groovy | 8 +++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java index 1777ed6bdd4..83acae51cda 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -23,6 +23,7 @@ import grails.plugins.DefaultGrailsPluginManager; import grails.plugins.GrailsPluginManager; import org.apache.grails.core.plugins.PluginDiscovery; +import org.grails.config.NavigableMap; import org.grails.config.PropertySourcesConfig; import org.grails.spring.DefaultRuntimeSpringConfiguration; import org.grails.spring.RuntimeSpringConfiguration; @@ -34,6 +35,10 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.PriorityOrdered; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.Resource; /** * Registers plugin beans contributed via @@ -72,7 +77,7 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t PluginDiscovery pluginDiscovery = applicationContext.getBean(PluginDiscovery.BEAN_NAME, PluginDiscovery.class); DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication(); - grailsApplication.setConfig(new PropertySourcesConfig(applicationContext.getEnvironment().getPropertySources())); + grailsApplication.setConfig(buildConfig()); grailsApplication.setApplicationContext(applicationContext); DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); @@ -88,6 +93,30 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t } } + /** + * Builds the {@link PropertySourcesConfig} that backs {@code grailsApplication.config} in this + * phase, registering the same conversion-service converters that + * {@code GrailsApplicationPostProcessor.loadApplicationConfig} registers for the main lifecycle. + * This gives before-auto-configuration closures parity when reading config — null-safe + * navigation of missing paths and {@code String -> Resource} coercion — not just scalar + * {@code getProperty(...)} access. + */ + private PropertySourcesConfig buildConfig() { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + ConfigurableConversionService conversionService = null; + if (environment instanceof AbstractEnvironment) { + conversionService = ((AbstractEnvironment) environment).getConversionService(); + conversionService.addConverter(String.class, Resource.class, applicationContext::getResource); + conversionService.addConverter(NavigableMap.NullSafeNavigator.class, String.class, source -> null); + conversionService.addConverter(NavigableMap.NullSafeNavigator.class, Object.class, source -> null); + } + PropertySourcesConfig config = new PropertySourcesConfig(environment.getPropertySources()); + if (conversionService != null) { + config.setConversionService(conversionService); + } + return config; + } + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // no-op diff --git a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy index 11b0c8d86fb..9b1ed6b5efe 100644 --- a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy +++ b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy @@ -57,6 +57,14 @@ interface GrailsApplicationLifeCycle { * iterating {@code grailsApplication} artefacts is not (they are not discovered yet — use * {@link #doWithSpring()} for those). * + *

Plugins only. This phase is drained for plugins. An application + * class does not need it: it is a {@code @Configuration}, and Spring Boot processes user + * configuration before auto-configuration, so a plain {@code @Bean} in the application class + * already registers ahead of auto-config and Boot's {@code @ConditionalOnMissingBean} beans + * defer to it. (Draining the application class here is also unsafe — its + * {@code grailsApplication} is not yet available this early.) So override this on a + * {@link grails.plugins.Plugin}; in an application class, declare a {@code @Bean} instead. + * * @return A closure that defines beans to be registered before auto-configuration * @since 8.0 */ From 0cdb6ec25848337dae1382bcb17111797eb24c16 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 19:48:32 -0500 Subject: [PATCH 11/20] Add end-to-end integration test for the before-auto-config phase (#8) app3 BeforeAutoConfigPhaseSpec (@Integration, real GrailsApp boot) asserts that the loadafter plugin's doWithSpringBeforeAutoConfiguration bean is registered through real plugin discovery and the early drain. Exercises the actual GrailsBeforeAutoConfigurationPostProcessor in a real boot (not a stub), complementing the unit tests that prove the @ConditionalOnMissingBean defer mechanism. Runs as part of the CI Functional Tests matrix. --- .../app3/BeforeAutoConfigPhaseSpec.groovy | 44 +++++++++++++++++++ .../loadafter/LoadafterGrailsPlugin.groovy | 9 ++++ 2 files changed, 53 insertions(+) create mode 100644 grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy diff --git a/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy b/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy new file mode 100644 index 00000000000..cccc00c9ae8 --- /dev/null +++ b/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy @@ -0,0 +1,44 @@ +/* + * 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 app3 + +import grails.testing.mixin.integration.Integration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import spock.lang.Specification + +/** + * End-to-end test of the before-auto-configuration phase through real plugin discovery and a real + * GrailsApp boot. The {@code loadfirst} plugin registers {@code beforeAutoConfigProbe} in + * {@code doWithSpringBeforeAutoConfiguration} (ahead of auto-config); the application defines a + * {@code @ConditionalOnMissingBean(name='beforeAutoConfigProbe')} default. The plugin's bean must + * win and the default must defer. + */ +@Integration +class BeforeAutoConfigPhaseSpec extends Specification { + + @Autowired + ApplicationContext applicationContext + + void "a real plugin's doWithSpringBeforeAutoConfiguration bean is registered in a real boot"() { + expect: 'loadfirst contributed the bean through real plugin discovery + the early drain' + applicationContext.containsBean('beforeAutoConfigProbe') + applicationContext.getBean('beforeAutoConfigProbe') == 'from-plugin-before-autoconfig' + } +} diff --git a/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy b/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy index 8a1c853f436..3d3ab357425 100755 --- a/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy +++ b/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy @@ -33,4 +33,13 @@ Brief summary/description of the plugin. def loadAfter = ['springSecurityCore'] + // Exercises the before-auto-configuration phase end-to-end (see app3 BeforeAutoConfigPhaseSpec): + // registered ahead of auto-config through real plugin discovery + the early drain. + @Override + Closure doWithSpringBeforeAutoConfiguration() { + { -> + beforeAutoConfigProbe(String, 'from-plugin-before-autoconfig') + } + } + } From e1710f3d1125fda6ab86cdadbe440e8fcf1b8287 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 19:57:34 -0500 Subject: [PATCH 12/20] Document the before-auto-configuration phase in the user guide (#11) Adds a "Registering Beans Before Auto-Configuration" section to the plugins guide (hookingIntoRuntimeConfiguration.adoc): when to use doWithSpringBeforeAutoConfiguration vs doWithSpring (the two permanent phases), the bean-definition-only/no-side-effects contract, the app-class-uses-@Bean note, and the grails.plugins.warnOnDeferrableOverride flag. --- .../hookingIntoRuntimeConfiguration.adoc | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc index f7b6593ea5d..660f0efbd5b 100644 --- a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc +++ b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc @@ -51,6 +51,39 @@ class I18nGrailsPlugin extends Plugin { This plugin configures the Grails `messageSource` bean and a couple of other beans to manage Locale resolution and switching. It using the link:spring.html#theBeanBuilderDSLExplained[Spring Bean Builder] syntax to do so. +==== Registering Beans Before Auto-Configuration + + +`doWithSpring` runs _after_ Spring Boot auto-configuration. That is the right phase for most beans, but it means a plugin bean that shares a name with a Spring Boot auto-configuration bean _overrides_ it — the auto-configuration bean is built and then replaced. + +When a plugin wants its bean to be the one Spring Boot's `@ConditionalOnMissingBean` defers to, override `doWithSpringBeforeAutoConfiguration` instead. Beans returned here are registered _ahead of_ auto-configuration, so the matching auto-configuration bean simply backs off — no override, and no wasted bean creation: + +[source,groovy] +---- +import org.springframework.web.servlet.i18n.SessionLocaleResolver +import grails.plugins.* + +class MyPlugin extends Plugin { + + @Override + Closure doWithSpringBeforeAutoConfiguration() {{-> + // Boot's @ConditionalOnMissingBean(name="localeResolver") now defers to this bean + localeResolver(SessionLocaleResolver) + }} +} +---- + +Use this phase only for beans that need to win over a Spring Boot default. Keep everything else in `doWithSpring`, which remains the correct place for: + +* beans that _decorate_ or wrap a bean auto-configuration already created (they need it to exist first); +* beans that iterate Grails artefacts (controllers, services, taglibs) — those are not discovered this early; +* intentionally overriding an _unconditional_ Boot bean (the only way to replace it). + +IMPORTANT: The closure returned by `doWithSpringBeforeAutoConfiguration` must only _define bean definitions_ and be free of side effects. This phase runs in an isolated, lightweight pass that instantiates plugins purely to collect their bean definitions; a closure with side effects (mutating shared state, I/O, registering listeners) would run twice. Reading `grailsApplication.config` is safe; iterating artefacts is not. This phase applies to plugins; an application class is itself a `@Configuration`, so a plain `@Bean` there already registers before auto-configuration. + +If a plugin bean defined in `doWithSpring` overrides an auto-configuration bean that is `@ConditionalOnMissingBean` on that name — a bean that _would_ have deferred — Grails logs a one-line warning pointing you to `doWithSpringBeforeAutoConfiguration`. Set `grails.plugins.warnOnDeferrableOverride=false` to silence it. + + ==== Customizing the Servlet Environment From 898793e74448939ca8efa5a155274f21369b7401 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 20:06:13 -0500 Subject: [PATCH 13/20] Migrate the grails-i18n bean cluster to @ConditionalOnMissingBean (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real framework site moved onto the overridable pattern the before-auto-config phase enables. localeResolver, localeChangeInterceptor and messageSource in I18nAutoConfiguration are now @ConditionalOnMissingBean by name. They still win by default (grails-i18n is @AutoConfigureBefore WebMvc/MessageSource auto-config, so it registers first — Boot's localeResolver defers to it), but are now cleanly overridable by a plugin's doWithSpringBeforeAutoConfiguration bean or an app @Bean, retiring the "unconditional framework bean that can only be replaced via an override warning" pattern. This is what PR #15751 attempted and could not achieve alone — it works here because the base PR (#13863) removed the injected @EnableWebMvc, so WebMvcAutoConfiguration is active and grails-i18n's before-ordering makes it win the @ConditionalOnMissingBean race. Validated: app1 boots green; conditions report shows I18nAutoConfiguration#localeResolver created and Boot's WebMvc localeResolver deferred. --- .../groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java b/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java index 0965fa64258..7c271e7fc84 100644 --- a/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java +++ b/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; @@ -56,11 +57,13 @@ public class I18nAutoConfiguration { private int fileCacheSeconds; @Bean(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) + @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) public LocaleResolver localeResolver() { return new SessionLocaleResolver(); } @Bean + @ConditionalOnMissingBean(name = "localeChangeInterceptor") public LocaleChangeInterceptor localeChangeInterceptor() { ParamsAwareLocaleChangeInterceptor localeChangeInterceptor = new ParamsAwareLocaleChangeInterceptor(); localeChangeInterceptor.setParamName("lang"); @@ -68,6 +71,7 @@ public LocaleChangeInterceptor localeChangeInterceptor() { } @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) + @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) public MessageSource messageSource(GrailsApplication grailsApplication, GrailsPluginManager pluginManager) { PluginAwareResourceBundleMessageSource messageSource = new PluginAwareResourceBundleMessageSource(grailsApplication, pluginManager); messageSource.setDefaultEncoding(encoding); From 3873af6b0067e73e9c01654ce3690a6908a5e690 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 20:57:15 -0500 Subject: [PATCH 14/20] Address code review: listener leak, test deferral, BDRPP cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (Critical-ish) Fix latent double ApplicationListener registration: the early phase no longer propagates the real context to its throwaway plugin instances (manager context left unset; the drain reads active profiles from the GrailsApplication's main context instead). Null-guard DefaultGrailsPlugin.setApplicationContext's listener registration. A plugin implementing ApplicationListener is now registered only once, by the real lifecycle. - Make the app3 integration test actually prove DEFERRAL: Application now defines a @ConditionalOnMissingBean(name='beforeAutoConfigProbe') default and the spec asserts the plugin's value wins (the default deferred), not just bean presence. Fixed the javadoc (loadafter, not loadfirst; the default does exist). - Drop the inert PriorityOrdered/getOrder() from the BDRPP — Spring does not sort manually-added post-processors by getOrder(); ordering comes solely from addBeanFactoryPostProcessor. Documented. - Guard plugin-discovery lookup on the LOCAL singleton (getSingleton) so a parent context's discovery can't trigger the early drain in a child context. Validated: grails-core compiles + full :grails-core:test green; app3 deferral integration test passes; app1 (GSP) boots clean. --- ...sBeforeAutoConfigurationPostProcessor.java | 36 ++++++++++++------- .../plugins/AbstractGrailsPluginManager.java | 7 ++-- .../grails/plugins/DefaultGrailsPlugin.java | 2 +- .../grails-app/init/app3/Application.groovy | 9 +++++ .../app3/BeforeAutoConfigPhaseSpec.groovy | 13 ++++--- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java index 83acae51cda..da700ddc52f 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -34,7 +34,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.PriorityOrdered; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.ConfigurableEnvironment; @@ -54,11 +53,22 @@ *

This deliberately does the minimum needed to drain the modern phase: it builds a lightweight * {@link GrailsApplication} backed by the already-populated {@code Environment} config and loads the plugin * classes (cheap). The full plugin lifecycle (artefact discovery, dynamic methods, legacy {@code doWithSpring}) - * still runs later in {@code GrailsApplicationPostProcessor}; {@code loadPlugins()} is idempotent. + * runs later in {@code GrailsApplicationPostProcessor} against a separate plugin manager, so each plugin is + * instantiated twice. That is safe for the bean-definition-only contract of the phase, with one caveat: a + * plugin that also implements {@link org.springframework.context.ApplicationListener} would, when given the + * real context, be registered as a listener in both passes — so this phase does not propagate + * the application context to its throwaway plugin instances (the manager context is left unset; bean + * definitions are flushed straight into the registry). + * + *

Ordering is guaranteed by being added via {@code addBeanFactoryPostProcessor} (manually-registered + * {@code BeanDefinitionRegistryPostProcessor}s run before registry-discovered ones such as + * {@code ConfigurationClassPostProcessor}). It is NOT an ordered/priority bean — Spring does not sort + * manually-added post-processors by {@code getOrder()} — so this class deliberately does not implement + * {@code PriorityOrdered}. * * @since 8.0 */ -public class GrailsBeforeAutoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered { +public class GrailsBeforeAutoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor { private static final Logger LOG = LoggerFactory.getLogger(GrailsBeforeAutoConfigurationPostProcessor.class); @@ -70,18 +80,25 @@ public GrailsBeforeAutoConfigurationPostProcessor(ConfigurableApplicationContext @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - if (!applicationContext.containsBean(PluginDiscovery.BEAN_NAME)) { - // No plugin discovery promoted to the context (e.g. unit-test slice) — nothing to do. + // Check the LOCAL singleton only — a parent context's discovery must not cause us to re-run + // the early drain into a child context (containsBean/getBean would delegate to the parent). + Object discovery = applicationContext.getBeanFactory().getSingleton(PluginDiscovery.BEAN_NAME); + if (!(discovery instanceof PluginDiscovery)) { + // No plugin discovery promoted to this context (e.g. unit-test slice) — nothing to do. return; } - PluginDiscovery pluginDiscovery = applicationContext.getBean(PluginDiscovery.BEAN_NAME, PluginDiscovery.class); + PluginDiscovery pluginDiscovery = (PluginDiscovery) discovery; DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication(); grailsApplication.setConfig(buildConfig()); grailsApplication.setApplicationContext(applicationContext); + grailsApplication.setMainContext(applicationContext); + // The manager's application context is deliberately NOT set: loadPlugins() must not propagate + // the real context to these throwaway plugin instances, or a plugin implementing + // ApplicationListener would be registered a second time (see class javadoc). The drain needs + // only the GrailsApplication (bound above); profiles are read from its main context. DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); - pluginManager.setApplicationContext(applicationContext); pluginManager.loadPlugins(); RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); @@ -121,9 +138,4 @@ private PropertySourcesConfig buildConfig() { public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // no-op } - - @Override - public int getOrder() { - return PriorityOrdered.HIGHEST_PRECEDENCE; - } } diff --git a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java index 4f2f9bd8918..9c4ace859f9 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -185,8 +185,11 @@ public Object convert(NavigableMap.NullSafeNavigator source) { @Override public void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { checkInitialised(); - String[] activeProfiles = applicationContext != null - ? applicationContext.getEnvironment().getActiveProfiles() + // The before-auto-config phase leaves the manager context unset (to avoid re-registering + // ApplicationListener plugins); read active profiles from the GrailsApplication's main context. + ApplicationContext context = application != null ? application.getMainContext() : null; + String[] activeProfiles = context != null + ? context.getEnvironment().getActiveProfiles() : new String[0]; for (PluginInfo pluginInfo : pluginDiscovery.getPluginsInTopologicalOrder()) { GrailsPlugin grailsPlugin = plugins.get(pluginInfo.getName()); diff --git a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java index 7ef7918fca3..cdf6a5a10ad 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java +++ b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java @@ -148,7 +148,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws if (this.plugin instanceof ApplicationContextAware) { ((ApplicationContextAware) plugin).setApplicationContext(applicationContext); } - if (this.plugin instanceof ApplicationListener) { + if (applicationContext != null && this.plugin instanceof ApplicationListener) { ((ConfigurableApplicationContext) applicationContext).addApplicationListener((ApplicationListener) plugin); } } diff --git a/grails-test-examples/app3/grails-app/init/app3/Application.groovy b/grails-test-examples/app3/grails-app/init/app3/Application.groovy index 2c8e46b552d..167765a4282 100755 --- a/grails-test-examples/app3/grails-app/init/app3/Application.groovy +++ b/grails-test-examples/app3/grails-app/init/app3/Application.groovy @@ -21,9 +21,18 @@ package app3 import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } + + // Default for the probe the loadafter plugin registers in its before-auto-configuration phase. + // Because the plugin registered it ahead of auto-config, this @ConditionalOnMissingBean default + // must defer to it — BeforeAutoConfigPhaseSpec asserts the plugin's value wins. + @Bean + @ConditionalOnMissingBean(name = 'beforeAutoConfigProbe') + String beforeAutoConfigProbe() { 'from-conditional-default' } } diff --git a/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy b/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy index cccc00c9ae8..ea062fcbc3b 100644 --- a/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy +++ b/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy @@ -25,10 +25,10 @@ import spock.lang.Specification /** * End-to-end test of the before-auto-configuration phase through real plugin discovery and a real - * GrailsApp boot. The {@code loadfirst} plugin registers {@code beforeAutoConfigProbe} in - * {@code doWithSpringBeforeAutoConfiguration} (ahead of auto-config); the application defines a - * {@code @ConditionalOnMissingBean(name='beforeAutoConfigProbe')} default. The plugin's bean must - * win and the default must defer. + * GrailsApp boot. The {@code loadafter} plugin (an app3 dependency) registers + * {@code beforeAutoConfigProbe} in {@code doWithSpringBeforeAutoConfiguration} (ahead of auto-config); + * {@code Application} defines a {@code @ConditionalOnMissingBean(name='beforeAutoConfigProbe')} + * default. The plugin's bean must win and the default must defer. */ @Integration class BeforeAutoConfigPhaseSpec extends Specification { @@ -36,9 +36,8 @@ class BeforeAutoConfigPhaseSpec extends Specification { @Autowired ApplicationContext applicationContext - void "a real plugin's doWithSpringBeforeAutoConfiguration bean is registered in a real boot"() { - expect: 'loadfirst contributed the bean through real plugin discovery + the early drain' - applicationContext.containsBean('beforeAutoConfigProbe') + void "a plugin's before-auto-config bean wins; the app @ConditionalOnMissingBean default defers"() { + expect: 'the plugin registered the probe ahead of auto-config, so the conditional default deferred to it' applicationContext.getBean('beforeAutoConfigProbe') == 'from-plugin-before-autoconfig' } } From 54d39913c744e948b44f2d649419ebfcf9ddf442 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 21:02:45 -0500 Subject: [PATCH 15/20] Cleanup: drop leftover debug logger + redundant no-op comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the vague "Registered plugin beans..." LOG.debug (leftover from prototype proof-marker debugging) from GrailsBeforeAutoConfigurationPostProcessor, and the now-unused Logger field/imports. - Drop "// no-op by default" inline comments from the GrailsPlugin/GrailsPluginManager default methods — the javadoc already states the default is a no-op. --- .../GrailsBeforeAutoConfigurationPostProcessor.java | 8 -------- .../src/main/groovy/grails/plugins/GrailsPlugin.java | 1 - .../main/groovy/grails/plugins/GrailsPluginManager.java | 1 - 3 files changed, 10 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java index da700ddc52f..0f9131f7e62 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -27,8 +27,6 @@ import org.grails.config.PropertySourcesConfig; import org.grails.spring.DefaultRuntimeSpringConfiguration; import org.grails.spring.RuntimeSpringConfiguration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -70,8 +68,6 @@ */ public class GrailsBeforeAutoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor { - private static final Logger LOG = LoggerFactory.getLogger(GrailsBeforeAutoConfigurationPostProcessor.class); - private final ConfigurableApplicationContext applicationContext; public GrailsBeforeAutoConfigurationPostProcessor(ConfigurableApplicationContext applicationContext) { @@ -104,10 +100,6 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); pluginManager.doRuntimeConfigurationBeforeAutoConfiguration(springConfig); springConfig.registerBeansWithRegistry(registry); - - if (LOG.isDebugEnabled()) { - LOG.debug("Registered plugin beans from the before-auto-configuration phase"); - } } /** diff --git a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java index d5cec47ec3d..4a5783ea04b 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java @@ -201,7 +201,6 @@ public interface GrailsPlugin extends ApplicationContextAware, Comparable, Grail * @since 8.0 */ default void doWithRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { - // no-op by default } /** diff --git a/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java b/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java index f988d878e20..9a016acfe0f 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java @@ -116,7 +116,6 @@ public interface GrailsPluginManager extends ApplicationContextAware { * @since 8.0 */ default void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { - // no-op by default } /** From 21cb710223ec02508dd42531758471d6264df33a Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 21:42:16 -0500 Subject: [PATCH 16/20] Tier 1: make UrlMappings framework beans overridable (@ConditionalOnMissingBean) grailsCorsFilter, urlMappingsErrorPageCustomizer and urlMappingsInfoHandlerAdapter are Grails-specific beans with no Spring Boot equivalent, so adding @ConditionalOnMissingBean is zero-risk: they still register by default (verified: all three "matched" in app1's conditions report) but an app/plugin can now provide its own without an override warning. --- .../plugins/web/mapping/UrlMappingsAutoConfiguration.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grails-url-mappings/src/main/groovy/org/grails/plugins/web/mapping/UrlMappingsAutoConfiguration.java b/grails-url-mappings/src/main/groovy/org/grails/plugins/web/mapping/UrlMappingsAutoConfiguration.java index 1d4edfae6a5..d1eb1234510 100644 --- a/grails-url-mappings/src/main/groovy/org/grails/plugins/web/mapping/UrlMappingsAutoConfiguration.java +++ b/grails-url-mappings/src/main/groovy/org/grails/plugins/web/mapping/UrlMappingsAutoConfiguration.java @@ -76,12 +76,14 @@ public LinkGenerator grailsLinkGenerator() { } @Bean + @ConditionalOnMissingBean(GrailsCorsFilter.class) @ConditionalOnProperty(name = Settings.SETTING_CORS_FILTER, havingValue = "true", matchIfMissing = true) public GrailsCorsFilter grailsCorsFilter(GrailsCorsConfiguration grailsCorsConfiguration) { return new GrailsCorsFilter(grailsCorsConfiguration); } @Bean + @ConditionalOnMissingBean(UrlMappingsErrorPageCustomizer.class) public UrlMappingsErrorPageCustomizer urlMappingsErrorPageCustomizer(ObjectProvider urlMappingsProvider) { UrlMappingsErrorPageCustomizer errorPageCustomizer = new UrlMappingsErrorPageCustomizer(); errorPageCustomizer.setUrlMappings(urlMappingsProvider.getIfAvailable()); @@ -89,6 +91,7 @@ public UrlMappingsErrorPageCustomizer urlMappingsErrorPageCustomizer(ObjectProvi } @Bean + @ConditionalOnMissingBean(UrlMappingsInfoHandlerAdapter.class) public UrlMappingsInfoHandlerAdapter urlMappingsInfoHandlerAdapter() { return new UrlMappingsInfoHandlerAdapter(); } From 931c0426cd4cbcd1aeaab12f08611117b064f05f Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 21:42:16 -0500 Subject: [PATCH 17/20] Tier 1: make Controllers Grails-specific beans overridable (@ConditionalOnMissingBean) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grailsWebRequestFilter (the FilterRegistrationBean) and webMvcConfig (GrailsWebMvcConfigurer) are Grails-specific, so @ConditionalOnMissingBean is zero-risk (both "matched" in app1's conditions report — created by default, now overridable). Deliberately NOT migrated: dispatcherServlet / dispatcherServletRegistration (GrailsDispatcherServlet intentionally REPLACES Boot's; ControllersAutoConfiguration is only @AutoConfigureBefore WebMvcAutoConfiguration, not DispatcherServletAutoConfiguration, so making it conditional risks deferring to Boot's plain DispatcherServlet) and multipartConfigElement (Boot's MultipartAutoConfiguration provides a @ConditionalOnMissingBean one; same ordering risk would lose Grails' config-driven settings). Those are the "intentional override of a Boot bean" category that legitimately stays. --- .../plugins/web/controllers/ControllersAutoConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index cd079b7def2..458c301c79c 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -123,6 +123,7 @@ public GrailsWebRequestFilter grailsWebRequest(ApplicationContext applicationCon } @Bean + @ConditionalOnMissingBean(name = "grailsWebRequestFilter") public FilterRegistrationBean grailsWebRequestFilter(GrailsWebRequestFilter grailsWebRequest) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(grailsWebRequest); @@ -163,6 +164,7 @@ public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApp } @Bean + @ConditionalOnMissingBean(GrailsWebMvcConfigurer.class) public GrailsWebMvcConfigurer webMvcConfig() { return new GrailsWebMvcConfigurer(resourcesCachePeriod, resourcesEnabled, resourcesPattern); } From ad122ae9cbf16fc121658dda730e3cce47b7aab9 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 22:52:04 -0500 Subject: [PATCH 18/20] Address second review: complete listener-leak fix, drop dead profile filter, search=CURRENT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1 (complete the earlier listener-leak fix): the throwaway DefaultGrailsApplication was still registering as an ApplicationListener on the real context via setApplicationContext. The drain only needs the app's config, so the early phase now gives it NO context at all (drops setApplicationContext + setMainContext); plugins were already not given the context. Impact was benign (the app listener only sets an inert flag on a GC'd object), but it's the same bug class the earlier review fixed for plugins, now closed on the GrailsApplication path too. - C3 (remove dead code): doRuntimeConfigurationBeforeAutoConfiguration's profile filter was inert — DefaultGrailsPlugin.profiles is never populated (isEnabled() always true), and the runtime profile isn't active this early anyway. Dropped the activeProfiles read entirely; the live supportsCurrentScopeAndEnvironment check + per-plugin error isolation remain. - C4: add search = SearchStrategy.CURRENT to the i18n localeResolver/messageSource/localeChangeInterceptor @ConditionalOnMissingBean guards, matching Boot's own messageSource, so they don't defer to an ancestor context's same-named bean in a parent/child setup. Validated: grails-core compiles + full :grails-core:test green; app3 deferral integration test passes (real drain unaffected by dropping the app context); app1 boots clean. --- ...sBeforeAutoConfigurationPostProcessor.java | 26 ++++++++++--------- .../plugins/AbstractGrailsPluginManager.java | 14 ++++------ .../plugins/i18n/I18nAutoConfiguration.java | 7 ++--- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java index 0f9131f7e62..9704a62f928 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -51,12 +51,13 @@ *

This deliberately does the minimum needed to drain the modern phase: it builds a lightweight * {@link GrailsApplication} backed by the already-populated {@code Environment} config and loads the plugin * classes (cheap). The full plugin lifecycle (artefact discovery, dynamic methods, legacy {@code doWithSpring}) - * runs later in {@code GrailsApplicationPostProcessor} against a separate plugin manager, so each plugin is - * instantiated twice. That is safe for the bean-definition-only contract of the phase, with one caveat: a - * plugin that also implements {@link org.springframework.context.ApplicationListener} would, when given the - * real context, be registered as a listener in both passes — so this phase does not propagate - * the application context to its throwaway plugin instances (the manager context is left unset; bean - * definitions are flushed straight into the registry). + * runs later in {@code GrailsApplicationPostProcessor} against a separate plugin manager and GrailsApplication, + * so each plugin is instantiated twice. That is safe for the bean-definition-only contract of the phase, with + * one caveat: anything implementing {@link org.springframework.context.ApplicationListener} would, when given + * the real context, be registered as a listener in both passes. So this phase gives the real context to + * neither its throwaway plugin instances (the manager context is left unset) nor + * its throwaway {@link GrailsApplication} (no {@code setApplicationContext}); bean definitions are flushed + * straight into the registry. * *

Ordering is guaranteed by being added via {@code addBeanFactoryPostProcessor} (manually-registered * {@code BeanDefinitionRegistryPostProcessor}s run before registry-discovered ones such as @@ -85,15 +86,16 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t } PluginDiscovery pluginDiscovery = (PluginDiscovery) discovery; + // A lightweight GrailsApplication, given ONLY its config so before-auto-config closures can read + // grailsApplication.config. It is deliberately given NO application context: setApplicationContext + // would register this throwaway app as an ApplicationListener on the real context, and likewise the + // manager context is left unset so loadPlugins() does not register throwaway plugin instances as + // listeners. Bean definitions are flushed straight into the registry below. + // (setConfig also publishes Holders.config; the real lifecycle resets it, and both wrap the same + // environment property sources, so the values are equivalent.) DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication(); grailsApplication.setConfig(buildConfig()); - grailsApplication.setApplicationContext(applicationContext); - grailsApplication.setMainContext(applicationContext); - // The manager's application context is deliberately NOT set: loadPlugins() must not propagate - // the real context to these throwaway plugin instances, or a plugin implementing - // ApplicationListener would be registered a second time (see class javadoc). The drain needs - // only the GrailsApplication (bound above); profiles are read from its main context. DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); pluginManager.loadPlugins(); diff --git a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java index 9c4ace859f9..24fe3b1f96a 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -185,17 +185,13 @@ public Object convert(NavigableMap.NullSafeNavigator source) { @Override public void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfiguration springConfig) { checkInitialised(); - // The before-auto-config phase leaves the manager context unset (to avoid re-registering - // ApplicationListener plugins); read active profiles from the GrailsApplication's main context. - ApplicationContext context = application != null ? application.getMainContext() : null; - String[] activeProfiles = context != null - ? context.getEnvironment().getActiveProfiles() - : new String[0]; for (PluginInfo pluginInfo : pluginDiscovery.getPluginsInTopologicalOrder()) { GrailsPlugin grailsPlugin = plugins.get(pluginInfo.getName()); - // Only plugins enabled for the current scope/environment, and that opt in by overriding - // the default no-op, register anything in this phase. - if (grailsPlugin.supportsCurrentScopeAndEnvironment() && grailsPlugin.isEnabled(activeProfiles)) { + // Skip plugins disabled for the current scope/environment; only plugins that opt in (override + // the default no-op) register anything here. Plugin *profile* filtering deliberately does not + // apply this early — plugin profiles are not evaluated yet, and the runtime profile is not + // activated until after ConfigurationClassPostProcessor has run. + if (grailsPlugin.supportsCurrentScopeAndEnvironment()) { try { grailsPlugin.doWithRuntimeConfigurationBeforeAutoConfiguration(springConfig); } catch (Throwable t) { diff --git a/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java b/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java index 7c271e7fc84..6d2ff646967 100644 --- a/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java +++ b/grails-i18n/src/main/groovy/org/grails/plugins/i18n/I18nAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.MessageSource; @@ -57,13 +58,13 @@ public class I18nAutoConfiguration { private int fileCacheSeconds; @Bean(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) - @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) + @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, search = SearchStrategy.CURRENT) public LocaleResolver localeResolver() { return new SessionLocaleResolver(); } @Bean - @ConditionalOnMissingBean(name = "localeChangeInterceptor") + @ConditionalOnMissingBean(name = "localeChangeInterceptor", search = SearchStrategy.CURRENT) public LocaleChangeInterceptor localeChangeInterceptor() { ParamsAwareLocaleChangeInterceptor localeChangeInterceptor = new ParamsAwareLocaleChangeInterceptor(); localeChangeInterceptor.setParamName("lang"); @@ -71,7 +72,7 @@ public LocaleChangeInterceptor localeChangeInterceptor() { } @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) - @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) + @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT) public MessageSource messageSource(GrailsApplication grailsApplication, GrailsPluginManager pluginManager) { PluginAwareResourceBundleMessageSource messageSource = new PluginAwareResourceBundleMessageSource(grailsApplication, pluginManager); messageSource.setDefaultEncoding(encoding); From 72b4985b779d70cb6db538709dc1bc19a738e689 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 22:32:10 -0600 Subject: [PATCH 19/20] Harden the @EnableWebMvc removal: legacy flag fallback + upgrade docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the base (#13863) review gaps: - GrailsViewResolverAutoConfiguration now honours the legacy spring.gsp.removeDefaultViewResolverBean property (which exists in apache/8.0.x) as a deprecated fallback for grails.web.removeDefaultViewResolverBean, logging a deprecation warning when the old name is used — instead of silently ignoring it. - Adds upgrading80x.adoc section 27 documenting that Boot's WebMvcAutoConfiguration is now active (no auto-injected @EnableWebMvc), what Grails still owns, the defaultViewResolver removal + opt-out flag, and that no action is required for typical apps. The JSON-views path the removal is justified by is already covered by the views-functional-tests example app (JSON/HAL render assertions + CircularSpec for the circular-view-path scenario), which passes on this branch — verified by running :grails-test-examples-views-functional-tests:integrationTest. Note: these harden content from base PR #13863; if #13863 merges standalone, cherry-pick them there. --- .../GrailsViewResolverAutoConfiguration.java | 18 +++++++++++-- .../src/en/guide/upgrading/upgrading80x.adoc | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java index 49b127f5d26..d572aba7b64 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java @@ -18,6 +18,8 @@ */ package org.grails.plugins.web.controllers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -41,15 +43,22 @@ * grails-gsp only loads for GSP applications, so the removal cannot live there. Ordered after * {@link WebMvcAutoConfiguration} so the bean exists by the time the registrar runs. * - *

Disable with {@code grails.web.removeDefaultViewResolverBean=false}. + *

Disable with {@code grails.web.removeDefaultViewResolverBean=false}. The earlier + * {@code spring.gsp.removeDefaultViewResolverBean} (when removal lived in grails-gsp) is still + * honoured for backward compatibility, but is deprecated. */ @AutoConfiguration(after = WebMvcAutoConfiguration.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @Import(GrailsViewResolverAutoConfiguration.RemoveDefaultViewResolverRegistrar.class) public class GrailsViewResolverAutoConfiguration { + static final String REMOVE_PROPERTY = "grails.web.removeDefaultViewResolverBean"; + static final String LEGACY_REMOVE_PROPERTY = "spring.gsp.removeDefaultViewResolverBean"; + static class RemoveDefaultViewResolverRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { + private static final Logger LOG = LoggerFactory.getLogger(RemoveDefaultViewResolverRegistrar.class); + private boolean removeDefaultViewResolverBean = true; @Override @@ -61,8 +70,13 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B @Override public void setEnvironment(Environment environment) { + // Honour the legacy grails-gsp property name for backward compatibility (deprecated). + Boolean legacy = environment.getProperty(LEGACY_REMOVE_PROPERTY, Boolean.class); + if (legacy != null) { + LOG.warn("'{}' is deprecated; use '{}' instead.", LEGACY_REMOVE_PROPERTY, REMOVE_PROPERTY); + } this.removeDefaultViewResolverBean = environment.getProperty( - "grails.web.removeDefaultViewResolverBean", Boolean.class, true); + REMOVE_PROPERTY, Boolean.class, legacy != null ? legacy : true); } } } diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 628b92cac7d..2762975a3e8 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -1008,3 +1008,28 @@ grails.gorm.default.constraints = { '*'(nullable: false) } ---- + +==== 27. Spring Boot Web MVC Auto-Configuration Is Now Active + +Earlier Grails versions auto-injected `@EnableWebMvc` into every application class, which switched Spring Boot's `WebMvcAutoConfiguration` **off** and made Grails responsible for the entire Web MVC setup. +Grails 8 no longer injects `@EnableWebMvc`, so Spring Boot's `WebMvcAutoConfiguration` is **active**, and Grails registers its own beans *ahead of* it so Boot's `@ConditionalOnMissingBean`-guarded beans defer to Grails. + +For the vast majority of applications this is transparent and requires **no action**. +Grails continues to own the beans it always has — its `dispatcherServlet` (`GrailsDispatcherServlet`), `localeResolver`, `messageSource`, `localeChangeInterceptor`, character-encoding and request-binding filters, URL-mappings handler — and Boot backs off in favour of them. +What changes is that Boot's auto-configuration now contributes the standard pieces it would in any Spring Boot MVC application (content negotiation, message converters, the welcome-page and static-resource handlers, async support, `mvcConversionService`/`mvcValidator`, etc.) rather than those being suppressed. + +**View resolution.** With `WebMvcAutoConfiguration` active, Boot contributes a catch-all `defaultViewResolver` (an `InternalResourceViewResolver`). For an application that does not use JSP/`InternalResource` views — a REST/JSON-views app, for example — that resolver forwards any unmatched view name to a servlet, producing `Circular view path [...]: would dispatch back to the current handler URL` errors. Grails therefore removes Boot's `defaultViewResolver` for every Grails servlet web application so its own view resolution is used. + +If you have a genuine reason to keep Boot's `defaultViewResolver`, opt out: + +[source,yaml] +.application.yml +---- +grails: + web: + removeDefaultViewResolverBean: false +---- + +NOTE: The earlier `spring.gsp.removeDefaultViewResolverBean` property (from when this removal lived in the GSP module) is still honoured for backward compatibility but is **deprecated** — use `grails.web.removeDefaultViewResolverBean`. Setting the old name logs a deprecation warning at startup. + +**Action required:** none for typical applications. If your application or a plugin explicitly registered MVC infrastructure to compensate for `@EnableWebMvc` being present (for example, manually re-adding a bean Boot's auto-configuration now provides), you can remove that workaround. If you defined a bean that collides by name with one Boot now contributes, prefer registering it so Boot defers (a plain `@Bean` in your application class, or `doWithSpringBeforeAutoConfiguration` in a plugin) rather than overriding it after the fact. From fbbefee70dd53155308ee34aab2d470ac98618cd Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Mon, 22 Jun 2026 22:46:07 -0600 Subject: [PATCH 20/20] Fix code style violations (checkstyle import order + operator wrap) - GrailsBeforeAutoConfigurationPostProcessor: remove now-unused grails.core.GrailsApplication / grails.plugins.GrailsPluginManager imports (fully-qualify the javadoc links), and order the springframework import group before the grails group. - DeferrableOverrideWarner / GrailsViewResolverAutoConfiguration: separate the slf4j (*) import group from the springframework group with a blank line. - AbstractGrailsPluginManager: move org.grails.core.exceptions.GrailsConfigurationException to its alphabetical position within the grails import group. - DeferrableOverrideWarner / AbstractGrailsPluginManager: put the string-concatenation '+' at the end of the line (OperatorWrap) in the new log/exception messages. aggregateStyleViolations now reports zero violations in the changed files. --- .../GrailsViewResolverAutoConfiguration.java | 1 + .../boot/config/DeferrableOverrideWarner.java | 13 ++++++------ ...sBeforeAutoConfigurationPostProcessor.java | 21 +++++++++---------- .../plugins/AbstractGrailsPluginManager.java | 6 +++--- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java index d572aba7b64..a2329e3028e 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; diff --git a/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java b/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java index 6eaf14149c4..4a97a37a17e 100644 --- a/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java +++ b/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -65,12 +66,12 @@ static void warnOnDeferrableOverrides(RuntimeSpringConfiguration springConfig, B continue; // a fresh bean, not an override } if (overridesDeferrableConditional(registry.getBeanDefinition(name), name)) { - LOG.warn("Bean '{}' registered in doWithSpring overrides a Spring Boot auto-configuration " - + "bean of the same name that is @ConditionalOnMissingBean(name=\"{}\") and would have " - + "deferred. Register it in doWithSpringBeforeAutoConfiguration() instead: the " - + "auto-configuration bean then backs off cleanly, with no override and no wasted bean " - + "creation. (doWithSpring stays correct for decoration, aggregation, artefact-driven " - + "beans, and intentional overrides of unconditional beans.)", name, name); + LOG.warn("Bean '{}' registered in doWithSpring overrides a Spring Boot auto-configuration " + + "bean of the same name that is @ConditionalOnMissingBean(name=\"{}\") and would have " + + "deferred. Register it in doWithSpringBeforeAutoConfiguration() instead: the " + + "auto-configuration bean then backs off cleanly, with no override and no wasted bean " + + "creation. (doWithSpring stays correct for decoration, aggregation, artefact-driven " + + "beans, and intentional overrides of unconditional beans.)", name, name); } } } diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java index 9704a62f928..44cc78b16b1 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -18,15 +18,6 @@ */ package grails.boot.config; -import grails.core.DefaultGrailsApplication; -import grails.core.GrailsApplication; -import grails.plugins.DefaultGrailsPluginManager; -import grails.plugins.GrailsPluginManager; -import org.apache.grails.core.plugins.PluginDiscovery; -import org.grails.config.NavigableMap; -import org.grails.config.PropertySourcesConfig; -import org.grails.spring.DefaultRuntimeSpringConfiguration; -import org.grails.spring.RuntimeSpringConfiguration; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -37,6 +28,14 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.Resource; +import grails.core.DefaultGrailsApplication; +import grails.plugins.DefaultGrailsPluginManager; +import org.apache.grails.core.plugins.PluginDiscovery; +import org.grails.config.NavigableMap; +import org.grails.config.PropertySourcesConfig; +import org.grails.spring.DefaultRuntimeSpringConfiguration; +import org.grails.spring.RuntimeSpringConfiguration; + /** * Registers plugin beans contributed via * {@link grails.core.GrailsApplicationLifeCycle#doWithSpringBeforeAutoConfiguration()} before @@ -49,14 +48,14 @@ * plugin's bean — no removal or override needed afterwards. * *

This deliberately does the minimum needed to drain the modern phase: it builds a lightweight - * {@link GrailsApplication} backed by the already-populated {@code Environment} config and loads the plugin + * {@link grails.core.GrailsApplication} backed by the already-populated {@code Environment} config and loads the plugin * classes (cheap). The full plugin lifecycle (artefact discovery, dynamic methods, legacy {@code doWithSpring}) * runs later in {@code GrailsApplicationPostProcessor} against a separate plugin manager and GrailsApplication, * so each plugin is instantiated twice. That is safe for the bean-definition-only contract of the phase, with * one caveat: anything implementing {@link org.springframework.context.ApplicationListener} would, when given * the real context, be registered as a listener in both passes. So this phase gives the real context to * neither its throwaway plugin instances (the manager context is left unset) nor - * its throwaway {@link GrailsApplication} (no {@code setApplicationContext}); bean definitions are flushed + * its throwaway {@link grails.core.GrailsApplication} (no {@code setApplicationContext}); bean definitions are flushed * straight into the registry. * *

Ordering is guaranteed by being added via {@code addBeanFactoryPostProcessor} (manually-registered diff --git a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java index 24fe3b1f96a..a3cbf711913 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -56,13 +56,13 @@ import grails.plugins.GrailsPluginManager; import grails.plugins.Plugin; import grails.plugins.exceptions.PluginException; -import org.grails.core.exceptions.GrailsConfigurationException; import grails.util.Environment; import grails.util.GrailsNameUtils; import org.apache.grails.core.plugins.PluginDiscovery; import org.apache.grails.core.plugins.PluginInfo; import org.apache.grails.core.plugins.PluginUtils; import org.grails.config.NavigableMap; +import org.grails.core.exceptions.GrailsConfigurationException; import org.grails.io.support.GrailsResourceUtils; import org.grails.plugins.support.WatchPattern; import org.grails.spring.RuntimeSpringConfiguration; @@ -195,8 +195,8 @@ public void doRuntimeConfigurationBeforeAutoConfiguration(RuntimeSpringConfigura try { grailsPlugin.doWithRuntimeConfigurationBeforeAutoConfiguration(springConfig); } catch (Throwable t) { - throw new GrailsConfigurationException("Error configuring before-auto-configuration beans for plugin " - + grailsPlugin.getName() + ": " + t.getMessage(), t); + throw new GrailsConfigurationException("Error configuring before-auto-configuration beans for plugin " + + grailsPlugin.getName() + ": " + t.getMessage(), t); } } }