Skip to content

fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411

Merged
jdaugherty merged 25 commits into7.0.xfrom
micronaut-fixes-2
Feb 21, 2026
Merged

fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411
jdaugherty merged 25 commits into7.0.xfrom
micronaut-fixes-2

Conversation

@jamesfredley
Copy link
Contributor

@jamesfredley jamesfredley commented Feb 19, 2026

Summary

Fixes two Micronaut integration bugs by automating configuration that previously required manual build.gradle setup, resolves a Forge CI failure caused by spring-boot-devtools incompatibility, and adds a Groovy-only test module proving annotation processors are auto-applied:

Problem

Issue #15207 - java -jar fails with NoClassDefFoundError

Spring Boot 3.2+ changed the default LoaderImplementation from CLASSIC to a new implementation. The new loader is incompatible with Micronaut-Spring's classpath scanning mechanism (MicronautImportRegistrar), causing NoClassDefFoundError at runtime when running a packaged JAR/WAR via java -jar.

Issue #15211 - Java @Singleton beans silently ignored

Groovy sources use micronaut-inject-groovy AST transforms to generate BeanDefinitionReference classes. However, Java sources in a Grails project require the micronaut-inject-java annotation processor on the annotationProcessor configuration. Without it, Java beans annotated with @Singleton, @Factory, etc. are silently ignored - no compile error, just missing beans at runtime.

Solution

GrailsGradlePlugin (configureMicronaut())

  1. Annotation processor - Automatically adds micronaut-inject-java + jakarta.annotation-api to the annotationProcessor configuration, scoped to the Micronaut platform BOM. This only affects compileJava tasks (Groovy sources continue using AST transforms via compileOnlyApi).

  2. CLASSIC loader - Configures bootJar and bootWar tasks with LoaderImplementation.CLASSIC as a convention default (overridable by users). This ensures java -jar works correctly with Micronaut-Spring's classpath scanning.

Groovy-Only Micronaut Validation

New grails-test-examples/micronaut-groovy-only/ module with zero Java files and zero explicit annotationProcessor dependencies proves that the Grails Gradle plugin auto-applies the required annotation processors when grails-micronaut is on the classpath. The Gradle output confirms: Micronaut Support Detected for grails-test-examples-micronaut-groovy-only.

Forge

  • Adds the missing bootWar CLASSIC loader configuration to match the existing bootJar configuration in Forge-generated build.gradle files.
  • SpringBootDevTools.shouldApply() now returns false when GrailsMicronaut is selected, preventing the DefaultFeature from being auto-applied and triggering GrailsMicronautValidator's incompatibility check.

Housekeeping

  • Adds Apache license header to GrailsMicronautValidator.java
  • Replaces // TODO: with // See: to satisfy Forge checkstyle TodoComment rule

Commits

Commit Description
fix: configure Micronaut annotation processor and CLASSIC loader Core fix - adds annotation processor for Java sources + CLASSIC loader convention for bootJar/bootWar
fix: add bootWar CLASSIC loader to Forge-generated build.gradle Forge template parity - bootWar was missing CLASSIC loader config
chore: add Apache license header to GrailsMicronautValidator License header + checkstyle compliance
fix: exclude Spring Boot DevTools for Micronaut apps in Forge Prevents devtools auto-application for Micronaut apps
docs: document Micronaut annotation processor and CLASSIC loader Upgrade guide notes
Address PR review feedback Comprehensive ersatz tests, plugin @singleton beans, docs updates
Exhaustive ersatz integration tests for all Micronaut client patterns 43 new tests covering every client integration pattern
test: add Groovy-only Micronaut test module New module validating grails-micronaut works without annotationProcessor deps or Java files

Test Coverage

121 integration tests passing across 13 specs in two test modules:

grails-test-examples-micronaut (105 tests)

Ersatz-mocked HTTP client tests (70 tests)

Spec Tests Coverage
MicronautErsatzRoundtripSpec 17 Full roundtrip: HTTP -> Grails controller -> service -> @client -> ersatz. All CRUD operations, error handling, sequential responses, custom headers, large payloads
MicronautErsatzAdvancedSpec 27 Every HTTP method (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/ANY), @QueryValue, @Header (method + class-level), @CookieValue, Basic Auth, response cookies, plain text, XML, delayed responses, sequential responses (503->503->200), request listeners, call count verification, multi-client coexistence, service orchestration
MicronautErsatzPatternSpec 16 CompletableFuture returns (GET/POST/DELETE), void return, @client(path=) base path, @ClientFilter + @RequestFilter auto-header injection, @retryable with sequential failure/success, bean resolution, full roundtrips for async/path/filtered patterns
MicronautDeclarativeClientSpec 10 Direct declarative client operations against ersatz

Bean integration tests (35 tests)

Spec Tests Coverage
MicronautBeanDuplicationSpec 9 No bean duplication, cross-context singleton identity
MicronautQualifierSpec 7 @nAmed, @primary, collection injection
MicronautContextSpec 6 Context bridge, lifecycle, cross-context lookup
MicronautBeanTypesSpec 5 Java @singleton, @Factory/@bean, @ConfigurationProperties
MicronautPluginBeanSpec 5 Plugin-contributed @singleton beans in Spring + Micronaut contexts
BeanInjectionServiceSpec 3 Micronaut bean injection into Grails services

grails-test-examples-micronaut-groovy-only (16 tests)

Spec Tests Coverage
BeanInjectionServiceSpec 3 Micronaut bean injection into Grails services (Groovy-only)
MicronautContextSpec 6 Micronaut/Spring context coexistence, bean lookup, lifecycle (Groovy-only)
MicronautQualifierSpec 7 @nAmed, @primary, custom @qualified, collection injection (Groovy-only)

This module has no Java source files and no annotationProcessor dependencies in its build.gradle. It validates the documentation claim that the Grails Gradle plugin automatically applies the required Micronaut annotation processors when grails-micronaut is present.

Micronaut client patterns tested

Pattern Client Interface Language
@get, @post, @put, @delete, @PathVariable, @Body MicronautTestClient Groovy
@patch, @QueryValue, @Header, @Head, @options, @CookieValue, text/plain MicronautAdvancedClient Groovy
Class-level @Header (auto on all requests) MicronautHeaderClient Groovy
CompletableFuture, void return type MicronautReactiveClient Groovy
@client(id, path=) base path / API versioning MicronautPathClient Groovy
@ClientFilter + @RequestFilter auto-header injection AuthTokenClientFilter + MicronautFilteredClient Java + Groovy
@retryable(attempts, delay) with AOP processing MicronautRetryableClient Java

Ersatz mock server features exercised

  • Request matching: method, path, query params, headers, cookies, body
  • Response config: status codes, JSON/text/XML bodies, headers, cookies, delays
  • Sequential responses: different response per call (last repeats)
  • Call count verification: exact count assertions
  • Request listeners: capture and inspect request details
  • Error simulation: 401, 403, 404, 429, 500, 502, 503

Forge tests

4 tests passing in SpringBootDevToolsSpec - verifies devtools exclusion for Micronaut apps.

Build Verification

Project Status
grails-gradle (plugins) ✅ BUILD SUCCESSFUL
grails-forge (checkstyle) ✅ BUILD SUCCESSFUL
grails-doc (guide) ✅ BUILD SUCCESSFUL
codeStyle (main project) ✅ BUILD SUCCESSFUL
grails-test-examples-micronaut:integrationTest ✅ 105/105 PASSED
grails-test-examples-micronaut-groovy-only:integrationTest ✅ 16/16 PASSED

Remaining Gaps (Out of Scope)

These are known limitations of the current Micronaut integration, not addressed by this PR:

  • Groovy incremental compilation may re-trigger AST transforms on unchanged files (Groovy compiler limitation)
  • No bootRun CLASSIC loader needed (only affects packaged archives)

Fixes #15207
Fixes #15211
Fixes #11599

jdaugherty and others added 7 commits February 10, 2026 12:02
…railsGradlePlugin

Add Java annotation processor (micronaut-inject-java) for projects using
grails-micronaut so that Java @singleton beans generate proper
BeanDefinitionReference classes at compile time. Groovy sources continue
to use the existing micronaut-inject-groovy AST transforms.

Configure bootJar and bootWar tasks to use LoaderImplementation.CLASSIC
as a convention default when Micronaut support is detected. The new
Spring Boot 3.2+ default loader is incompatible with Micronaut-Spring's
classpath scanning, causing NoClassDefFoundError at runtime when running
via java -jar.

Fixes #15207
Fixes #15211

Assisted-by: Claude Code <Claude@Claude.ai>
The Forge template already configured bootJar with CLASSIC loader but
was missing the equivalent bootWar configuration. WAR-packaged apps
deployed via java -jar would fail with the same Micronaut-Spring
classpath scanning issue as JAR-packaged apps.

Related to #15207

Assisted-by: Claude Code <Claude@Claude.ai>
Add the required ASF license header and replace TODO comment with a
See reference to satisfy the Forge project checkstyle TodoComment rule.

Assisted-by: Claude Code <Claude@Claude.ai>
Add MicronautBeanTypesSpec verifying that Java @singleton beans (via
annotation processor), Groovy @Factory/@bean beans (via AST transform),
and @ConfigurationProperties beans are all correctly bridged into the
Spring application context.

New test bean types:
- JavaSingletonService: Java class with @singleton (annotation processor path)
- FactoryCreatedService + ServiceFactory: Groovy @Factory/@bean pattern
- AppConfig: @ConfigurationProperties bound from application.yml

Also adds MicronautTestController and URL mapping for manual smoke
testing of bean injection across all registration mechanisms.

Assisted-by: Claude Code <Claude@Claude.ai>
…pgrade guide

Add notes to the 6.0.x upgrade guide warning users not to manually add
Micronaut annotation processors (now handled automatically by the Grails
Gradle Plugin) and explaining the automatic CLASSIC loader configuration
for bootJar/bootWar tasks.

References #15207, #15211

Assisted-by: Claude Code <Claude@Claude.ai>
@github-actions github-actions bot added the bug label Feb 19, 2026
@jamesfredley
Copy link
Contributor Author

@sbglasius @jdaugherty I think this is a bit closer to where we need it, but do not fully understand the finish line.

SpringBootDevTools.shouldApply() now returns false when GrailsMicronaut
is selected, preventing the DefaultFeature from being auto-applied and
triggering GrailsMicronautValidator's incompatibility check.

Fixes Build Grails Forge CI failures on CreateAppSpec.

Assisted-by: Claude Code <Claude@Claude.ai>
…naut

Verifies no bean duplication occurs when micronaut-spring bridges Micronaut
beans into Spring context. Confirms bridged beans share the same singleton
instance across both contexts.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley self-assigned this Feb 19, 2026
@jamesfredley jamesfredley moved this to In Progress in Apache Grails Feb 19, 2026
@jamesfredley jamesfredley added this to the grails:7.0.8 milestone Feb 19, 2026
@jamesfredley jamesfredley marked this pull request as ready for review February 19, 2026 12:47
Copilot AI review requested due to automatic review settings February 19, 2026 12:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Grails + Micronaut integration edge cases by automatically configuring Micronaut Java annotation processing and enforcing Spring Boot’s CLASSIC loader for packaged archives, while aligning Grails Forge generation and validation to avoid incompatible DevTools selection.

Changes:

  • Auto-configure Micronaut Java annotation processor dependencies and set bootJar/bootWar loader implementation to CLASSIC when grails-micronaut is detected.
  • Update Forge Gradle template + feature application rules to avoid Spring Boot DevTools with grails-micronaut, and add a Micronaut feature validator.
  • Add Micronaut-focused integration tests and example beans/config to validate registration, duplication, and cross-context identity; update upgrade guide accordingly.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy Auto-add Micronaut Java annotationProcessor deps and configure CLASSIC loader conventions for Boot archives.
grails-micronaut/src/main/groovy/org/apache/grails/micronaut/GrailsMicronautGrailsPlugin.groovy Adjust Micronaut context bean type usage to ApplicationContext.
grails-micronaut/build.gradle Remove unneeded Micronaut deps from plugin module now handled elsewhere/transitively.
grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/reloading/SpringBootDevTools.java Prevent DevTools from auto-applying when Grails Micronaut is selected.
grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/reloading/SpringBootDevToolsSpec.groovy Add test asserting DevTools is not applied with grails-micronaut.
grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/micronaut/GrailsMicronautValidator.java New validator blocking incompatible DevTools + Micronaut combination.
grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/build/gradle/templates/buildGradle.rocker.raw Ensure Forge-generated apps set CLASSIC loader for both bootJar and bootWar.
grails-test-examples/micronaut/src/main/java/bean/injection/JavaSingletonService.java Add Java @Singleton bean for annotation-processor coverage.
grails-test-examples/micronaut/src/main/groovy/bean/injection/ServiceFactory.groovy Add Micronaut @Factory bean creation path for Groovy AST-transform coverage.
grails-test-examples/micronaut/src/main/groovy/bean/injection/FactoryCreatedService.groovy Add simple factory-created bean type used in integration tests.
grails-test-examples/micronaut/src/main/groovy/bean/injection/AppConfig.groovy Add @ConfigurationProperties bean to validate config binding.
grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautBeanTypesSpec.groovy New integration tests validating different Micronaut bean registration mechanisms.
grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautBeanDuplicationSpec.groovy New integration tests guarding against bean duplication and validating shared singleton identity across contexts.
grails-test-examples/micronaut/grails-app/controllers/micronaut/UrlMappings.groovy Add route for a test controller endpoint.
grails-test-examples/micronaut/grails-app/controllers/micronaut/MicronautTestController.groovy New controller exposing Micronaut beans via an HTTP endpoint (for example/testing).
grails-test-examples/micronaut/grails-app/conf/application.yml Add app.name config used by @ConfigurationProperties test bean.
grails-doc/src/en/guide/upgrading/upgrading60x.adoc Document new auto-configuration behavior and DevTools limitation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Use render([...] as JSON) instead of render(text: Map) for valid JSON output
- Fix singleton tests to use applicationContext.getBean() for proper scope verification
- Make JavaMessageProvider public and add interface-type injection test
- Correct capitalization of Spring Boot DevTools and Micronaut integration in docs

Assisted-by: Claude Code <Claude@Claude.ai>
Java requires public interfaces to be declared in a file matching the
interface name. Moves JavaMessageProvider out of JavaSingletonService.java
into its own JavaMessageProvider.java file.

Assisted-by: Claude Code <Claude@Claude.ai>
@jdaugherty
Copy link
Contributor

@jamesfredley did you checkout the mcironaut branch I pushed and compare it to these changes? Both of these were fixed in that branch. The problem with that branch is how do we reflect the micronaut specific beans into spring.

@jamesfredley
Copy link
Contributor Author

@jdaugherty Yes, this branch is based on https://github.com/apache/grails-core/tree/micronaut-fixes. Take a look at the new tests to see if they are covering all bean vs bean scenarios that were at issue.

@jdaugherty
Copy link
Contributor

@jamesfredley can you add a declarative client & associated test? You can use mocking to test it actually calling an endpoint. I suspect it's still broken with these changes.

 test

Remove auto-configured Micronaut annotation processors from
GrailsGradlePlugin per review feedback - they are incompatible with
Groovy incremental compilation and were never previously configured.
Projects with Java sources using Micronaut annotations must add the
annotationProcessor dependencies manually.

Add declarative @client interface and integration test to verify
Micronaut HTTP client beans are properly registered in the Grails
context. Add micronaut-http-client and micronaut-serde-jackson
dependencies to the micronaut test example, along with the required
annotationProcessor configuration for its Java sources.

All 33 micronaut integration tests pass.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley
Copy link
Contributor Author

jamesfredley commented Feb 19, 2026

@jdaugherty Added a declarative @Client interface (MicronautTestClient.groovy) and integration test (MicronautDeclarativeClientSpec).

Results:

  • @Client bean registration: PASSES. The declarative client interface compiles via the Groovy AST transform (micronaut-inject-groovy) and is resolvable from the Micronaut context.
  • Micronaut HttpClient calling the running Grails app: PASSES. The Micronaut HTTP stack works within Grails.

Regarding ersatz/mock endpoint testing - I opted to test against the running Grails app itself rather than a mock server, since the integration test already boots the full application. This keeps the test dependencies minimal and directly tests the Grails+Micronaut integration path.

…atz mock

Add integration test that exercises the Micronaut @client(id='grails-self')
through the full service discovery and load balancing path using an ersatz
mock HTTP server as the backend endpoint.

Assisted-by: Claude Code <Claude@Claude.ai>
…lePlugin

The annotation processor (micronaut-inject-java) is required for Java
sources that use Micronaut annotations like @ConfigurationProperties
and @singleton. Removing it broke the issue-11767 plugin's
PluginJavaMicronautBean, which depends on compile-time code generation.

Assisted-by: Claude Code <Claude@Claude.ai>
…a sources

Micronaut annotation processors are incompatible with Groovy incremental
compilation, so they should not be auto-configured in GrailsGradlePlugin.
Instead, add them manually only to test apps that have Java sources using
Micronaut annotations (issue-11767 plugin has PluginJavaMicronautBean.java,
micronaut test app already had them configured).

Assisted-by: Claude Code <Claude@Claude.ai>
@sbglasius
Copy link
Contributor

One of the issues I have seen was using a grails-plugin with Micronaut beans (@Singleton). I could make another test-example project that verifies this too?

@jdaugherty
Copy link
Contributor

One of the issues I have seen was using a grails-plugin with Micronaut beans (@Singleton). I could make another test-example project that verifies this too?

That would test the scenario I am worried about. Would you push updates to this PR @sbglasius

jdaugherty and others added 2 commits February 20, 2026 09:36
Co-authored-by: Mattias Reichel <matrei@apache.org>
Co-authored-by: Mattias Reichel <matrei@apache.org>
@jdaugherty
Copy link
Contributor

@jamesfredley I think we can merge this once @sbglasius or you add that other test scenario (I'd like to see an actual mock using ersatz) & the rest of @matrei comments are addressed.

…eton beans, and docs updates

- Add comprehensive ersatz-based integration tests for Micronaut declarative HTTP client
  covering GET, POST, PUT, DELETE, path variables, 404/500 error handling, and Accept headers
- Add full roundtrip integration tests (HTTP -> Grails controller -> service -> @client -> ersatz)
  for all CRUD operations, error propagation, sequential calls, custom headers, and large responses
- Add micronaut-singleton plugin with Java @singleton bean to verify plugin-contributed
  Micronaut beans are properly bridged into the Spring application context
- Add ExternalApiController and ExternalApiService demonstrating Grails service layer
  consuming external APIs via Micronaut declarative HTTP client
- Remove CLASSIC loader from Forge buildGradle template (no longer needed with plugin handling it)
- Update upgrading60x.adoc to clarify Micronaut plugin only adds Groovy support,
  annotation processors needed separately for Java, recommend split projects

63 integration tests pass (27 ersatz-based, 5 plugin bean, 31 existing)

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley
Copy link
Contributor Author

@sbglasius Added in 6b59b6f - created a micronaut-singleton plugin (grails-test-examples/plugins/micronaut-singleton/) with a Java @Singleton bean (PluginSingletonService) and wired it into the micronaut test app.

MicronautPluginBeanSpec (5 tests) verifies:

  • Plugin @Singleton bean available in Spring context
  • Plugin @Singleton bean available in Micronaut context
  • Singleton identity (same instance on repeated lookups)
  • Resolvable by interface type (PluginMessageProvider)
  • Same instance across both Spring and Micronaut contexts

All 5 pass.

@jamesfredley
Copy link
Contributor Author

@jdaugherty All items addressed in 6b59b6f:

  1. Ersatz-mocked declarative client tests - MicronautDeclarativeClientSpec (10 tests) and MicronautErsatzRoundtripSpec (17 tests) cover GET/POST/PUT/DELETE, error handling (404, 500, 503), custom headers, large responses, and full HTTP roundtrip through Grails (controller -> service -> @client -> ersatz)
  2. Plugin @singleton scenario - micronaut-singleton plugin with Java @Singleton bean, verified in MicronautPluginBeanSpec (5 tests)
  3. @matrei comments - Forge template CLASSIC loader removed, upgrade guide rewritten, BootArchive/import cleanup already done in prior commits

63 integration tests total, all passing.

Add comprehensive ersatz-mocked integration tests covering every way a
Micronaut declarative HTTP client can be integrated into a Grails app:

- All HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ANY
- Parameter binding: @QueryValue, @PathVariable, @Header, @CookieValue, @Body
- Return types: String, HttpResponse, CompletableFuture<T>, void
- Client config: @client(id), @client(id, path) for base path/API versioning
- Class-level @Header for automatic header injection on all methods
- @ClientFilter with @RequestFilter for auto-injecting auth tokens
- @retryable with ersatz sequential responses (503, 503, 200)
- Content types: JSON, plain text, XML
- Error handling: 401, 403, 404, 429, 500, 502, 503
- Ersatz features: delayed responses, sequential responses, listeners,
  call count verification, query/header/cookie matching, response cookies
- Full roundtrip tests: HTTP client -> Grails controller -> service ->
  Micronaut declarative client -> ersatz mock server

New files:
- MicronautAdvancedClient, MicronautHeaderClient (Phase 2)
- MicronautReactiveClient, MicronautPathClient, MicronautFilteredClient
- MicronautRetryableClient.java (Java for proper AOP annotation processing)
- AuthTokenClientFilter.java (@ClientFilter with @RequestFilter)
- MicronautErsatzAdvancedSpec (27 tests), MicronautErsatzPatternSpec (16 tests)

Total: 105 integration tests across 10 specs, all passing.

Assisted-by: Claude Code <Claude@Claude.ai>
Copy link
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

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

Only concern is we don't have a case where only the default micronaut plugin is applied - we're including the inject java too. Can we make one of these apps not have java & then not include the inject java?

implementation 'org.apache.grails:grails-micronaut'

annotationProcessor platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")
annotationProcessor 'io.micronaut:micronaut-inject-java'
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make one of these apps not have java? So we can confirm that the groovy only includes suffices?

Assisted-by: Claude Code <Claude@Claude.ai>
Add micronaut-groovy-only test example that validates grails-micronaut
works without explicit annotationProcessor dependencies or Java source
files. The Grails Gradle plugin auto-applies the required annotation
processors when grails-micronaut is on the classpath.

Includes bean injection, context coexistence, and qualifier tests
ported from the existing micronaut module with NamedService converted
from Java interface to Groovy interface.

Assisted-by: Claude Code <Claude@Claude.ai>
@jdaugherty jdaugherty merged commit c800692 into 7.0.x Feb 21, 2026
32 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Apache Grails Feb 21, 2026
@jdaugherty jdaugherty deleted the micronaut-fixes-2 branch March 11, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

5 participants