jamesfredley opened a new pull request, #16101:
URL: https://github.com/apache/grails-core/pull/16101
## Summary
This change restores the Grails 7 lifecycle behavior that made the promoted
`GrailsApplication` available through `Holders` while plugin `doWithSpring`
callbacks execute.
It allows existing Grails 7-era plugins, including audit-logging 6.0.0, to
start under Grails 8 early plugin registration without requiring plugin-side
changes or `legacyCommandSupport`.
The implementation also makes failure rollback ownership-aware so a failed
application context cannot clear or overwrite a newer application published by
another context.
## User-visible failure
Applications using audit-logging 6.0.0 fail during startup on Grails
8.0.0-M5 with this path:
```text
IllegalArgumentException: GrailsApplication not found
at grails.util.Holders.getGrailsApplication
at grails.plugins.auditlogging.AuditLoggingConfigUtils.reloadAuditConfig
at grails.plugins.auditlogging.AuditLoggingGrailsPlugin.doWithSpring
at grails.boot.config.GrailsEarlyPluginRegistrationPostProcessor
```
audit-logging 6.0.0 resets and reloads its configuration from
`doWithSpring`. Its configuration helper resolves the application through
`Holders.grailsApplication`, which was valid during the equivalent Grails 7
plugin lifecycle phase.
Relevant upstream implementation:
-
[`AuditLoggingGrailsPlugin#doWithSpring`](https://github.com/apache/grails-audit-logging-plugin/blob/v6.0.0/plugin/src/main/groovy/grails/plugins/auditlogging/AuditLoggingGrailsPlugin.groovy)
-
[`AuditLoggingConfigUtils#reloadAuditConfig`](https://github.com/apache/grails-audit-logging-plugin/blob/v6.0.0/plugin/src/main/groovy/grails/plugins/auditlogging/AuditLoggingConfigUtils.groovy)
- [`ReflectionUtils` Holder-based application
lookup](https://github.com/apache/grails-audit-logging-plugin/blob/v6.0.0/plugin/src/main/groovy/grails/plugins/auditlogging/ReflectionUtils.groovy)
`legacyCommandSupport` cannot affect this failure because command
compatibility is unrelated to plugin Spring configuration timing.
## Root cause
Commit `a853ddaf6f2aafe7b8e9dfc5e661dab2c37c88e0` from #15934 introduced
`GrailsEarlyPluginRegistrationPostProcessor` so plugin beans are registered
before Spring Boot auto-configuration evaluates `@ConditionalOnMissingBean`
conditions.
That move changed the observable lifecycle order:
1. Grails 8 M5 creates and initializes the promoted
`DefaultGrailsApplication`.
2. The early post-processor calls
`pluginManager.doRuntimeConfiguration(...)`.
3. Plugin `doWithSpring` callbacks execute.
4. Only after all callbacks finish does the post-processor call
`Holders.setGrailsApplication(...)`.
Step 4 is too late for legacy plugins that access the Holder from step 3.
Grails 7 used the opposite order in `GrailsApplicationPostProcessor`: it
assigned `Holders.grailsApplication` before invoking `doRuntimeConfiguration`.
The M5 ordering therefore introduced a backward-compatibility regression even
though the exact Holder timing was a de facto plugin contract rather than a
formally documented API guarantee.
## Implementation
### Publish before plugin runtime configuration
`GrailsEarlyPluginRegistrationPostProcessor` now publishes the fully
initialized, promoted `GrailsApplication` fallback immediately before
`pluginManager.doRuntimeConfiguration(...)`.
This is the narrowest compatibility point:
- The application has already been initialized.
- Artefacts have already been registered.
- Legacy `doWithSpring` callbacks can resolve the same promoted application
that later becomes the context singleton.
- Discovery-strategy precedence remains unchanged from Grails 7.
- Plugin ordering and early bean-definition timing remain unchanged.
### Preserve failure semantics
Publishing earlier means failures can occur while a new application is
visible globally. The post-processor therefore records the exact fallback it
replaces and restores it if early registration fails.
The cleanup catches `Throwable` because Groovy closures can throw checked
exceptions without declaring them at the Java call site. Runtime exceptions and
errors retain their original identity. Checked failures are wrapped in Spring's
`BeanInitializationException` after process-wide state is restored.
`Environment.initializing` is reset on every failure path.
### Make rollback ownership-aware
An unconditional restore is unsafe when application contexts initialize
concurrently:
1. Context A publishes application A.
2. Context B publishes application B.
3. Context A fails.
4. A must not restore its prior value over B.
The private Holder fallback now uses `AtomicReference<GrailsApplication>`:
- Existing reads use `get()`.
- Existing writes and clear operations use `set()`.
- Early publication uses `getAndSet()` to capture the exact overwritten
fallback without invoking discovery strategies.
- Failure rollback uses `compareAndSet(expected, previous)` so only the
context that still owns the fallback may restore it.
Existing public Holder methods retain source and binary compatibility.
## Regression coverage
`EarlyPluginRegistrationOrderingSpec` now exercises the real public startup
path through `GrailsPluginLifecycleInitializer` and
`AnnotationConfigApplicationContext.refresh()`.
Added coverage verifies:
- A Grails 7-style plugin can read the promoted application from `Holders`
inside `doWithSpring`.
- The object exposed through `Holders` is the same application promoted into
the context.
- Runtime failures reset `Environment.initializing` and restore the prior
Holder fallback.
- Fallback exchange and restoration do not invoke application discovery
strategies.
- Checked exceptions from Groovy `doWithSpring` closures are wrapped only
after global state is restored.
- A failed context does not overwrite a newer publisher.
- A real two-thread race between conditional rollback and a concurrent
setter always leaves the newer publication installed.
- Existing early-registration ordering, bean override, single-manager-pass,
and control scenarios remain green.
The first new compatibility test was run before the production change and
reproduced the reported `IllegalArgumentException: GrailsApplication not found`
at `GrailsEarlyPluginRegistrationPostProcessor#doRuntimeConfiguration`. It
passed after the publication order changed.
## Verification
The following checks pass on Java 21 and Gradle 9.6.0:
```text
.\gradlew.bat :grails-core:test --tests
"grails.boot.config.EarlyPluginRegistrationOrderingSpec" --rerun-tasks
--offline --max-workers=7
.\gradlew.bat :grails-core:test --offline --max-workers=7
.\gradlew.bat :grails-core:codeStyle --offline --max-workers=7
```
The focused specification passes all 14 lifecycle scenarios. The complete
`grails-core` test and CLI test tasks pass.
The repository-required aggregate verification was also attempted with:
```text
.\gradlew.bat clean aggregateViolations :grails-test-report:check --continue
--max-workers=7
```
All generated violation summaries report no violations:
- Checkstyle: clean
- CodeNarc: clean
- PMD: clean
- SpotBugs: clean
The aggregate test gate has one unrelated Windows-only failure in
`:grails-test-examples-mail:integrationTest`: `MailServiceSpec.should handle
newlines in text GSP views` expects LF but receives CRLF. Rerunning that single
feature serially with `--rerun-tasks` reproduces the same CRLF mismatch. The
affected `grails-core` module is green and this change does not touch the mail
test example.
## Review evidence
The final cumulative diff was reviewed against `origin/8.0.x` by both
mandatory reviewers:
- Impact-aware Oracle: GREEN
- Codex CLI review: GREEN
The review loop specifically found and drove fixes for checked-exception
cleanup, discovery-strategy side effects, and concurrent rollback ownership
before the branch was pushed.
## Compatibility and scope
This is intentionally a framework compatibility fix rather than an
audit-logging-specific workaround. Other Grails 7-era plugins that access
`Holders.grailsApplication` during `doWithSpring` receive the same restored
lifecycle behavior.
No configuration property, command compatibility mode, dependency, or public
plugin hook changes. No documentation update is needed because this restores
prior behavior rather than introducing a new user-facing option.
--
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]