Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
32ac386
prevent duplicate loading of micronaut beans & fix bootJar task
jdaugherty Nov 19, 2025
37f5114
Merge remote-tracking branch 'origin/7.0.x' into micronaut-fixes
jamesfredley Feb 19, 2026
6fd254b
fix: configure Micronaut annotation processor and CLASSIC loader in G…
jamesfredley Feb 19, 2026
19fa41e
fix: add bootWar CLASSIC loader to Forge-generated build.gradle
jamesfredley Feb 19, 2026
beff44a
chore: add Apache license header to GrailsMicronautValidator
jamesfredley Feb 19, 2026
888fa45
test: add integration tests for Micronaut bean type registration
jamesfredley Feb 19, 2026
6d4dfc9
docs: document Micronaut annotation processor and CLASSIC loader in u…
jamesfredley Feb 19, 2026
fbf50a2
fix: exclude Spring Boot DevTools for Micronaut apps in Forge
jamesfredley Feb 19, 2026
b3062c6
test: add bean duplication and cross-context identity tests for Micro…
jamesfredley Feb 19, 2026
b04d6d5
fix: address review feedback on test correctness and documentation
jamesfredley Feb 19, 2026
4e9d6b9
refactor: extract JavaMessageProvider to its own file
jamesfredley Feb 19, 2026
ae7747a
fix: remove annotation processor auto-config and add declarative @Cli…
jamesfredley Feb 19, 2026
e6e202a
Merge branch '7.0.x' into micronaut-fixes-2
jamesfredley Feb 19, 2026
782b950
test: invoke declarative @Client through load balancing path with ers…
jamesfredley Feb 19, 2026
03d1a51
fix: restore Micronaut annotation processor auto-config in GrailsGrad…
jamesfredley Feb 19, 2026
79cdfc1
fix: move Micronaut annotation processor config to test apps with Jav…
jamesfredley Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions grails-doc/src/en/guide/upgrading/upgrading60x.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,12 @@ Here's an example `gradle.properties` file:
micronautPlatformVersion=4.9.2
----

Please note that due to https://github.com/micronaut-projects/micronaut-spring/issues/769[this issue], Spring Boot Dev tools does not work with the micronaut integration.

IMPORTANT: Do **not** manually add Micronaut annotation processors (such as `micronaut-inject-java`) to your `build.gradle`. The Grails Gradle Plugin automatically configures the correct annotation processor for Java source files and uses Groovy AST transforms (`micronaut-inject-groovy`) for Groovy source files. Manually adding annotation processors may cause duplicate bean definitions or incremental compilation issues (see https://github.com/apache/grails-core/issues/15211[#15211]).

NOTE: The Grails Gradle Plugin automatically configures the Spring Boot `bootJar` and `bootWar` tasks to use the `CLASSIC` loader implementation when `grails-micronaut` is detected. This is required for `java -jar` execution to work correctly with the Micronaut-Spring integration (see https://github.com/apache/grails-core/issues/15207[#15207]). If you have explicitly set `loaderImplementation` in your `build.gradle`, you can remove it as the Grails Gradle Plugin now handles this automatically.

===== 12.5 hibernate-ehcache

The `org.hibernate:hibernate-ehcache` library is no longer provided by the `org.apache.grails:grails-hibernate5` plugin. If
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repositories {
compileJava.options.release = @features.getTargetJdk()

@if (features.contains("jrebel")) {
bootRun {
tasks.named('bootRun') {
dependsOn(generateRebel)
if (project.hasProperty("rebelAgent")) {
jvmArgs(rebelAgent)
Expand All @@ -107,6 +107,17 @@ bootRun {

}

@if (features.contains("grails-micronaut")) {
tasks.named('bootJar') {
loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC
}

tasks.named('bootWar') {
loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC
}

}

@if (features.contains("spock")) {
tasks.withType(Test).configureEach {
useJUnitPlatform()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.grails.forge.feature.micronaut;

import jakarta.inject.Singleton;
import org.grails.forge.application.ApplicationType;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.reloading.SpringBootDevTools;
import org.grails.forge.feature.validation.FeatureValidator;
import org.grails.forge.options.Options;

import java.util.Set;

@Singleton
public class GrailsMicronautValidator implements FeatureValidator {
@Override
public void validatePreProcessing(Options options, ApplicationType applicationType, Set<Feature> features) {
if (features.stream().anyMatch(f -> f instanceof GrailsMicronaut)) {
if (features.stream().anyMatch(f -> (f instanceof SpringBootDevTools))) {
// See: https://github.com/micronaut-projects/micronaut-spring/issues/769
throw new IllegalArgumentException("Spring Boot Dev Tools are not supported with Grails Micronaut");
}
}
}

@Override
public void validatePostProcessing(Options options, ApplicationType applicationType, Set<Feature> features) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.grails.forge.build.dependencies.Scope;
import org.grails.forge.feature.DefaultFeature;
import org.grails.forge.feature.Feature;
import org.grails.forge.feature.micronaut.GrailsMicronaut;
import org.grails.forge.options.Options;

import java.util.Set;
Expand Down Expand Up @@ -61,6 +62,9 @@ public boolean isVisible() {

@Override
public boolean shouldApply(ApplicationType applicationType, Options options, Set<Feature> selectedFeatures) {
if (selectedFeatures.stream().anyMatch(f -> f instanceof GrailsMicronaut)) {
return false;
}
return selectedFeatures.stream().noneMatch(f -> f instanceof ReloadingFeature);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,12 @@ class SpringBootDevToolsSpec extends ApplicationContextSpec implements CommandOu
def ex = thrown(IllegalArgumentException)
ex.message.contains("There can only be one of the following features selected")
}

void "test spring-boot-devtools is not applied when grails-micronaut is selected"() {
when:
final Features features = getFeatures(["grails-micronaut"])

then:
!features.contains("spring-boot-devtools")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ import org.springframework.boot.gradle.dsl.SpringBootExtension
import org.springframework.boot.gradle.plugin.ResolveMainClassName
import org.springframework.boot.gradle.plugin.SpringBootPlugin
import org.springframework.boot.gradle.tasks.bundling.BootArchive
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.bundling.BootWar
import org.springframework.boot.gradle.tasks.run.BootRun
import org.springframework.boot.loader.tools.LoaderImplementation

import javax.inject.Inject

Expand Down Expand Up @@ -376,10 +379,22 @@ class GrailsGradlePlugin extends GroovyPlugin {
}
}

project.logger.info('Adding Micronaut annotationProcessor dependencies to project {}', project.name)
// Add Micronaut annotation processor for Java source files.
// 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.

project.getDependencies().add('annotationProcessor', project.dependencies.platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion"))
project.getDependencies().add('annotationProcessor', 'io.micronaut:micronaut-inject-java')
project.getDependencies().add('annotationProcessor', 'jakarta.annotation:jakarta.annotation-api')

project.logger.info('Configuring CLASSIC boot loader for Micronaut compatibility in {}', project.name)
project.tasks.withType(BootJar).configureEach { BootJar bootJar ->
bootJar.loaderImplementation.convention(LoaderImplementation.CLASSIC)
}
project.tasks.withType(BootWar).configureEach { BootWar bootWar ->
bootWar.loaderImplementation.convention(LoaderImplementation.CLASSIC)
}
}
}

Expand Down
3 changes: 1 addition & 2 deletions grails-micronaut/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ group = 'org.apache.grails'
dependencies {
compileOnlyApi platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")
compileOnlyApi 'io.micronaut:micronaut-inject-groovy'
compileOnlyApi 'io.micronaut:micronaut-inject-java'

// Use the micronaut spring starter so that any bean in the spring context will be exposed to micronaut
api platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")
api 'io.micronaut.spring:micronaut-spring-boot-starter'
api 'io.micronaut.spring:micronaut-spring-context'

// For the grails plugin interface
compileOnly platform(project(':grails-bom'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@

package org.apache.grails.micronaut

import grails.plugins.GrailsPlugin
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import io.micronaut.context.ConfigurableApplicationContext
import io.micronaut.context.env.AbstractPropertySourceLoader
import io.micronaut.context.env.PropertySource

import grails.plugins.GrailsPlugin
import grails.plugins.GrailsPluginManager
import grails.plugins.Plugin
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.AbstractPropertySourceLoader
import io.micronaut.context.env.PropertySource

@Slf4j
@CompileStatic
Expand All @@ -53,7 +52,7 @@ class GrailsMicronautGrailsPlugin extends Plugin {
throw new IllegalStateException('A Micronaut Application Context should exist prior to the loading of the Grails Micronaut plugin.')
}

def micronautContext = applicationContext.getBean('micronautApplicationContext', ConfigurableApplicationContext)
def micronautContext = applicationContext.getBean('micronautApplicationContext', ApplicationContext)
def micronautEnv = micronautContext.environment

log.debug('Loading configurations from the plugins to the parent Micronaut context')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ spring:
management:
endpoints:
enabled-by-default: false
app:
name: test-micronaut-app

---
grails:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package micronaut

import bean.injection.AppConfig
import bean.injection.FactoryCreatedService
import bean.injection.JavaSingletonService
import bean.injection.NamedService
import groovy.transform.CompileStatic

import org.springframework.beans.factory.annotation.Autowired

@CompileStatic
class MicronautTestController {

@Autowired
JavaSingletonService javaSingletonService

@Autowired
FactoryCreatedService factoryCreatedService

@Autowired
AppConfig appConfig

@Autowired
NamedService namedService

def index() {
render(contentType: 'application/json', text: [
javaMessage: javaSingletonService.message,
factoryName: factoryCreatedService.name,
appName: appConfig.name,
namedService: namedService.name
])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class UrlMappings {
}
}

"/micronaut-test"(controller: 'micronautTest', action: 'index')

"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package micronaut

import bean.injection.AppConfig
import bean.injection.FactoryCreatedService
import bean.injection.JavaSingletonService
import bean.injection.NamedService

import grails.testing.mixin.integration.Integration
import io.micronaut.context.ApplicationContext as MicronautApplicationContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext as SpringApplicationContext
import org.springframework.context.ApplicationContextAware
import spock.lang.Specification

/**
* Integration tests verifying no bean duplication occurs between Spring and Micronaut contexts.
*
* When micronaut-spring bridges Micronaut beans into Spring, each bean should appear
* exactly once in the Spring context (not duplicated). Similarly, Micronaut beans
* should maintain correct counts in the Micronaut context.
*/
@Integration
class MicronautBeanDuplicationSpec extends Specification implements ApplicationContextAware {

@Autowired
MicronautApplicationContext micronautContext

SpringApplicationContext springContext

void setApplicationContext(SpringApplicationContext applicationContext) {
this.springContext = applicationContext
}

void "Java @Singleton bean appears exactly once in Spring context"() {
when:
def beans = springContext.getBeansOfType(JavaSingletonService)

then: "only one instance is registered, not duplicated by the bridge"
beans.size() == 1
}

void "@Factory/@Bean created bean appears exactly once in Spring context"() {
when:
def beans = springContext.getBeansOfType(FactoryCreatedService)

then: "only one instance is registered"
beans.size() == 1
}

void "@ConfigurationProperties bean appears exactly once in Spring context"() {
when:
def beans = springContext.getBeansOfType(AppConfig)

then: "only one instance is registered"
beans.size() == 1
}

void "NamedService implementations appear exactly 4 times in Spring context"() {
when:
def beans = springContext.getBeansOfType(NamedService)

then: "4 implementations, not 8 from duplication"
beans.size() == 4
}

void "Micronaut context has exactly 4 NamedService implementations"() {
when:
def beans = micronautContext.getBeansOfType(NamedService)

then:
beans.size() == 4
}

void "Grails service appears exactly once in Spring context"() {
when:
def beans = springContext.getBeansOfType(BeanInjectionService)

then: "Grails artefact not duplicated by Micronaut bridge"
beans.size() == 1
}

void "bridged Micronaut bean in Spring is same instance as in Micronaut context"() {
when:
def fromSpring = springContext.getBean(JavaSingletonService)
def fromMicronaut = micronautContext.getBean(JavaSingletonService)

then: "bridge shares the same singleton, not a copy"
fromSpring.is(fromMicronaut)
}

void "factory-created bean in Spring is same instance as in Micronaut context"() {
when:
def fromSpring = springContext.getBean(FactoryCreatedService)
def fromMicronaut = micronautContext.getBean(FactoryCreatedService)

then: "bridge shares the same singleton"
fromSpring.is(fromMicronaut)
}

void "@ConfigurationProperties bean in Spring is same instance as in Micronaut context"() {
when:
def fromSpring = springContext.getBean(AppConfig)
def fromMicronaut = micronautContext.getBean(AppConfig)

then: "bridge shares the same singleton"
fromSpring.is(fromMicronaut)
}
}
Loading
Loading