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..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 @@ -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 = findProfilesDirectory() + + 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) @@ -127,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 2637eb83989..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,11 +19,13 @@ 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 import org.grails.forge.options.JdkVersion import org.grails.forge.utils.CommandSpec +import spock.lang.Unroll class CreateAppSpec extends CommandSpec { @@ -60,72 +62,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: + def result = executeGradle('test', '--tests', 'example.grails.ComponentScanningSpec') + + then: + result.task(':test').outcome == TaskOutcome.SUCCESS + } + + 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 +}