Skip to content

Make i18n and URL-mappings framework beans overridable (@ConditionalOnMissingBean)#15916

Merged
codeconsole merged 20 commits into
apache:8.0.xfrom
codeconsole:feat/overridable-i18n-urlmappings-beans
Jul 8, 2026
Merged

Make i18n and URL-mappings framework beans overridable (@ConditionalOnMissingBean)#15916
codeconsole merged 20 commits into
apache:8.0.xfrom
codeconsole:feat/overridable-i18n-urlmappings-beans

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Summary

Guards several Grails framework beans with @ConditionalOnMissingBean so an application- or plugin-defined bean takes precedence cleanly, instead of relying on bean-definition overriding (which produces a startup warning, or fails outright when spring.main.allow-bean-definition-overriding=false):

  • grails-i18n (I18nAutoConfiguration): localeResolver, localeChangeInterceptor, messageSource
  • grails-url-mappings (UrlMappingsAutoConfiguration): grailsCorsFilter, urlMappingsErrorPageCustomizer, urlMappingsInfoHandlerAdapter

All beans still register by default — this only changes what happens when the user defines their own.

Depends on #13863

This PR is stacked on #13863 (removal of the auto-injected @EnableWebMvc) and includes its commits; merge that first, after which this diff reduces to the changes described above.

The dependency is real, not just mechanical: with @EnableWebMvc present, Spring's WebMvcConfigurationSupport contributes its own localeResolver bean, which would win the @ConditionalOnMissingBean race and silently replace Grails' SessionLocaleResolver. With #13863 in, I18nAutoConfiguration is ordered before WebMvcAutoConfiguration, so the Grails bean registers first and Boot's conditional bean defers to it. This is what #15751 attempted and could not achieve standalone.

Context

Extracted from #15755 as an independently useful piece while the plugin-lifecycle discussion there is resolved (see the review discussion about unifying bean registration ahead of auto-configuration).

Testing

New auto-configuration specs for both modules covering default registration, the grails.cors.filter toggle, and user-bean back-off for every guarded bean. Both modules' full test suites and checkstyle/CodeNarc pass.

@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.
…ctive

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.
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 (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.
Resolve upgrade-notes conflict in upgrading80x.adoc: keep apache/8.0.x's
'MIME Types Are Now Framework Defaults' and 'Accept Header Honored for All
Clients' sections and renumber the appended '@EnableWebMvc No Longer Added'
section to 30 (subsections 30.1/30.2), giving sequential 26-30 numbering.
…eWebMvc

# Conflicts:
#	grails-doc/src/en/guide/upgrading/upgrading80x.adoc
…beans

- Honour the deprecated spring.gsp.removeDefaultViewResolverBean property
  as a fallback for grails.web.removeDefaultViewResolverBean, logging a
  deprecation warning when the old name is set
- Guard grailsWebRequestFilter and webMvcConfig with
  @ConditionalOnMissingBean so user-defined beans take precedence
- Update the 8.0 upgrade notes and add test coverage for the property
  matrix and the bean back-off behavior
Guard framework beans with @ConditionalOnMissingBean so an application-
or plugin-defined bean takes precedence cleanly instead of relying on
bean-definition overriding:

- i18n: localeResolver, localeChangeInterceptor, messageSource
- URL mappings: grailsCorsFilter, urlMappingsErrorPageCustomizer,
  urlMappingsInfoHandlerAdapter

The beans still register by default. The localeResolver guard requires
the @EnableWebMvc removal (apache#13863): with @EnableWebMvc present,
WebMvcConfigurationSupport contributes its own localeResolver bean that
would win the @ConditionalOnMissingBean race and silently replace
Grails' SessionLocaleResolver.

Adds auto-configuration test coverage for both modules (default
registration, property toggles, and user-bean back-off) and documents
the change in the 8.0 upgrade notes.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything immediately wrong with this. I merged the latest 8.0.x branch into this one to see if there are any notable test failures. I also asked AI to review it and it some minor comments that we should probably discuss. Overall, approving. We should wait to merge it until we have a second review though.

}

@Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT) here. Boot's own MessageSourceAutoConfiguration scopes its equivalent guard to SearchStrategy.CURRENT specifically because a messageSource is commonly present in a parent context, and each child context should still get its own. With the default search strategy (ancestors included), any hierarchical deployment would cause the Grails PluginAwareResourceBundleMessageSource to silently back off in favor of the parent's plain message source, losing plugin message-bundle resolution. The same consideration applies (more weakly) to localeResolver and localeChangeInterceptor above, but messageSource is the one with a real hierarchy precedent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied — and to all three beans, not just messageSource. DispatcherServlet resolves localeResolver/localeChangeInterceptor from its own context, so the parent-suppression problem applies with the same force there: a child context whose bean backed off would fall back to DispatcherServlet's default AcceptHeaderLocaleResolver, not the parent's bean. Also verified Boot 4 no longer contributes a WebMvc localeResolver at all (it did in Boot 3), so SearchStrategy.CURRENT has no auto-config interplay to worry about. Added a parent-context test covering all three.


==== 32. More Framework Beans Are Cleanly Overridable

Several Grails framework beans that were previously registered unconditionally are now guarded with `@ConditionalOnMissingBean`, so an application- or plugin-defined bean of the same name (or type) takes precedence cleanly instead of relying on bean-definition overriding:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "or plugin-defined" claim is stronger than what @ConditionalOnMissingBean can deliver. Beans contributed by a plugin's doWithSpring (or an app's resources.groovy) are registered by GrailsApplicationPostProcessor, a plain (unordered) BeanDefinitionRegistryPostProcessor that runs after ConfigurationClassPostProcessor has already evaluated auto-configuration conditions. Those beans therefore still land via bean-definition overriding, exactly as before — the conditional only sees beans defined in @Configuration classes and other auto-configurations. Suggest qualifying this so users don't expect doWithSpring beans to trigger the back-off (e.g. "a bean defined in application configuration classes"), or explicitly noting that doWithSpring/resources.groovy beans continue to rely on overriding.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — qualified. The section now scopes the clean back-off to @Bean methods in application configuration classes, and a NOTE spells out that doWithSpring/resources.groovy beans continue to take effect through bean-definition overriding, exactly as before. (Closing that gap is what #15934 does — once the retimed doWithSpring drain registers plugin beans before the conditions are evaluated, this note can be revisited.)

Comment thread grails-i18n/build.gradle Outdated
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 49.3593%. Comparing base (5e561da) to head (1ea2031).

Files with missing lines Patch % Lines
...ain/java/grails/gsp/boot/GspAutoConfiguration.java 0.0000% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15916        +/-   ##
==================================================
+ Coverage     49.3373%   49.3593%   +0.0220%     
- Complexity      16775      16777         +2     
==================================================
  Files            1985       1986         +1     
  Lines           93327      93336         +9     
  Branches        16336      16337         +1     
==================================================
+ Hits            46045      46070        +25     
+ Misses          40158      40139        -19     
- Partials         7124       7127         +3     
Files with missing lines Coverage Δ
.../web/controllers/ControllersAutoConfiguration.java 87.5000% <100.0000%> (+3.8636%) ⬆️
...ntrollers/GrailsViewResolverAutoConfiguration.java 100.0000% <100.0000%> (ø)
...core/src/main/groovy/grails/config/Settings.groovy 100.0000% <ø> (ø)
...compiler/injection/ApplicationClassInjector.groovy 8.0000% <ø> (+0.1569%) ⬆️
...org/grails/plugins/i18n/I18nAutoConfiguration.java 75.0000% <ø> (ø)
...gins/web/mapping/UrlMappingsAutoConfiguration.java 63.6364% <ø> (ø)
...grails/web/servlet/mvc/GrailsWebRequestFilter.java 54.5454% <100.0000%> (ø)
...ain/java/grails/gsp/boot/GspAutoConfiguration.java 0.0000% <0.0000%> (ø)

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codeconsole and others added 6 commits July 7, 2026 11:54
Review feedback on the stacked PR apache#15916: the property is user-facing
configuration, so the constant belongs in grails.config.Settings with
the other grails.web.* keys rather than a package-private field.
- Scope the i18n @ConditionalOnMissingBean guards to SearchStrategy.CURRENT:
  DispatcherServlet resolves localeResolver/localeChangeInterceptor from its
  own context and Boot's MessageSourceAutoConfiguration scopes its messageSource
  guard the same way, so parent-context beans must not suppress the child's
- Broaden the CORS guard to CorsFilter so any user CORS filter registration
  wins, not only a custom GrailsCorsFilter (two CORS filters never coexist)
- Reference the new Settings.WEB_REMOVE_DEFAULT_VIEW_RESOLVER_BEAN constant
  (declared on the base branch) instead of a string literal
- Qualify the upgrade-notes claim: the back-off applies to beans registered
  before auto-configuration (application @bean methods); doWithSpring and
  resources.groovy beans continue to rely on bean-definition overriding
- Add trailing newlines to the touched build files
- Tests: parent-context hierarchy coverage for the i18n beans and plain
  CorsFilter back-off coverage
@testlens-app

testlens-app Bot commented Jul 7, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 1ea2031
▶️ Tests: 18206 executed
⚪️ Checks: 61/61 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole merged commit 87a4afd into apache:8.0.x Jul 8, 2026
109 of 110 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants