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]

Reply via email to