codeconsole opened a new pull request, #15755:
URL: https://github.com/apache/grails-core/pull/15755
## Summary
Adds a new plugin lifecycle phase, `doWithSpringBeforeAutoConfiguration()`,
that lets a Grails plugin register Spring beans **before** Spring Boot
auto-configuration runs. Boot beans guarded by `@ConditionalOnMissingBean` then
**defer** to the plugin's bean, instead of the plugin having to **override or
remove** the Boot bean afterwards (the only thing the existing `doWithSpring()`
can do, because it runs *after* auto-configuration).
This is the root-cause fix for an entire class of friction: the "Overriding
bean definition for bean 'localeResolver'" warnings, and the
`removeBeanDefinition`/alias hacks Grails uses to bend Boot's beans into its
own model.
> **Depends on #13863** ("Stop `@EnableWebMvc` from being automatically
added"). This branch is stacked on it, so the first 4 commits here are from
that PR — the diff will shrink to just this feature once #13863 merges. The
feature *requires* #13863: Boot's `WebMvcAutoConfiguration` must be active for
`@ConditionalOnMissingBean` to have anything to defer.
## Why
`GrailsApplicationPostProcessor` (which drains `doWithSpring`) is an
un-ordered `BeanDefinitionRegistryPostProcessor`; Boot's
`ConfigurationClassPostProcessor` is `PriorityOrdered` and expands every
`@AutoConfiguration` first. So **every `doWithSpring` bean is registered after
auto-config** — a plugin can only override or remove a Boot bean, never make
`@ConditionalOnMissingBean` back off. That late override is also wasteful: the
Boot bean is built, then thrown away.
## How
A second `BeanDefinitionRegistryPostProcessor`
(`GrailsBeforeAutoConfigurationPostProcessor`) is added via
`addBeanFactoryPostProcessor` (manually-registered BDRPPs run **before**
registry-discovered ones such as `ConfigurationClassPostProcessor`). It drains
each plugin's `doWithSpringBeforeAutoConfiguration` closure into the registry
ahead of auto-config. A narrow, opt-out warning (`DeferrableOverrideWarner`)
nudges the one anti-pattern this removes — a late `doWithSpring` bean
overriding a name-guarded `@ConditionalOnMissingBean` auto-config bean that
*would* have deferred.
## Two permanent phases (not modern-vs-legacy)
`doWithSpring` is **not** deprecated. It stays the correct phase for:
decorating/wrapping a bean auto-config already created; aggregating/inspecting
the full registry; artefact-driven beans (one per controller/service/taglib —
those need the loaded `GrailsApplication` and stay late); and intentionally
overriding an *unconditional* Boot bean. Only "register late solely to override
a deferrable conditional" moves to the new phase.
## What's included
- **Mechanism**: the `doWithSpringBeforeAutoConfiguration()` lifecycle
method (default no-op on `GrailsApplicationLifeCycle`/`Plugin`), the drain
plumbing, the BDRPP + initializer (`spring.factories`).
- **Warning**: `DeferrableOverrideWarner`, gated on
`grails.plugins.warnOnDeferrableOverride` (default on), precise to name-guarded
`@ConditionalOnMissingBean` overrides.
- **First migrations** to the overridable pattern (each still wins by
default; now overridable without a warning):
- **grails-i18n**: `localeResolver`, `localeChangeInterceptor`,
`messageSource` → `@ConditionalOnMissingBean`. This is exactly what #15751
attempted and could not achieve alone — it works here because #13863 removed
`@EnableWebMvc`, so grails-i18n's
`@AutoConfigureBefore(WebMvcAutoConfiguration)` wins the race.
- **grails-url-mappings** / **grails-controllers**: the Grails-specific
beans (`grailsCorsFilter`, `urlMappingsErrorPageCustomizer`,
`urlMappingsInfoHandlerAdapter`, `grailsWebRequestFilter` registration,
`webMvcConfig`). `dispatcherServlet`/`multipartConfigElement` are deliberately
left as intentional Boot replacements (they'd need
`@AutoConfigureBefore(DispatcherServlet/MultipartAutoConfiguration)` to stay
safe).
- **Tests**: a unit test proving the ordering makes
`@ConditionalOnMissingBean` defer (+ control); a `DeferrableOverrideWarner`
precision test; and an `@Integration` test (app3) proving a real plugin's bean
wins over an app's `@ConditionalOnMissingBean` default through a real boot.
- **Docs**: a "Registering Beans Before Auto-Configuration" section in the
plugins guide.
## Design notes
- **The contract** (documented on the API): the closure must define bean
definitions only and be side-effect-free — the phase runs an isolated
lightweight plugin load, and the real lifecycle re-instantiates plugins, so
side effects would run twice. The early phase deliberately does **not**
propagate the application context to its throwaway plugin instances, so a
plugin implementing `ApplicationListener` is registered only once.
- **Rejected: sharing one plugin manager/`GrailsApplication` across the two
phases.** It's fragile — `setApplication()` doesn't update the field
`DefaultGrailsPlugin` binds `doWithSpring`'s `application` from, so rebinding
breaks artefact handling (`tagLibClasses`). The isolated lightweight load is
the robust design; the second (cheap) plugin instantiation is benign.
- **Application classes don't need this phase**: an app class is a
`@Configuration`, and Boot processes user config before auto-config, so a plain
`@Bean` already registers ahead of auto-config. The phase is plugin-scoped.
- **AOT/native**: introduces no new risk — it mirrors the existing
`GrailsApplicationPostProcessor` runtime-plugin-load + `BeanBuilder` mechanism.
## Validation
Full `:grails-core:test` green; app1 (GSP) and app3 (Hibernate) boot clean;
the app3 integration test proves deferral end-to-end; every migration verified
via the conditions report (bean still created by default, Boot's defers).
## Suggested rollout
Ship opt-in + the warning → migrate framework beans incrementally (i18n +
the Tier-1 set done here) → make the modern phase the default registration
timing → remove the legacy override path. Each conversion deletes one
Boot-fighting site.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]