Skip to content

Register plugin beans before Spring Boot auto-configuration by retiming doWithSpring#15934

Open
codeconsole wants to merge 18 commits into
apache:8.0.xfrom
codeconsole:feat/plugin-beans-before-autoconfig
Open

Register plugin beans before Spring Boot auto-configuration by retiming doWithSpring#15934
codeconsole wants to merge 18 commits into
apache:8.0.xfrom
codeconsole:feat/plugin-beans-before-autoconfig

Conversation

@codeconsole

@codeconsole codeconsole commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Supersedes #15755, implementing the design agreed in its review discussion: instead of adding a new lifecycle method, the existing plugin entry point is retimed. Completes the follow-up scoped out of #15409 ("we could move plugins completely to the autoconfiguration workflow") and delivers the visible half of #14915.

  • doWithSpring/doWithRuntimeConfiguration now runs before Spring Boot auto-configuration. An ApplicationContextInitializer adds a manually-registered BeanDefinitionRegistryPostProcessor, which Spring runs ahead of ConfigurationClassPostProcessor. Plugin beans are in the registry before auto-configuration conditions are evaluated, so Boot's @ConditionalOnMissingBean beans defer to plugin beans — no more overriding Boot beans after the fact.
  • A single plugin manager and GrailsApplication, built once in the early phase from PluginDiscovery (the 7.x bootstrap infrastructure) and promoted as singletons that GrailsApplicationPostProcessor reuses — so plugins are loaded by one manager pass rather than by a throwaway pass followed by the real one (the superseded Register plugin beans before Spring Boot auto-configuration (doWithSpringBeforeAutoConfiguration) #15755 approach). (Grails still constructs each plugin's GrailsClass reference instance plus its real instance, as it always has.)
  • Artefact discovery moves into the same early phase (resolving application classes via the same classes() path as before, honoring the compile-time packageNames() injection), so doWithSpring closures that iterate artefacts — controllers, services, interceptors — work unmodified.
  • BeanRegistrar is the new native registration API for plugins: beanRegistrar() on GrailsApplicationLifeCycle/Plugin, applied pre-refresh through Spring's own BeanRegistryAdapter (the mechanism GenericApplicationContext.register(BeanRegistrar...) uses). It is a functional interface, so a plugin can return a closure coerced with as BeanRegistrar — no separate class needed. AOT-friendly by design.
  • The bean DSL entry point is deprecated (@Deprecated(since = '8.0') on doWithSpring) pointing at beanRegistrar(). Deprecated DSL closures keep draining through the existing RuntimeSpringConfiguration path — just earlier — until removal. The DSL internals are deliberately not reimplemented on BeanRegistry: the DSL supports constructs with no clean equivalent (parent/abstract beans, late-resolved references, BeanConfiguration post-processing), the plumbing is invisible to users, and that translation work would be deleted at DSL removal anyway.

resources.groovy/resources.xml and the application class's own doWithSpring drain where they always did, so application beans still override plugin beans under the default allow-bean-definition-overriding=true. The one behavioral edge — apps explicitly setting it false now get BeanDefinitionOverrideException where the old single-drain merge was silent — is documented in the upgrade notes.

Regressions found and fixed by verification

The retiming sweep caught two real breakages, both fixed generally in this PR:

  1. OSIV silently disabled (differential-tested: passes on 8.0.x, failed on the branch before the fix): the Hibernate/Mongo datastore initializers detected "web application" via containsBeanDefinition('dispatcherServlet') — an auto-configuration bean, always absent at the early drain. AbstractDatastoreInitializer.isWebApplicationRegistry() now also does a registration-order-independent WebApplicationContext type check. Follow-up: Neo4jDataStoreSpringInitializer needs the identical one-line change in its own build (separate PR).
  2. Controllers in sibling packages lost their beans: early artefact discovery initially scanned only the Application class's package, missing the injected packageNames() override. Fixed by resolving application classes through classes() for exact parity with the previous behavior.

Verification

All green, zero failures across grails-core, grails-test-suite-uber, grails-test-suite-web, grails-gsp, grails-controllers, grails-web-boot, sitemesh3, mongodb-core (testcontainers), hibernate5/7, plus the full hibernate7 app1:integrationTest and app1:integrationTest functional suites.

Test coverage includes: ordering specs proving a plugin doWithSpring bean and a plugin beanRegistrar() bean each win a @ConditionalOnMissingBean race (with a no-early-phase control) and that the beanRegistrar() closure form works; app-over-plugin override under the default settings, and a BeanDefinitionOverrideException when overriding is disabled; a single-manager-pass check (no throwaway extra load); the initializing flag resetting when the early phase throws; an end-to-end app3 integration test exercising both doWithSpring and beanRegistrar() in a real booted application; Hibernate and MongoDB OSIV registration; and the actuator /env and /health endpoints serializing to JSON. Neo4j's suite and the MongoDB example apps run in their dedicated CI jobs (independent Gradle build / require a Mongo instance); noted above.

Docs: new upgrade-notes section covering the retiming, the override edge, and the DSL deprecation; a "what's new" entry; the plugin guide's runtime-configuration page documents beanRegistrar() first (concise closure form) with doWithSpring as the deprecated-but-working alternative.

Related: #15755 (superseded), #15409 / #15408 (7.x groundwork), #14915 (bean DSL deprecation ticket), #13863 / #15916 (independent extractions from #15755).

Plugin bean definitions previously registered through
GrailsApplicationPostProcessor, a registry-discovered
BeanDefinitionRegistryPostProcessor that runs after
ConfigurationClassPostProcessor has expanded auto-configurations, so
plugin beans had to override Boot's auto-configured defaults. Register
them ahead of auto-configuration instead so that Boot's
@ConditionalOnMissingBean guards back off in favour of plugin beans.

GrailsPluginLifecycleInitializer (registered via spring.factories) adds
GrailsEarlyPluginRegistrationPostProcessor with
addBeanFactoryPostProcessor; manually-added registry post-processors
are guaranteed to run before registry-discovered ones, including
ConfigurationClassPostProcessor. The early phase builds the one true
GrailsApplication and DefaultGrailsPluginManager from the promoted
PluginDiscovery singleton, performs artefact discovery (required
because core plugins iterate artefacts inside doWithSpring), drains
doWithSpring/doWithRuntimeConfiguration into the registry, and promotes
both singletons plus a completion marker.

Application classes are resolved for early artefact discovery from the
source classes GrailsApp now stashes in the context, falling back to
GrailsApplicationClass bean definitions already present in the registry
when the application is started through a plain SpringApplication. The
default scanning logic is extracted from GrailsAutoConfiguration.classes()
into ApplicationClassScanner so there is a single implementation.

GrailsApplicationPostProcessor reuses the promoted grailsApplication
(adopting the application class via the new
DefaultGrailsApplication.setApplicationClass) and pluginManager, skips
loadPlugins and the initialization sequence when the early phase ran,
and no longer re-drains plugin runtime configuration; resources.groovy,
resources.xml and the application's own doWithSpring still drain in
GrailsApplicationPostProcessor as before. Plugins and the plugin
manager are constructed exactly once.

DevelopmentModeWatchSpec now supplies its watched plugin through the
real PluginDiscovery bootstrap path instead of defining a second plugin
manager bean, which would be ambiguous with the promoted singleton.
Introduce beanRegistrar() on GrailsApplicationLifeCycle (interface
default returning null) and grails.plugins.Plugin as the modern,
Spring-native way for plugins and applications to register beans, built
on Spring Framework 7's org.springframework.beans.factory.BeanRegistrar
(register(BeanRegistry, Environment)).

GrailsPlugin gains a default getBeanRegistrar() accessor, implemented
by DefaultGrailsPlugin to expose the plugin instance's registrar,
mirroring how doWithSpring reaches the instance today. The early plugin
registration phase applies each enabled plugin's registrar to the bean
definition registry in plugin order, immediately after the doWithSpring
drain and still ahead of Spring Boot auto-configuration, using
BeanRegistryAdapter exactly as GenericApplicationContext.register does.
The application class's registrar drains in
GrailsApplicationPostProcessor at the same point as the application
doWithSpring closure.

doWithSpring() on GrailsApplicationLifeCycle and Plugin is deprecated
since 8.0; the bean builder DSL continues to work unchanged.

Documents the retimed lifecycle and the new API in the 8.0 upgrade
guide and rewrites the plugin runtime-configuration guide around
beanRegistrar() with doWithSpring as the deprecated alternative.
Extends the ordering spec to prove a registrar bean wins a
@ConditionalOnMissingBean race and that both hooks coexist on one
plugin.
…ase coverage

Adds an ordering spec feature proving that under the default
spring.main.allow-bean-definition-overriding=true an application
doWithSpring bean still replaces a plugin bean of the same name after
the retimed lifecycle split the two registrations.

Ports the end-to-end integration coverage to app3: the loadafter plugin
registers a probe bean in plain doWithSpring and the application
defines a @ConditionalOnMissingBean default for the same name;
PluginBeansBeforeAutoConfigurationSpec asserts the plugin's bean wins
through real plugin discovery and a real boot.

Extends the 8.0 upgrade notes with the bean-definition-overriding edge
case (apps explicitly disabling overriding now see
BeanDefinitionOverrideException where shadowing was previously silent)
and the pre-auto-configuration registry contract for plugins that
inspect the registry inside doWithSpring.
The early plugin registration phase scanned each application source
class's own package, but the Grails compiler injects a packageNames()
override into the application class that lists every project package.
Artefacts in packages other than the application class's own (e.g. a
controller in a sibling package) were therefore discovered only later
by GrailsApplicationPostProcessor, after the plugins' doWithSpring had
already run — leaving controllers without beans and failing requests
with NoSuchBeanDefinitionException.

Early artefact discovery now resolves the application classes exactly
the way GrailsApplicationPostProcessor does: classes() invoked on a
GrailsAutoConfiguration instance (created solely to compute the scan),
honoring the injected packageNames() as well as user overrides of
classes(), packageNames() and limitScanningToApplication(). Sources
that are not application classes no longer contribute artefacts,
matching the previous lifecycle.

Verified by UriMatchingInterceptorFunctionalSpec in the hibernate5 and
hibernate7 app1 test examples, which exercises a controller and an
interceptor living outside the application class's package.
The GORM initializers decided whether to register the open-session-in-
view interceptor by checking for the dispatcherServlet bean definition.
That bean is registered by Spring Boot auto-configuration, so with
plugin beans now draining before auto-configuration the check always
returned false and the interceptor silently stopped registering in web
applications (verified: openSessionInViewInterceptor present on 8.0.x,
absent on this branch before this fix).

AbstractDatastoreInitializer gains isWebApplicationRegistry(), which
still honours a dispatcherServlet definition when present but otherwise
checks whether the registry is the web application context itself — a
signal independent of auto-configuration ordering. The Hibernate 5 and
Hibernate 7 initializers and the MongoDB initializer use it in place of
the bean-definition check.

Note: Neo4jDataStoreSpringInitializer (grails-data-neo4j, a separate
Gradle build) has the identical dispatcherServlet check and needs the
same one-line change once it builds against this grails-datamapping-core.

New OpenSessionInViewSpec integration tests in the hibernate5 and
hibernate7 app1 examples pin the interceptor's registration.
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.34555% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.4471%. Comparing base (acc277d) to head (e737052).
⚠️ Report is 45 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
.../boot/config/GrailsApplicationPostProcessor.groovy 69.7674% 4 Missing and 9 partials ⚠️
...ig/GrailsEarlyPluginRegistrationPostProcessor.java 86.5979% 6 Missing and 7 partials ⚠️
...ails/boot/config/ApplicationArtefactScanner.groovy 46.1538% 6 Missing and 1 partial ⚠️
...gorm/bootstrap/AbstractDatastoreInitializer.groovy 25.0000% 3 Missing and 3 partials ⚠️
...-core/src/main/groovy/grails/boot/GrailsApp.groovy 58.3333% 1 Missing and 4 partials ⚠️
.../grails/boot/config/GrailsAutoConfiguration.groovy 25.0000% 3 Missing ⚠️
...e/src/main/groovy/grails/plugins/GrailsPlugin.java 0.0000% 1 Missing ⚠️
...bootstrap/MongoDbDataStoreSpringInitializer.groovy 0.0000% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15934        +/-   ##
==================================================
+ Coverage     49.3593%   49.4471%   +0.0878%     
- Complexity      16777      16846        +69     
==================================================
  Files            1986       1991         +5     
  Lines           93336      93504       +168     
  Branches        16337      16375        +38     
==================================================
+ Hits            46070      46235       +165     
+ Misses          40140      40127        -13     
- Partials         7126       7142        +16     
Files with missing lines Coverage Δ
...ins/web/controllers/ControllersGrailsPlugin.groovy 35.5556% <ø> (-4.4444%) ⬇️
.../boot/config/GrailsPluginLifecycleInitializer.java 100.0000% <100.0000%> (ø)
...n/groovy/grails/core/DefaultGrailsApplication.java 64.9660% <100.0000%> (+0.8281%) ⬆️
...oovy/grails/core/GrailsApplicationLifeCycle.groovy 100.0000% <100.0000%> (ø)
...-core/src/main/groovy/grails/plugins/Plugin.groovy 60.0000% <100.0000%> (+2.1053%) ⬆️
...groovy/org/grails/plugins/DefaultGrailsPlugin.java 50.6250% <100.0000%> (+2.6755%) ⬆️
...e/src/main/groovy/grails/plugins/GrailsPlugin.java 0.0000% <0.0000%> (ø)
...bootstrap/MongoDbDataStoreSpringInitializer.groovy 46.4286% <0.0000%> (ø)
.../grails/boot/config/GrailsAutoConfiguration.groovy 61.5385% <25.0000%> (-7.4271%) ⬇️
...-core/src/main/groovy/grails/boot/GrailsApp.groovy 48.3696% <58.3333%> (+2.4393%) ⬆️
... and 4 more

... and 14 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.

@jdaugherty

jdaugherty commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

I merged the latest 8.x branch changes, apologies for the test failures initially. They were due to merging up 7 -> 8 + the vulnerability fixes + the spring boot 4 vs 3 mismatches. Will review this after the tests are run. Any failures related to the merge, I'll try to fix and merge into this PR

The controllers plugin registered its own RequestMappingHandlerMapping
(annotationHandlerMapping) and RequestMappingHandlerAdapter
(annotationHandlerAdapter) to support @controller annotated beans, a
remnant of the pre-Boot era. In a Grails application Spring's MVC
configuration always provides the fully configured
requestMappingHandlerMapping and requestMappingHandlerAdapter, so the
Grails pair was never consulted: the mapping sorts after Spring's
order-0 mapping, and the adapter tied with Spring's on order and lost
by bean registration order.

With plugin beans now registering before auto-configuration, the
registration order of the two adapters flipped and the DispatcherServlet
began selecting the bare Grails adapter — which lacks the JSON message
converters — for every annotated handler method, including the actuator
endpoints. Requests to /actuator/env and /actuator/health failed with
HttpMessageNotWritableException: 'No converter for
EnvironmentDescriptor with preset Content-Type null', surfacing as the
issue-10279 functional test regression.

Removing the never-consulted duplicates restores the previous effective
behavior regardless of registration order. The localeChangeInterceptor
wiring on the removed mapping was equally unreachable and had no
effect. Documented in the 8.0 upgrade notes.

Verified by ActuatorEnvClosureSpec (passes again, differential-checked:
passes on 8.0.x, failed on this branch before this change) and the full
functional test selection.
…before-autoconfig

# Conflicts:
#	grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@codeconsole codeconsole requested review from jamesfredley, jdaugherty, matrei and sbglasius and removed request for jamesfredley and jdaugherty July 8, 2026 15:15

@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 made a first pass at this review. I think this is the right direction overall.

Can we also add a test example that explicitly exercises the new registrar functionality?

Comment thread grails-core/src/main/groovy/grails/boot/config/ApplicationClassScanner.groovy Outdated
Comment thread grails-core/src/main/groovy/grails/boot/GrailsApp.groovy Outdated
Comment thread grails-core/src/main/groovy/grails/plugins/Plugin.groovy Outdated
Comment thread grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc Outdated
Comment thread grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc Outdated
- Rename ApplicationClassScanner to ApplicationArtefactScanner to reflect
  that it scans the application's artefact classes, not the application class
- Apply plugin beanRegistrar()s on the GrailsApplicationPostProcessor
  fallback path so the new API behaves identically in contexts not booted
  through GrailsApp
- Reset the initializing flag if the early registration phase throws, so a
  failed context does not leak the system property to later contexts
- Narrow the application-scan catch to Exception | LinkageError
- Resolve String application sources to classes, and recover an application
  class from the registry when the stash holds none
- Drop the consumed source-classes stash singleton
- Guard setApplicationClass as set-once
- Strengthen the doWithSpring deprecation wording
- Replace the linear isArtefact scan with a Set lookup
- Add a trailing newline to spring.factories
- Docs: lead the beanRegistrar examples with the concise closure form, add
  a what's new entry, and note the double application-class instantiation
- Tests: closure-coercion registrar, setApplicationClass set-once, and an
  app3 beanRegistrar() end-to-end example
@codeconsole

Copy link
Copy Markdown
Contributor Author

Can we also add a test example that explicitly exercises the new registrar functionality?

Added — the loadafter plugin now also registers a bean through beanRegistrar() (using the concise closure-coercion form), app3's Application declares a matching @ConditionalOnMissingBean default, and PluginBeansBeforeAutoConfigurationSpec asserts the registrar's bean wins in a real boot. It passes alongside the existing doWithSpring case. There's also unit coverage in EarlyPluginRegistrationOrderingSpec (a beanRegistrar() bean winning a @ConditionalOnMissingBean race, and the coerced-closure form).

@codeconsole codeconsole requested a review from jdaugherty July 8, 2026 19:57
@testlens-app

This comment has been minimized.

@codeconsole codeconsole requested a review from borinquenkid July 8, 2026 23:21
@borinquenkid

Copy link
Copy Markdown
Member

Note: This comment was generated by an AI coding assistant (Claude), directed by a human reviewer to verify that this PR's tests actually cover the situations described in its own description and commit messages. Findings below are grounded in specific file/line citations from the diff and the 8.0.x base branch, not inference.

Overall, the core architectural claims are well covered: EarlyPluginRegistrationOrderingSpec and app3's PluginBeansBeforeAutoConfigurationSpec genuinely prove plugin doWithSpring/beanRegistrar() beans beat @ConditionalOnMissingBean Boot defaults, at both unit and true end-to-end level (confirmed app3 actually depends on the loadafter plugin). The two headline regressions — "OSIV silently disabled" and "sibling-package controllers losing beans" — are also genuinely pinned: OpenSessionInViewSpec in both hibernate5/app1 and hibernate7/app1, and the pre-existing UriMatchingInterceptorFunctionalSpec, which really does exercise a controller (demo.InterceptorDemoController) living outside the Application class's own package (functionaltests).

A few gaps, in rough priority order:

  1. Mongo OSIV fix is untested. MongoDbDataStoreSpringInitializer gets the identical isWebApplicationRegistry() swap that fixed the Hibernate OSIV regression (MongoDbDataStoreSpringInitializer.groovy:129-130), but no test anywhere asserts mongoOpenSessionInViewInterceptor registers in a web app — the same bug class just proven to silently break Hibernate.
  2. /actuator/health half of the actuator regression is untested. Commit dd5ac787 states both /actuator/env and /actuator/health failed with HttpMessageNotWritableException, but ActuatorEnvClosureSpec only requests /actuator/env (lines 40, 48).
  3. BeanDefinitionOverrideException edge case (new upgrade-notes section) has no test. The closest existing spec, GrailsAppContextOverridingSpec, only checks that allow-bean-definition-overriding=false propagates to the bean factory flag — it never registers a colliding plugin+app bean pair or asserts the exception.
  4. "Built/instantiated exactly once" claim is checked by existence, not count. EarlyPluginRegistrationOrderingSpec asserts the grailsApplication/pluginManager singletons exist (or don't, in the control case), but nothing spies on construction to prove single instantiation.
  5. Flag-reset-on-exception path is unexercised. GrailsEarlyPluginRegistrationPostProcessor's catch (RuntimeException | Error e) { Environment.setInitializing(false); } branch has no test that makes the early phase throw and then checks the flag resets — only the successful-refresh reset is asserted.

Flagging 1-2 as the highest priority since they mirror a regression class this PR already proved happened once.

jdaugherty pushed a commit that referenced this pull request Jul 9, 2026
Neo4jDataStoreSpringInitializer gated the open-session-in-view
interceptor on the dispatcherServlet bean definition. That bean is
registered by Spring Boot auto-configuration, so under the Grails 8
lifecycle (plugin beans drain before auto-configuration, #15934)
the check always returned false and the interceptor silently stopped
registering in web applications.

The check still honours a dispatcherServlet definition when present but
otherwise tests whether the registry is the web application context
itself, a signal independent of auto-configuration ordering. The logic
mirrors AbstractDatastoreInitializer.isWebApplicationRegistry() added to
grails-datamapping-core in #15934 (where it is exercised by the
hibernate5/hibernate7 OpenSessionInViewSpec integration tests); it is
inlined here because this build resolves grails-datamapping-core from
published artifacts that do not yet contain the shared method, and the
override becomes removable once it does.
- Assert the actuator /health endpoint also serializes to JSON (the same
  converter path the /env regression exposed)
- Add a MongoDB open-session-in-view registration test mirroring the
  Hibernate one, covering the identical isWebApplicationRegistry() fix
- Cover the bean-definition-overriding-disabled edge with a colliding
  plugin+application bean pair that must throw
- Prove the early phase resets the initializing flag when it throws
- Prove the early phase loads plugins with a single manager pass, and
  correct the upgrade note: Grails always constructs a plugin's reference
  and real instance, so the guarantee is one manager pass, not a single
  plugin instantiation
@codeconsole

Copy link
Copy Markdown
Contributor Author

Thanks — this audit was accurate and genuinely useful; all five gaps are now addressed in e737052.

  1. Mongo OSIV — Added OpenSessionInViewSpec to grails-test-examples/mongodb/base, mirroring the Hibernate one, asserting mongoOpenSessionInViewInterceptor registers in a web app. It exercises the same isWebApplicationRegistry() fix. I couldn't run it green locally — the entire mongodb/base integration suite can't reach Mongo in my environment (the existing BookSpec fails with the identical MongoTimeoutException against a local container), so it will run in the MongoDB CI job alongside the others.
  2. /actuator/health — Added a /actuator/health assertion to ActuatorEnvClosureSpec (passes locally). It goes through the same MVC JSON-converter path the /env regression exposed.
  3. BeanDefinitionOverrideException edge — Added a test that sets allow-bean-definition-overriding=false, registers a colliding plugin+application bean pair, and asserts the exception (passes).
  4. "Built exactly once" — Good catch, and it surfaced a real inaccuracy: adding a construction counter showed a plugin is instantiated twice, not once — but that's pre-existing Grails behavior (DefaultGrailsPlugin builds a GrailsPluginClass reference instance and the real instance), unrelated to this PR. What the retiming actually guarantees is a single plugin-manager pass rather than a throwaway pass plus the real one (the rejected earlier approach, which would have multiplied that count). I reworded the test to assert exactly that (early-phase count equals a single bare loadPlugins()), and corrected the upgrade note that claimed "plugins are instantiated once."
  5. Flag-reset-on-exception — Added a test with a plugin whose doWithSpring throws during the early phase, asserting Environment.isInitializing() is false afterward (passes) — directly exercising the catch branch.

grails-core full suite, the ordering spec (all 9 cases), and the issue-10279 actuator spec pass locally; the mongo spec is the only one gated on CI infrastructure.

The Mongo OSIV regression coverage added in e737052 lives in
grails-test-examples/mongodb/base, which requires a real Mongo already
listening on localhost:27017 and can't be verified without that
external dependency. grails-data-mongodb/core already has
Testcontainers wired up via AutoStartedMongoSpec (auto-starts Mongo,
needs only Docker), so add MongoOpenSessionInViewSpec there, exercising
MongoDbDataStoreSpringInitializer.isWebApplicationRegistry() directly
against a GenericWebApplicationContext with no dispatcherServlet bean
definition present yet — the exact ordering the retimed plugin
lifecycle now requires — plus a non-web control case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@borinquenkid

Copy link
Copy Markdown
Member

Note: This comment was generated by an AI coding assistant (Claude), directed by a human reviewer, as a follow-up to the coverage audit above.

All 5 gaps from the audit are addressed as of e737052, and confirmed locally:

  1. Mongo OSIVOpenSessionInViewSpec (mongodb/base) added.
  2. /actuator/health — assertion added to ActuatorEnvClosureSpec, passes locally.
  3. BeanDefinitionOverrideException edge — new test in EarlyPluginRegistrationOrderingSpec, passes locally.
  4. "Built exactly once" — reworded to assert a single manager pass (not a throwaway-then-real double pass); the original wording was corrected in the upgrade notes since a plugin is legitimately instantiated twice (reference GrailsClass + real instance, pre-existing behavior). Passes locally.
  5. Flag-reset-on-exception — new test with a throwing doWithSpring, asserts Environment.isInitializing() resets. Passes locally.

On top of that, d054328fe1 adds a second, unit-testable version of the Mongo OSIV coverage in grails-data-mongodb/core (MongoOpenSessionInViewSpec), alongside the existing mongodb/base functional spec. The functional spec depends on an external Mongo already listening on localhost:27017 and can't be verified without that; this one uses the AutoStartedMongoSpec base already in this module (Testcontainers' MongoDBContainer), so it only needs Docker. It exercises MongoDbDataStoreSpringInitializer against a real GenericWebApplicationContext with no dispatcherServlet bean definition present — the exact registration-order-independent scenario isWebApplicationRegistry() was introduced for — plus a non-web control case. Both new tests pass locally, and the full grails-data-mongodb-core suite and codeStyle are clean.

@borinquenkid borinquenkid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You did all the fixes required, we added the mongo coverage

…d check

The mongodb/base example does not register mongoOpenSessionInViewInterceptor
— the open-session-in-view class lives in grails-datastore-web, which that
app does not depend on — so the integration test asserted a bean that is
never present and failed in the MongoDB CI job.

Cover the actual fix instead with a unit test of
AbstractDatastoreInitializer.isWebApplicationRegistry(), the shared method
the Hibernate and MongoDB initializers both use. The web-application-context
branch (which needs spring-web) remains covered end-to-end by the Hibernate
open-session-in-view integration tests.
…fig' into feat/plugin-beans-before-autoconfig
@codeconsole

Copy link
Copy Markdown
Contributor Author

Correcting my Mongo OSIV test (it broke the MongoDB Functional Tests job): the OpenSessionInViewSpec I added to grails-test-examples/mongodb/base asserted a bean that app never registers — the Mongo OSIV class lives in grails-datastore-web, which mongodb/base doesn't depend on — so it failed against real Mongo. I've removed it.

@wbduque's MongoOpenSessionInViewSpec (grails-data-mongodb/core, Testcontainers via AutoStartedMongoSpec) is the right coverage here — it exercises MongoDbDataStoreSpringInitializer.isWebApplicationRegistry() against a real GenericWebApplicationContext with no dispatcherServlet yet, plus a non-web control. Thanks for that; merged it in.

I also kept a small complementary unit test (AbstractDatastoreInitializerWebApplicationSpec) covering the shared method's other branches — null registry and the legacy dispatcherServlet signal — with no Docker needed. Between the two, plus the Hibernate OSIV integration tests, the isWebApplicationRegistry() fix is covered on all branches.

…before-autoconfig

# Conflicts:
#	grails-core/src/main/groovy/grails/plugins/Plugin.groovy
…fig' into feat/plugin-beans-before-autoconfig
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants