Skip to content

Commit e64ca7a

Browse files
committed
feat(spring): Add component scanning support
* Add *Component Scan marker class as base for package scanning. * Update *Configuration classes to use it for finding self-classes.
1 parent 19a32c3 commit e64ca7a

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

src/main/resources/META-INF/archetype-post-generate.groovy

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,27 +204,53 @@ if (subDir.isDirectory()) projectDir = subDir
204204
// ─── Spring configuration class generator ─────────────────────────────────────
205205
// When includeSpring is true, every module the app composes carries a @Configuration
206206
// class in its `.config` sub-package (the app's Application class @Imports them all)
207-
// and declares spring-context. Everything here is skipped when includeSpring is false.
207+
// and declares spring-context. The module root package also gets a marker
208+
// "{Module}ComponentScan" interface that the @Configuration targets via
209+
// @ComponentScan(basePackageClasses=…) — a type-safe anchor for scanning the whole
210+
// module. Everything here is skipped when includeSpring is false.
208211

209212
def SPRING_CONTEXT = [groupId: 'org.springframework', artifactId: 'spring-context']
210213
def springCtx = includeSpring ? [SPRING_CONTEXT] : [] // appended to module deps only when Spring is enabled
211214
def configFqns = []
212215

213-
/** Writes a @Configuration class into {module}/…/{mainSub}/config and records its FQN; no-op when Spring is off. */
216+
/** Writes a {Module}ComponentScan marker interface into the module root package and a
217+
* @Configuration class (anchored to it via @ComponentScan) into {module}/…/{mainSub}/config,
218+
* recording the config FQN; no-op when Spring is off. */
214219
def configClass = { String module, String mainSub ->
215220
if (!includeSpring) return null
216-
def className = module.split('-').collect { it.capitalize() }.join('') + 'Configuration'
221+
def baseName = module.split('-').collect { it.capitalize() }.join('')
222+
def className = baseName + 'Configuration'
223+
def scanName = baseName + 'ComponentScan'
224+
225+
// Marker interface in the module root package — the @ComponentScan basePackageClasses anchor.
226+
def rootPkgFq = (pkgPath + '/' + mainSub).replace('/', '.')
227+
writeFile(projectDir, "${module}/src/main/java/${pkgPath}/${mainSub}/${scanName}.java", """\
228+
package ${rootPkgFq};
229+
230+
/**
231+
* Anchor class for based package class scanning.
232+
*/
233+
public interface ${scanName} {
234+
}
235+
""")
236+
217237
def pkgFq = (pkgPath + '/' + mainSub + '/config').replace('/', '.')
218238
def dir = "${module}/src/main/java/${pkgPath}/${mainSub}/config"
239+
def imports = [
240+
"import ${rootPkgFq}.${scanName};",
241+
'import org.springframework.context.annotation.ComponentScan;',
242+
'import org.springframework.context.annotation.Configuration;',
243+
].sort().join('\n')
219244
writeFile(projectDir, "${dir}/${className}.java", """\
220245
package ${pkgFq};
221246
222-
import org.springframework.context.annotation.Configuration;
247+
${imports}
223248
224249
/**
225250
* Spring configuration for the ${module} module.
226251
*/
227252
@Configuration
253+
@ComponentScan(basePackageClasses = ${scanName}.class)
228254
public class ${className} {
229255
}
230256
""")

src/test/resources/projects/full/verify.groovy

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ check("presentation-rest/src/main/java/${p}/presentation/rest/config/Presentatio
219219
assert text("common-domain/src/main/java/${p}/common/domain/config/CommonDomainConfiguration.java").contains('@Configuration') \
220220
: 'config class must be annotated @Configuration'
221221

222+
// every app-composed module also carries a {Module}ComponentScan marker interface in its root
223+
// package, with the @Configuration anchored to it via @ComponentScan(basePackageClasses=…)
224+
check("common-domain/src/main/java/${p}/common/domain/CommonDomainComponentScan.java")
225+
check("presentation-rest/src/main/java/${p}/presentation/rest/PresentationRestComponentScan.java")
226+
def scanIface = text("common-domain/src/main/java/${p}/common/domain/CommonDomainComponentScan.java")
227+
assert scanIface.contains('public interface CommonDomainComponentScan') : 'marker must be a public interface'
228+
assert scanIface.contains('Anchor class for based package class scanning.') : 'marker must carry the anchor Javadoc'
229+
def cdConfig = text("common-domain/src/main/java/${p}/common/domain/config/CommonDomainConfiguration.java")
230+
assert cdConfig.contains('@ComponentScan(basePackageClasses = CommonDomainComponentScan.class)') \
231+
: 'config class must @ComponentScan its module marker interface'
232+
assert cdConfig.contains("import ${p.replace('/', '.')}.common.domain.CommonDomainComponentScan;") \
233+
: 'config class must import its module marker interface'
234+
222235
// parent imports the Spring Boot BOM; app pulls the starters
223236
assert parentPom.contains('<artifactId>spring-boot-starter-parent</artifactId>') : 'parent must inherit spring-boot-starter-parent'
224237
assert appPom.contains('<artifactId>spring-boot-starter-web</artifactId>') : 'app must depend on spring-boot-starter-web'

0 commit comments

Comments
 (0)