fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411
fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411jdaugherty merged 25 commits into7.0.xfrom
Conversation
…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>
|
@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>
There was a problem hiding this comment.
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/bootWarloader implementation toCLASSICwhengrails-micronautis 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.
grails-test-examples/micronaut/grails-app/controllers/micronaut/MicronautTestController.groovy
Outdated
Show resolved
Hide resolved
...-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautBeanTypesSpec.groovy
Show resolved
Hide resolved
grails-test-examples/micronaut/src/main/java/bean/injection/JavaSingletonService.java
Outdated
Show resolved
Hide resolved
- 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>
|
@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. |
|
@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. |
|
@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. |
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
Outdated
Show resolved
Hide resolved
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>
|
@jdaugherty Added a declarative Results:
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. |
...amples/micronaut/src/integration-test/groovy/micronaut/MicronautDeclarativeClientSpec.groovy
Show resolved
Hide resolved
…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>
|
One of the issues I have seen was using a grails-plugin with Micronaut beans ( |
That would test the scenario I am worried about. Would you push updates to this PR @sbglasius |
...ge-core/src/main/java/org/grails/forge/feature/build/gradle/templates/buildGradle.rocker.raw
Outdated
Show resolved
Hide resolved
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
Outdated
Show resolved
Hide resolved
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
Outdated
Show resolved
Hide resolved
Co-authored-by: Mattias Reichel <matrei@apache.org>
Co-authored-by: Mattias Reichel <matrei@apache.org>
|
@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>
|
@sbglasius Added in 6b59b6f - created a
All 5 pass. |
|
@jdaugherty All items addressed in 6b59b6f:
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>
jdaugherty
left a comment
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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>
Summary
Fixes two Micronaut integration bugs by automating configuration that previously required manual
build.gradlesetup, resolves a Forge CI failure caused by spring-boot-devtools incompatibility, and adds a Groovy-only test module proving annotation processors are auto-applied:java -jarbroken for grails-micronaut apps due to Spring Boot 3.2+ default loader incompatibility@Singletonbeans not registered when using Groovy incremental compilation (missing annotation processor)grails-micronautworks without explicitannotationProcessordependencies or Java source filesProblem
Issue #15207 -
java -jarfails withNoClassDefFoundErrorSpring Boot 3.2+ changed the default
LoaderImplementationfromCLASSICto a new implementation. The new loader is incompatible with Micronaut-Spring's classpath scanning mechanism (MicronautImportRegistrar), causingNoClassDefFoundErrorat runtime when running a packaged JAR/WAR viajava -jar.Issue #15211 - Java
@Singletonbeans silently ignoredGroovy sources use
micronaut-inject-groovyAST transforms to generateBeanDefinitionReferenceclasses. However, Java sources in a Grails project require themicronaut-inject-javaannotation processor on theannotationProcessorconfiguration. Without it, Java beans annotated with@Singleton,@Factory, etc. are silently ignored - no compile error, just missing beans at runtime.Solution
GrailsGradlePlugin (
configureMicronaut())Annotation processor - Automatically adds
micronaut-inject-java+jakarta.annotation-apito theannotationProcessorconfiguration, scoped to the Micronaut platform BOM. This only affectscompileJavatasks (Groovy sources continue using AST transforms viacompileOnlyApi).CLASSIC loader - Configures
bootJarandbootWartasks withLoaderImplementation.CLASSICas a convention default (overridable by users). This ensuresjava -jarworks 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 explicitannotationProcessordependencies proves that the Grails Gradle plugin auto-applies the required annotation processors whengrails-micronautis on the classpath. The Gradle output confirms:Micronaut Support Detected for grails-test-examples-micronaut-groovy-only.Forge
bootWarCLASSIC loader configuration to match the existingbootJarconfiguration in Forge-generatedbuild.gradlefiles.SpringBootDevTools.shouldApply()now returnsfalsewhenGrailsMicronautis selected, preventing the DefaultFeature from being auto-applied and triggeringGrailsMicronautValidator's incompatibility check.Housekeeping
GrailsMicronautValidator.java// TODO:with// See:to satisfy Forge checkstyleTodoCommentruleCommits
fix: configure Micronaut annotation processor and CLASSIC loaderfix: add bootWar CLASSIC loader to Forge-generated build.gradlechore: add Apache license header to GrailsMicronautValidatorfix: exclude Spring Boot DevTools for Micronaut apps in Forgedocs: document Micronaut annotation processor and CLASSIC loaderAddress PR review feedbackExhaustive ersatz integration tests for all Micronaut client patternstest: add Groovy-only Micronaut test moduleTest 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)
MicronautErsatzRoundtripSpecMicronautErsatzAdvancedSpecMicronautErsatzPatternSpecMicronautDeclarativeClientSpecBean integration tests (35 tests)
MicronautBeanDuplicationSpecMicronautQualifierSpecMicronautContextSpecMicronautBeanTypesSpecMicronautPluginBeanSpecBeanInjectionServiceSpecgrails-test-examples-micronaut-groovy-only(16 tests)BeanInjectionServiceSpecMicronautContextSpecMicronautQualifierSpecThis module has no Java source files and no
annotationProcessordependencies in itsbuild.gradle. It validates the documentation claim that the Grails Gradle plugin automatically applies the required Micronaut annotation processors whengrails-micronautis present.Micronaut client patterns tested
MicronautTestClientMicronautAdvancedClientMicronautHeaderClientMicronautReactiveClientMicronautPathClientAuthTokenClientFilter+MicronautFilteredClientMicronautRetryableClientErsatz mock server features exercised
Forge tests
4 tests passing in
SpringBootDevToolsSpec- verifies devtools exclusion for Micronaut apps.Build Verification
grails-gradle(plugins)grails-forge(checkstyle)grails-doc(guide)codeStyle(main project)grails-test-examples-micronaut:integrationTestgrails-test-examples-micronaut-groovy-only:integrationTestRemaining Gaps (Out of Scope)
These are known limitations of the current Micronaut integration, not addressed by this PR:
bootRunCLASSIC loader needed (only affects packaged archives)Fixes #15207
Fixes #15211
Fixes #11599