You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/08/06 19:22:05 UTC

(grails-core) branch fix/14914-component-scan created (now 5af734f65e)

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a change to branch fix/14914-component-scan
in repository https://gitbox.apache.org/repos/asf/grails-core.git


      at 5af734f65e Configure generated applications for component scanning

This branch includes the following new commits:

     new 5af734f65e Configure generated applications for component scanning

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(grails-core) 01/01: Configure generated applications for component scanning

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch fix/14914-component-scan
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 5af734f65eb43855e1f5163f16b13c98dc28a47d
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Thu Aug 6 15:20:09 2026 -0400

    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
---
 .../guide/conf/applicationClass/customizing.adoc   |   4 +-
 .../feature/lang/groovy/application.rocker.raw     |   5 +-
 .../feature/lang/GrailsApplicationSpec.groovy      |  16 +++
 .../org/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 246639c6a6..48fa90a564 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 3440e6dcc2..c4250ee5c8 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 4c00384293..8a50293161 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 2637eb8398..e4dd8aba49 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 13dbec46be..18e9f76c39 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 08c9b83087..34814b0f52 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
+}