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 ecce85b66e0..5774c175e9f 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 @@ -109,13 +109,25 @@ 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 + @ConditionalOnMissingBean(name = "grailsWebRequestFilter") + public FilterRegistrationBean grailsWebRequestFilter(GrailsWebRequestFilter grailsWebRequest) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(grailsWebRequest); registrationBean.setDispatcherTypes(EnumSet.of( DispatcherType.FORWARD, DispatcherType.INCLUDE, @@ -153,6 +165,7 @@ public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApp } @Bean + @ConditionalOnMissingBean(GrailsWebMvcConfigurer.class) public GrailsWebMvcConfigurer webMvcConfig() { return new GrailsWebMvcConfigurer(resourcesCachePeriod, resourcesEnabled, resourcesPattern); } 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..a2329e3028e --- /dev/null +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsViewResolverAutoConfiguration.java @@ -0,0 +1,83 @@ +/* + * 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.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; +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. + * + * 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}. 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 + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + if (removeDefaultViewResolverBean && registry.containsBeanDefinition("defaultViewResolver")) { + registry.removeBeanDefinition("defaultViewResolver"); + } + } + + @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( + REMOVE_PROPERTY, Boolean.class, legacy != null ? legacy : 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 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..4a97a37a17e --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/DeferrableOverrideWarner.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 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/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..44cc78b16b1 --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsBeforeAutoConfigurationPostProcessor.java @@ -0,0 +1,134 @@ +/* + * 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.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.convert.support.ConfigurableConversionService; +import org.springframework.core.env.AbstractEnvironment; +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 + * 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 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 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 + * {@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 { + + private final ConfigurableApplicationContext applicationContext; + + public GrailsBeforeAutoConfigurationPostProcessor(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + // 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 = (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()); + + DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); + pluginManager.loadPlugins(); + + RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); + pluginManager.doRuntimeConfigurationBeforeAutoConfiguration(springConfig); + springConfig.registerBeansWithRegistry(registry); + } + + /** + * 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 7f3ca551ec3..9b1ed6b5efe 100644 --- a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy +++ b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy @@ -35,6 +35,41 @@ 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. + * + *

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). + * + *

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 + */ + 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..4a5783ea04b 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java @@ -193,6 +193,16 @@ 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) { + } + /** * 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..9a016acfe0f 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPluginManager.java @@ -107,6 +107,17 @@ 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) { + } + /** * 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/compiler/injection/ApplicationClassInjector.groovy b/grails-core/src/main/groovy/org/grails/compiler/injection/ApplicationClassInjector.groovy index 73fda353f85..bd215309bf4 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 @@ -144,7 +144,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( 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..a3cbf711913 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java +++ b/grails-core/src/main/groovy/org/grails/plugins/AbstractGrailsPluginManager.java @@ -62,6 +62,7 @@ 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; @@ -176,6 +177,31 @@ 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()); + // 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) { + throw new GrailsConfigurationException("Error configuring before-auto-configuration beans for plugin " + + grailsPlugin.getName() + ": " + t.getMessage(), t); + } + } + } + } + /** * 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 8064850d832..dc0a9bc8103 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java +++ b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java @@ -149,7 +149,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); } } @@ -416,6 +416,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 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 {} +} 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 {} +} 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 diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index c17cb0d63bc..95610787363 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -1346,3 +1346,28 @@ grails: - Presto - Trident ---- + +==== 31. 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. 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 d32d50f72c3..0c2d0d6877d 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); } } 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..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 @@ -21,7 +21,9 @@ 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.condition.SearchStrategy; import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.MessageSource; @@ -56,11 +58,13 @@ public class I18nAutoConfiguration { private int fileCacheSeconds; @Bean(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", search = SearchStrategy.CURRENT) public LocaleChangeInterceptor localeChangeInterceptor() { ParamsAwareLocaleChangeInterceptor localeChangeInterceptor = new ParamsAwareLocaleChangeInterceptor(); localeChangeInterceptor.setParamName("lang"); @@ -68,6 +72,7 @@ public LocaleChangeInterceptor localeChangeInterceptor() { } @Bean(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); 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 new file mode 100644 index 00000000000..ea062fcbc3b --- /dev/null +++ b/grails-test-examples/app3/src/integration-test/groovy/app3/BeforeAutoConfigPhaseSpec.groovy @@ -0,0 +1,43 @@ +/* + * 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 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 { + + @Autowired + ApplicationContext applicationContext + + 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' + } +} 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') + } + } + } 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(); } 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)