fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411
fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411jamesfredley wants to merge 16 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. |
| // Groovy sources are handled by micronaut-inject-groovy AST transforms (via compileOnlyApi), | ||
| // but Java sources require the Java annotation processor to generate BeanDefinition classes. | ||
| // The annotationProcessor configuration only affects compileJava tasks, not compileGroovy. | ||
| project.logger.info('Adding Micronaut annotationProcessor for Java sources in {}', project.name) |
There was a problem hiding this comment.
The annotation processors are incompatible with groovy incremental compilation and have previously never been configured. I'd argue we shouldn't configure them and document it so that if someone wants it for java, they can have it.
There was a problem hiding this comment.
Agreed. Removed the auto-configured annotation processors. Added a comment in the plugin noting the incompatibility with Groovy incremental compilation and pointing to the docs for manual setup.
The micronaut test example now explicitly configures the annotationProcessor deps in its own build.gradle since it has Java sources (JavaSingletonService.java). All 33 integration tests pass.
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. |
|
|
||
| void "Micronaut HttpClient can reach the running Grails application"() { | ||
| given: "a Micronaut HTTP client targeting the running server" | ||
| def client = HttpClient.create("http://localhost:$serverPort".toURL()) |
There was a problem hiding this comment.
I think you need to actually invoke the declarative client. This isn't going through the load balancing path and from my experience that's the bean that isn't copied to the spring context.
There was a problem hiding this comment.
You can use the previously mentioned https://github.com/cjstehno/ersatz server to mock the response so the client has a valid endpoint
There was a problem hiding this comment.
Done in 782b950. The test now invokes client.index() through the full service discovery and load balancing path. The @Client(id='grails-self') resolves via micronaut.http.services.grails-self.url to an ersatz mock server that returns a known JSON response - the test asserts both the response content and that ersatz received exactly one request via ersatz.verify().
There was a problem hiding this comment.
Good call - used ersatz:4.0.1 (core, not the groovy variant which has a classpath issue). The ersatz server starts on a fixed port (19876) configured in application.yml under the test environment as micronaut.http.services.grails-self.url. All 3 tests pass.
There was a problem hiding this comment.
I'm suggesting we check in such a test so we know when this breaks in the future.
…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>
Summary
Fixes two Micronaut integration bugs by automating configuration that previously required manual
build.gradlesetup, and resolves a Forge CI failure caused by spring-boot-devtools incompatibility: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)Problem
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.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 loader in GrailsGradlePluginfix: add bootWar CLASSIC loader to Forge-generated build.gradlechore: add Apache license header to GrailsMicronautValidatortest: add integration tests for Micronaut bean type registrationMicronautBeanTypesSpeccovering Java @singleton, Groovy @Factory/@bean, and @ConfigurationProperties bean registrationdocs: document Micronaut annotation processor and CLASSIC loader in upgrade guidefix: exclude Spring Boot DevTools for Micronaut apps in Forgetest: add bean duplication and cross-context identity testsTest Coverage
30 integration tests passing in
grails-test-examples-micronaut:BeanInjectionServiceSpec(3 tests) - existing Micronaut bean injection testsMicronautBeanTypesSpec(5 tests) - Java @singleton, @Factory/@bean, @ConfigurationProperties, singleton identity checksMicronautContextSpec(6 tests) - context bridge, lifecycle, and cross-context bean lookup testsMicronautQualifierSpec(7 tests) - qualifier/named bean injection and collection testsMicronautBeanDuplicationSpec(9 tests) - NEW - bean count assertions, cross-context singleton identity, no-duplication guards4 Forge tests passing in
SpringBootDevToolsSpec:New test bean types added:
JavaSingletonService@Singletonvia annotation processorFactoryCreatedService@Factory/@Beanvia AST transformServiceFactory@Factorywith@SingletonmethodAppConfig@ConfigurationProperties('app')bound fromapplication.ymlBuild Verification
grails-gradle(plugins)grails-forge(checkstyle)grails-doc(guide)codeStyle(main project)grails-test-examples-micronaut: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