Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
76401d0
Stop @EnableWebMvc from being automatically added to Grails Applications
codeconsole Jun 22, 2026
b05894e
Keep Grails' GrailsWebRequest bound once WebMvcAutoConfiguration is a…
codeconsole Jun 22, 2026
0a49d58
Remove Boot's defaultViewResolver for all Grails servlet web apps
codeconsole Jun 22, 2026
04622e3
Consolidate defaultViewResolver removal into one place
codeconsole Jun 22, 2026
fd012ce
Add doWithSpringBeforeAutoConfiguration plugin phase (prototype)
codeconsole Jun 22, 2026
68cdf62
Test: prove early BDRPP makes @ConditionalOnMissingBean defer
codeconsole Jun 22, 2026
84a451d
Warn (narrowly) when doWithSpring overrides a deferrable conditional …
codeconsole Jun 22, 2026
7044d95
Formalize the before-auto-configuration phase contract (no sharing)
codeconsole Jun 23, 2026
a727691
Harden the early drain: profile/scope filtering + per-plugin error is…
codeconsole Jun 23, 2026
86d2182
Config parity for early phase; document app classes use @Bean (#5, #7)
codeconsole Jun 23, 2026
0cdb6ec
Add end-to-end integration test for the before-auto-config phase (#8)
codeconsole Jun 23, 2026
e1710f3
Document the before-auto-configuration phase in the user guide (#11)
codeconsole Jun 23, 2026
898793e
Migrate the grails-i18n bean cluster to @ConditionalOnMissingBean (#13)
codeconsole Jun 23, 2026
3873af6
Address code review: listener leak, test deferral, BDRPP cleanups
codeconsole Jun 23, 2026
54d3991
Cleanup: drop leftover debug logger + redundant no-op comments
codeconsole Jun 23, 2026
21cb710
Tier 1: make UrlMappings framework beans overridable (@ConditionalOnM…
codeconsole Jun 23, 2026
931c042
Tier 1: make Controllers Grails-specific beans overridable (@Conditio…
codeconsole Jun 23, 2026
ad122ae
Address second review: complete listener-leak fix, drop dead profile …
codeconsole Jun 23, 2026
72b4985
Harden the @EnableWebMvc removal: legacy flag fallback + upgrade docs
codeconsole Jun 23, 2026
fbbefee
Fix code style violations (checkstyle import order + operator wrap)
codeconsole Jun 23, 2026
477f49d
Merge remote-tracking branch 'upstream/8.0.x' into feature/plugin-bef…
codeconsole Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,25 @@ public FilterRegistrationBean<Filter> 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<Filter> grailsWebRequestFilter(ApplicationContext applicationContext) {
FilterRegistrationBean<Filter> 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(GrailsWebRequestFilter grailsWebRequest) {
FilterRegistrationBean<GrailsWebRequestFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(grailsWebRequest);
registrationBean.setDispatcherTypes(EnumSet.of(
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
Expand Down Expand Up @@ -153,6 +165,7 @@ public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApp
}

@Bean
@ConditionalOnMissingBean(GrailsWebMvcConfigurer.class)
public GrailsWebMvcConfigurer webMvcConfig() {
return new GrailsWebMvcConfigurer(resourcesCachePeriod, resourcesEnabled, resourcesPattern);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.grails.plugins.web.controllers.ControllersAutoConfiguration
org.grails.plugins.web.controllers.GrailsViewResolverAutoConfiguration
Original file line number Diff line number Diff line change
@@ -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 <em>deferred</em> 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.
*
* <p>The check is deliberately narrow. {@code doWithSpring} (after auto-configuration) has permanent,
* legitimate uses that this MUST stay silent on:
* <ul>
* <li>decorating/wrapping a bean that auto-config created (needs it to exist first);</li>
* <li>aggregating or inspecting the fully-populated registry;</li>
* <li>artefact-driven beans (one per controller/service/taglib) that need a loaded GrailsApplication;</li>
* <li>intentionally overriding an <em>unconditional</em> Boot bean (the only way to replace it).</li>
* </ul>
* So it fires only on the one anti-pattern the modern phase removes: overriding a <em>name-guarded</em>
* {@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<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ConfigurableApplicationContext> {

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addBeanFactoryPostProcessor(new GrailsBeforeAutoConfigurationPostProcessor(applicationContext));
}
}
Loading
Loading