Skip to content

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

Open
jamesfredley wants to merge 16 commits into7.0.xfrom
micronaut-fixes-2
Open

fix: configure Micronaut annotation processor and CLASSIC boot loader automatically#15411
jamesfredley wants to merge 16 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, and resolves a Forge CI failure caused by spring-boot-devtools incompatibility:

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.

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 in GrailsGradlePlugin 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
test: add integration tests for Micronaut bean type registration New MicronautBeanTypesSpec covering Java @singleton, Groovy @Factory/@bean, and @ConfigurationProperties bean registration
docs: document Micronaut annotation processor and CLASSIC loader in upgrade guide Upgrade guide notes warning against manual annotation processor setup
fix: exclude Spring Boot DevTools for Micronaut apps in Forge Prevents devtools auto-application for Micronaut apps, fixing Forge CI
test: add bean duplication and cross-context identity tests Verifies no bean duplication and shared singleton identity across Spring/Micronaut contexts

Test Coverage

30 integration tests passing in grails-test-examples-micronaut:

  • BeanInjectionServiceSpec (3 tests) - existing Micronaut bean injection tests
  • MicronautBeanTypesSpec (5 tests) - Java @singleton, @Factory/@bean, @ConfigurationProperties, singleton identity checks
  • MicronautContextSpec (6 tests) - context bridge, lifecycle, and cross-context bean lookup tests
  • MicronautQualifierSpec (7 tests) - qualifier/named bean injection and collection tests
  • MicronautBeanDuplicationSpec (9 tests) - NEW - bean count assertions, cross-context singleton identity, no-duplication guards

4 Forge tests passing in SpringBootDevToolsSpec:

  • 3 existing feature/dependency/exclusivity tests
  • 1 NEW - verifies devtools is not applied when grails-micronaut is selected

New test bean types added:

Bean Type Registration Path
JavaSingletonService Java class @Singleton via annotation processor
FactoryCreatedService Groovy POJO @Factory/@Bean via AST transform
ServiceFactory Groovy factory @Factory with @Singleton method
AppConfig Groovy config @ConfigurationProperties('app') bound from application.yml

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 ✅ 30/30 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.

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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>
@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.


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())
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can use the previously mentioned https://github.com/cjstehno/ersatz server to mock the response so the client has a valid endpoint

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

2 participants

Comments