Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 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
af91912
style: formatting and readability
matrei Feb 20, 2026
f4fe7bd
refactor: groovify
matrei Feb 20, 2026
6c81244
Use BootArchive instead of BootWar/BootJar
jdaugherty Feb 20, 2026
749364d
Remove unused imports
jdaugherty Feb 20, 2026
251e6fd
Merge branch '7.0.x' into micronaut-fixes-2
jamesfredley Feb 20, 2026
6b59b6f
Address PR review feedback: comprehensive ersatz tests, plugin @Singl…
jamesfredley Feb 21, 2026
db7f62f
Exhaustive ersatz integration tests for all Micronaut client patterns
jamesfredley Feb 21, 2026
47e4cbd
Merge branch '7.0.x' into micronaut-fixes-2
jamesfredley Feb 21, 2026
1344716
test: add Groovy-only Micronaut test module
jamesfredley Feb 21, 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
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ gradleCycloneDxPluginVersion=2.4.1
micronautPlatformVersion=4.9.2

# Libraries only specific to test apps, these should not be exposed
ersatzVersion=4.0.1
grailsSpringSecurityVersion=7.0.1-SNAPSHOT
jbossTransactionApiVersion=2.0.0.Final
# Note: we do not import the micronaut bom in our tests to avoid spring version mismatches
Expand Down
18 changes: 18 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,24 @@ 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 DevTools does not work with the Micronaut integration.

The Grails Gradle Plugin automatically configures Groovy-based Micronaut bean registration via AST transforms (`micronaut-inject-groovy` on `compileOnlyApi`). Groovy classes annotated with `@Singleton`, `@Factory`, `@ConfigurationProperties`, etc. are processed automatically.

IMPORTANT: If your project contains **Java source files** with Micronaut annotations (e.g. `@Singleton`, `@Factory`), you must manually add the Micronaut annotation processor to your `build.gradle`. The annotation processor is not configured automatically because it is incompatible with Groovy incremental compilation (see https://github.com/apache/grails-core/issues/15211[#15211]). For projects that mix Java and Groovy Micronaut beans, consider splitting them into separate source sets or modules to avoid incremental compilation issues.

[source,groovy]
.build.gradle - Adding annotation processor for Java Micronaut beans
----
dependencies {
annotationProcessor platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")
annotationProcessor 'io.micronaut:micronaut-inject-java'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
----

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 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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 java.util.Set;

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;

@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 @@ -19,18 +19,21 @@
package org.grails.forge.feature.reloading;

import jakarta.inject.Singleton;

import org.grails.forge.application.ApplicationType;
import org.grails.forge.application.generator.GeneratorContext;
import org.grails.forge.build.dependencies.Dependency;
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;

@Singleton
public class SpringBootDevTools implements ReloadingFeature, DefaultFeature {

@Override
public String getName() {
return "spring-boot-devtools";
Expand All @@ -43,7 +46,11 @@ public String getTitle() {

@Override
public String getDescription() {
return "Spring Boot Devtools is a powerful tool that enhances development productivity by providing features like automatic application restarts on code changes, live reloading of static resources, and remote debugging support. It enables developers to rapidly iterate and test changes during the development process, making it a valuable asset for Spring Boot projects.";
return "Spring Boot Devtools is a powerful tool that enhances development productivity " +
"by providing features like automatic application restarts on code changes, live " +
"reloading of static resources, and remote debugging support. It enables developers " +
"to rapidly iterate and test changes during the development process, making it a " +
"valuable asset for Spring Boot projects.";
}

@Override
Expand All @@ -61,6 +68,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 @@ -16,45 +16,45 @@
* specific language governing permissions and limitations
* under the License.
*/

package org.grails.forge.feature.reloading

import spock.lang.Unroll

import org.grails.forge.ApplicationContextSpec
import org.grails.forge.application.ApplicationType
import org.grails.forge.feature.Features
import org.grails.forge.fixture.CommandOutputFixture

class SpringBootDevToolsSpec extends ApplicationContextSpec implements CommandOutputFixture {

void "test spring-boot-devtools feature"() {

when:
final Features features = getFeatures(["spring-boot-devtools"])

then:
features.contains("spring-boot-devtools")

expect:
'spring-boot-devtools' in getFeatures(['spring-boot-devtools'])
}

void "test spring-boot-devtools dependency is present for #applicationType application type"() {

@Unroll
void "test spring-boot-devtools dependency is present for #applicationType application type"(ApplicationType applicationType) {
when:
def output = generate(applicationType, ["spring-boot-devtools"])
def build = output["build.gradle"]
def output = generate(applicationType, ['spring-boot-devtools'])
def build = output['build.gradle']

then:
build.contains("developmentOnly \"org.springframework.boot:spring-boot-devtools\"")
build.contains('developmentOnly "org.springframework.boot:spring-boot-devtools"')

where:
applicationType << [ApplicationType.WEB, ApplicationType.REST_API]
}

void "test there can be only one of Reloading feature"() {
when:
getFeatures(["spring-boot-devtools", "jrebel"])
getFeatures(['spring-boot-devtools', 'jrebel'])

then:
def ex = thrown(IllegalArgumentException)
ex.message.contains("There can only be one of the following features selected")
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"() {
expect:
!('spring-boot-devtools' in getFeatures(['grails-micronaut']))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ 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.run.BootRun
import org.springframework.boot.loader.tools.LoaderImplementation

import javax.inject.Inject

Expand Down Expand Up @@ -377,10 +378,11 @@ class GrailsGradlePlugin extends GroovyPlugin {
}
}

project.logger.info('Adding Micronaut annotationProcessor dependencies to project {}', project.name)
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(BootArchive).configureEach {
it.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 @@ -16,13 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.grails.micronaut

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import io.micronaut.context.ConfigurableApplicationContext
import io.micronaut.context.ApplicationContext as MicronautApplicationContext
import io.micronaut.context.env.AbstractPropertySourceLoader
import io.micronaut.context.env.PropertySource

Expand Down Expand Up @@ -50,24 +49,39 @@ class GrailsMicronautGrailsPlugin extends Plugin {
}

if (!applicationContext.containsBean('micronautApplicationContext')) {
throw new IllegalStateException('A Micronaut Application Context should exist prior to the loading of the Grails Micronaut 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',
MicronautApplicationContext
)
def micronautEnv = micronautContext.environment

log.debug('Loading configurations from the plugins to the parent Micronaut context')

def plugins = pluginManager.allPlugins
def pluginsFromContext = pluginManagerFromContext ? pluginManagerFromContext.allPlugins : new GrailsPlugin[0]
def pluginsFromContext = pluginManagerFromContext ?
pluginManagerFromContext.allPlugins :
new GrailsPlugin[0]
int priority = AbstractPropertySourceLoader.DEFAULT_POSITION
[plugins, pluginsFromContext].each { pluginsToProcess ->
Arrays.stream(pluginsToProcess)
.filter { plugin -> plugin.propertySource != null }
.forEach { plugin ->
log.debug('Loading configurations from {} plugin to the parent Micronaut context', plugin.name)
// If invoking the source as `.source`, the NavigableMapPropertySource will return null, while invoking the getter, it will return the correct value
micronautEnv.addPropertySource(PropertySource.of("grails.plugins.$plugin.name", (Map) plugin.propertySource.getSource(), --priority))
pluginsToProcess
.findAll { it.propertySource != null }
.each {
log.debug('Loading configurations from {} plugin to the parent Micronaut context', it.name)
// If invoking the source as `.source`, the NavigableMapPropertySource will return null,
// while invoking the getter, it will return the correct value
micronautEnv.addPropertySource(
PropertySource.of(
"grails.plugins.$it.name",
(Map) it.propertySource.getSource(),
--priority
)
)
}
}
micronautEnv.refresh()
Expand Down
14 changes: 13 additions & 1 deletion grails-test-examples/micronaut/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.springframework.boot.gradle.tasks.bundling.BootArchive

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
Expand Down Expand Up @@ -35,20 +37,30 @@ apply plugin: 'org.apache.grails.gradle.grails-gsp'
dependencies {
implementation platform(project(':grails-bom'))

implementation "io.micronaut:micronaut-http-client:$micronautHttpClientVersion"
implementation "io.micronaut:micronaut-retry:$micronautHttpClientVersion"
implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion"
implementation 'org.apache.grails:grails-dependencies-starter-web'
implementation 'org.apache.grails:grails-micronaut'
implementation 'org.apache.grails:grails-scaffolding'
implementation project(':grails-test-examples-plugins-micronaut-singleton')

annotationProcessor platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")
annotationProcessor 'io.micronaut:micronaut-inject-java'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

testAndDevelopmentOnly platform(project(':grails-bom'))
testAndDevelopmentOnly 'org.apache.grails:grails-dependencies-assets'

runtimeOnly 'cloud.wondrify:asset-pipeline-grails'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.apache.tomcat:tomcat-jdbc'
runtimeOnly 'cloud.wondrify:asset-pipeline-grails'

testImplementation "io.micronaut:micronaut-http-client:$micronautHttpClientVersion"
testImplementation 'org.apache.grails:grails-dependencies-test'

integrationTestImplementation testFixtures('org.apache.grails:grails-geb')
integrationTestImplementation "io.github.cjstehno.ersatz:ersatz:$ersatzVersion"
}

apply {
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 Expand Up @@ -125,6 +127,11 @@ environments:
dbCreate: create-drop
url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
test:
micronaut:
http:
services:
grails-self:
url: http://localhost:19876
dataSource:
dbCreate: update
url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
Expand Down
Loading
Loading