From 5af734f65eb43855e1f5163f16b13c98dc28a47d Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 6 Aug 2026 15:20:09 -0400 Subject: [PATCH 1/2] Configure generated applications for component scanning Add base-package Spring component scanning to Forge and profile Application templates, with generated-source and runtime coverage. Closes #14914 Assisted-by: opencode:gpt-5.6-sol --- .../conf/applicationClass/customizing.adoc | 4 +- .../lang/groovy/application.rocker.raw | 5 +- .../feature/lang/GrailsApplicationSpec.groovy | 16 ++ .../grails/forge/create/CreateAppSpec.groovy | 141 ++++++++++++++---- .../Application.groovy | 4 +- .../Application.groovy | 4 +- 6 files changed, 143 insertions(+), 31 deletions(-) diff --git a/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc b/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc index 246639c6a62..48fa90a5646 100644 --- a/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc +++ b/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc @@ -23,7 +23,9 @@ There are several ways in which you can customize the `Application` class. === Customizing Scanning -By default Grails will scan all known source directories for controllers, domain class etc., however if there are packages in other JAR files you wish to scan you can do so by overriding the `packageNames()` method of the `Application` class: +Newly generated Grails applications include `@ComponentScan(value = 'your.application.package')` on the `Application` class. This scans standard Spring components in the application's base package and its subpackages. + +Grails scans its known source directories separately for Grails artefacts such as controllers and domain classes. To add packages to that Grails artefact scan, override the `packageNames()` method of the `Application` class: [source,groovy] ---- diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/lang/groovy/application.rocker.raw b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/lang/groovy/application.rocker.raw index 3440e6dcc29..c4250ee5c89 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/lang/groovy/application.rocker.raw +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/lang/groovy/application.rocker.raw @@ -31,10 +31,12 @@ import groovy.transform.CompileStatic import io.micronaut.spring.boot.starter.EnableMicronaut } +import org.springframework.context.annotation.ComponentScan @if(features.contains("spring-boot-starter-security")) { import org.springframework.context.annotation.Import - } + + import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration @if(applicationType.equals(ApplicationType.PLUGIN) || applicationType.equals(ApplicationType.WEB_PLUGIN)) { @@ -46,6 +48,7 @@ import grails.plugins.metadata.PluginSource @@PluginSource } @@CompileStatic +@@ComponentScan(value = '@project.getPackageName()') @if(features.contains("grails-micronaut")) { @@EnableMicronaut } diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy index 4c00384293f..8a502931613 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy @@ -42,6 +42,7 @@ class GrailsApplicationSpec extends BeanContextSpec implements CommandOutputFixt output.containsKey("grails-app/init/example/grails/Application.${Language.GROOVY.extension}".toString()) def applicationGroovyFile = output.get("grails-app/init/example/grails/Application.${Language.GROOVY.extension}".toString()) applicationGroovyFile.contains("@CompileStatic") + applicationGroovyFile.contains("@ComponentScan(value = 'example.grails')") !applicationGroovyFile.contains("@PluginSource") where: @@ -58,11 +59,26 @@ class GrailsApplicationSpec extends BeanContextSpec implements CommandOutputFixt output.containsKey("grails-app/init/example/grails/Application.${Language.GROOVY.extension}".toString()) def applicationGroovyFile = output.get("grails-app/init/example/grails/Application.${Language.GROOVY.extension}".toString()) applicationGroovyFile.contains("@PluginSource") + applicationGroovyFile.contains("@ComponentScan(value = 'example.grails')") where: applicationType << [ApplicationType.PLUGIN, ApplicationType.WEB_PLUGIN] } + void 'profile Application skeletons configure base-package component scanning'() { + given: + File profiles = new File('../../grails-profiles').canonicalFile + + expect: + ['base', 'plugin'].every { String profile -> + File application = new File(profiles, "${profile}/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy") + assert application.file, "missing profile Application skeleton ${application}" + assert application.text.contains('import org.springframework.context.annotation.ComponentScan') + assert application.text.contains("@ComponentScan(value = '@grails.codegen.defaultPackage@')") + true + } + } + void "REST-API application should have ApplicationController"() { when: def output = generate(ApplicationType.REST_API) diff --git a/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy b/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy index 2637eb83989..e4dd8aba491 100644 --- a/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy +++ b/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy @@ -24,6 +24,7 @@ import org.grails.forge.application.OperatingSystem import org.grails.forge.options.DevelopmentReloading import org.grails.forge.options.JdkVersion import org.grails.forge.utils.CommandSpec +import spock.lang.Unroll class CreateAppSpec extends CommandSpec { @@ -60,72 +61,158 @@ class CreateAppSpec extends CommandSpec { new File(dir, "grails-app/i18n").exists() } - void "test create-app creates a correct Application.groovy"() { + @Unroll + void "test create-app #applicationType creates a correct Application.groovy"() { given: - generateProject(OperatingSystem.MACOS_ARCH64, [], ApplicationType.DEFAULT_OPTION) + generateProject(OperatingSystem.MACOS_ARCH64, [], applicationType) def applicationClassSourceFile = new File(dir, 'grails-app/init/example/grails/Application.groovy') expect: applicationClassSourceFile.exists() - applicationClassSourceFile.text == '''\ + applicationClassSourceFile.text == applicationSource.stripIndent(8) + + where: + applicationType | applicationSource + ApplicationType.WEB | '''\ package example.grails import groovy.transform.CompileStatic + import org.springframework.context.annotation.ComponentScan + import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration @CompileStatic + @ComponentScan(value = 'example.grails') class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } } - '''.stripIndent(8) - } + ''' + ApplicationType.REST_API | '''\ + package example.grails - void "test create-app with micronaut feature"() { - given: - // Micronaut features require JDK 25+ because micronaut-core's ScopedValues - // references java.lang.ScopedValue.CallableOp (JEP 506, finalized in JDK 25). - generateProject(OperatingSystem.MACOS_ARCH64, ['grails-micronaut'], ApplicationType.WEB, - DevelopmentReloading.DEFAULT_OPTION, JdkVersion.JDK_25) + import groovy.transform.CompileStatic - def gradleProperties = new File(dir, 'gradle.properties') - def gradleBuildFile = new File(dir, 'build.gradle') + import org.springframework.context.annotation.ComponentScan - expect: - gradleProperties.exists() - gradleProperties.text.contains('micronautPlatformVersion=4.10.16') - gradleBuildFile.exists() - gradleBuildFile.text.contains('implementation "org.apache.grails:grails-micronaut"') - } + import grails.boot.GrailsApp + import grails.boot.config.GrailsAutoConfiguration - void "test create-app web-plugin creates a correct Application.groovy"() { - given: - generateProject(OperatingSystem.MACOS_ARCH64, [], ApplicationType.WEB_PLUGIN) + @CompileStatic + @ComponentScan(value = 'example.grails') + class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } + } + ''' + ApplicationType.PLUGIN | '''\ + package example.grails - def applicationClassSourceFile = new File(dir, 'grails-app/init/example/grails/Application.groovy') + import groovy.transform.CompileStatic - expect: - applicationClassSourceFile.exists() - applicationClassSourceFile.text == '''\ + import org.springframework.context.annotation.ComponentScan + + import grails.boot.GrailsApp + import grails.boot.config.GrailsAutoConfiguration + import grails.plugins.metadata.PluginSource + + @PluginSource + @CompileStatic + @ComponentScan(value = 'example.grails') + class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } + } + ''' + ApplicationType.WEB_PLUGIN | '''\ package example.grails import groovy.transform.CompileStatic + import org.springframework.context.annotation.ComponentScan + import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration import grails.plugins.metadata.PluginSource @PluginSource @CompileStatic + @ComponentScan(value = 'example.grails') class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } } - '''.stripIndent(8) + ''' + } + + void "test generated application scans standard Spring components"() { + given: + generateProject(OperatingSystem.MACOS_ARCH64) + File componentSourceFile = new File(dir, 'src/main/groovy/example/grails/components/ScannedComponent.groovy') + componentSourceFile.parentFile.mkdirs() + componentSourceFile.text = '''\ + package example.grails.components + + import org.springframework.stereotype.Component + + @Component + class ScannedComponent { + String value() { + 'scanned' + } + } + '''.stripIndent() + File componentSpecFile = new File(dir, 'src/test/groovy/example/grails/ComponentScanningSpec.groovy') + componentSpecFile.parentFile.mkdirs() + componentSpecFile.text = '''\ + package example.grails + + import example.grails.components.ScannedComponent + import grails.testing.mixin.integration.Integration + import org.springframework.beans.factory.annotation.Autowired + import spock.lang.Specification + + @Integration + class ComponentScanningSpec extends Specification { + + @Autowired + ScannedComponent scannedComponent + + void "standard Spring component is injected"() { + expect: + scannedComponent.value() == 'scanned' + } + } + '''.stripIndent() + + when: + String output = executeGradle('test', '--tests', 'example.grails.ComponentScanningSpec').output + + then: + output.contains('BUILD SUCCESSFUL') + } + + void "test create-app with micronaut feature"() { + given: + // Micronaut features require JDK 25+ because micronaut-core's ScopedValues + // references java.lang.ScopedValue.CallableOp (JEP 506, finalized in JDK 25). + generateProject(OperatingSystem.MACOS_ARCH64, ['grails-micronaut'], ApplicationType.WEB, + DevelopmentReloading.DEFAULT_OPTION, JdkVersion.JDK_25) + + def gradleProperties = new File(dir, 'gradle.properties') + def gradleBuildFile = new File(dir, 'build.gradle') + + expect: + gradleProperties.exists() + gradleProperties.text.contains('micronautPlatformVersion=4.10.16') + gradleBuildFile.exists() + gradleBuildFile.text.contains('implementation "org.apache.grails:grails-micronaut"') } @Override diff --git a/grails-profiles/base/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy b/grails-profiles/base/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy index 13dbec46be6..18e9f76c39d 100644 --- a/grails-profiles/base/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy +++ b/grails-profiles/base/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy @@ -2,12 +2,14 @@ package @grails.codegen.defaultPackage@ import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration +import org.springframework.context.annotation.ComponentScan import groovy.transform.CompileStatic @CompileStatic +@ComponentScan(value = '@grails.codegen.defaultPackage@') class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } -} \ No newline at end of file +} diff --git a/grails-profiles/plugin/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy b/grails-profiles/plugin/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy index 08c9b83087b..34814b0f52b 100644 --- a/grails-profiles/plugin/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy +++ b/grails-profiles/plugin/skeleton/grails-app/init/@grails.codegen.defaultPackage.path@/Application.groovy @@ -3,10 +3,12 @@ package @grails.codegen.defaultPackage@ import grails.boot.* import grails.boot.config.GrailsAutoConfiguration import grails.plugins.metadata.* +import org.springframework.context.annotation.ComponentScan @PluginSource +@ComponentScan(value = '@grails.codegen.defaultPackage@') class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } -} \ No newline at end of file +} From 2c602ff01b19b8e07f32ca59dcdbe48ca5d2f22c Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 6 Aug 2026 16:51:08 -0400 Subject: [PATCH 2/2] Harden component scan regression tests Resolve profile templates independently of the test working directory and assert the generated test task outcome through Gradle TestKit. Assisted-by: opencode:gpt-5.6-sol --- .../forge/feature/lang/GrailsApplicationSpec.groovy | 11 ++++++++++- .../org/grails/forge/create/CreateAppSpec.groovy | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy index 8a502931613..393b8fb701c 100644 --- a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/lang/GrailsApplicationSpec.groovy @@ -67,7 +67,7 @@ class GrailsApplicationSpec extends BeanContextSpec implements CommandOutputFixt void 'profile Application skeletons configure base-package component scanning'() { given: - File profiles = new File('../../grails-profiles').canonicalFile + File profiles = findProfilesDirectory() expect: ['base', 'plugin'].every { String profile -> @@ -143,4 +143,13 @@ class GrailsApplicationSpec extends BeanContextSpec implements CommandOutputFixt applicationType << ApplicationType.values().toList() } + + private static File findProfilesDirectory() { + File current = new File(GrailsApplicationSpec.getProtectionDomain().getCodeSource().getLocation().toURI()) + while (current && !new File(current, 'grails-profiles').isDirectory()) { + current = current.parentFile + } + assert current != null: 'Unable to locate the grails-core repository root' + new File(current, 'grails-profiles') + } } diff --git a/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy b/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy index e4dd8aba491..63427d147a7 100644 --- a/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy +++ b/grails-forge/test-core/src/test/groovy/org/grails/forge/create/CreateAppSpec.groovy @@ -19,6 +19,7 @@ package org.grails.forge.create +import org.gradle.testkit.runner.TaskOutcome import org.grails.forge.application.ApplicationType import org.grails.forge.application.OperatingSystem import org.grails.forge.options.DevelopmentReloading @@ -192,10 +193,10 @@ class CreateAppSpec extends CommandSpec { '''.stripIndent() when: - String output = executeGradle('test', '--tests', 'example.grails.ComponentScanningSpec').output + def result = executeGradle('test', '--tests', 'example.grails.ComponentScanningSpec') then: - output.contains('BUILD SUCCESSFUL') + result.task(':test').outcome == TaskOutcome.SUCCESS } void "test create-app with micronaut feature"() {