codeconsole commented on code in PR #16019:
URL: https://github.com/apache/grails-core/pull/16019#discussion_r3679572495


##########
grails-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
##########
@@ -1 +1 @@
-org.grails.plugins.core.CoreAutoConfiguration
\ No newline at end of file
+org.grails.plugins.CoreAutoConfiguration

Review Comment:
   Documented rather than made configurable, in `0067d15e23`. The three FQCN 
changes the guide was missing now get the same treatment as line 1582 — 
`CoreAutoConfiguration`'s package move, `GrailsCacheAutoConfiguration` → 
`CacheAutoConfiguration`, and `DataBindingConfiguration` → 
`DataBindingAutoConfiguration` — including your point that Spring Boot only 
reports an invalid exclude for a class still on the classpath, so a stale entry 
is ignored in silence and the beans it was meant to suppress register anyway.
   
   Teaching `autoConfigurationName` to take a fully-qualified name was the 
deliberate non-choice: these classes should carry their convention name, and an 
escape hatch that lets one keep a package it no longer lives beside would 
undercut that.



##########
grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc:
##########
@@ -119,6 +119,143 @@ class I18nGrailsPlugin extends Plugin {
 A plugin may override either `doWithSpring()` or `doWithSpring(BeanBuilder)`, 
but not both. The closure-returning `doWithSpring()` remains fully supported 
for backwards compatibility. Defining both forms in the same plugin is an 
error: the plugin will fail to load with an exception, since the two forms are 
alternatives and defining both indicates an incomplete migration between them. 
Consolidate the bean definitions into a single form.
 
 
+==== Compiling Bean Definitions into a Spring Boot AutoConfiguration
+
+
+`beanRegistrar()` and `doWithSpring` register beans at a single, fixed point 
in startup: before Spring Boot processes any 
`{springbootapi}org/springframework/boot/autoconfigure/AutoConfiguration.html[AutoConfiguration]`.
 That is enough for a bean to win against a Boot default guarded by 
`@ConditionalOnMissingBean`, but it cannot express ordering *relative to a 
specific* auto-configuration — running before `WebMvcAutoConfiguration` but 
after `MessageSourceAutoConfiguration`, say, regardless of where those two land 
in Boot's overall sort. Only a real `AutoConfiguration` class can declare that 
ordering, via `@AutoConfiguration(before = ..., after = ...)`.
+
+The link:{api}grails/compiler/beans/GrailsBeans.html[GrailsBeans] annotation 
lets you author bean definitions with DSL syntax similar to the classic Beans 
DSL, while compiling them at build time into genuine `@Bean` factory methods on 
a plain `AutoConfiguration` class — no closures, and nothing DSL-specific, 
survive into the compiled bytecode. Add `org.apache.grails:grails-beans-dsl` as 
a dependency and annotate a class whose `beans` property is a closure of 
`bean(["name", ] Type) { ... }` statements:
+
+[source,groovy]
+----
+import grails.compiler.beans.GrailsBeans
+import org.springframework.boot.autoconfigure.AutoConfiguration
+import 
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
+import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
+import org.springframework.context.MessageSource
+import 
org.springframework.context.support.ReloadableResourceBundleMessageSource
+import org.springframework.web.servlet.i18n.CookieLocaleResolver
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
+
+@GrailsBeans
+@AutoConfiguration(before = [MessageSourceAutoConfiguration, 
WebMvcAutoConfiguration])
+class I18nBeans {
+
+    def beans = {
+        bean('messageSource', ReloadableResourceBundleMessageSource) {
+            new ReloadableResourceBundleMessageSource(basename: 
'WEB-INF/grails-app/i18n/messages')
+        }
+
+        bean('localeResolver', 
CookieLocaleResolver).conditionalOnMissingBean(CookieLocaleResolver) {
+            new CookieLocaleResolver('locale')
+        }
+
+        bean('localeChangeInterceptor', LocaleChangeInterceptor) { 
MessageSource messageSource ->
+            new LocaleChangeInterceptor(paramName: 'lang')
+        }
+    }
+}
+----
+
+Each `bean(["name", ] Type)` statement compiles to a public method returning 
the declared type and annotated `@Bean("name")`. The generated method's name is 
an implementation detail — Spring resolves the bean by its `@Bean("name")` 
value, never by the method name. It matches the bean name when that is a valid 
Java identifier not already taken by an existing member (declared, inherited, 
or defined elsewhere in the DSL — a bean named `toString` will not override 
`Object.toString()`); otherwise a synthesized `<type>$N` name (`service$0`, 
`service$1`, ...) is used instead, so any legal, non-blank Spring bean name 
compiles:
+
+* The bean name defaults to the decapitalized simple class name when omitted.
+* The same bean name may be declared by more than one `bean(...)` statement — 
the standard Spring Boot pattern for mutually exclusive variants of one bean, 
such as the framework's own `grailsUrlConverter` selected by 
`@ConditionalOnProperty` — provided every declaration with the name carries its 
own discriminating condition (such as `.annotate(ConditionalOnProperty, ...)`), 
so that at most one of them registers at runtime. Duplicating a bean name 
without that is a compile-time error, since Spring would keep the first 
definition and silently skip the rest.
+* Chaining `.conditionalOnMissingBean(...)` adds a `@ConditionalOnMissingBean` 
annotation to the generated method, matching Spring Boot's usual back-off 
semantics. It takes positional types (`.conditionalOnMissingBean(Greeter)`), 
the annotation's own named attributes (`.conditionalOnMissingBean(name: 
'localeResolver', search: SearchStrategy.CURRENT)`), or both. With no arguments 
at all it compiles to the bare annotation, letting Spring Boot infer the 
back-off type from the method's return type — the recommended form when a bean 
simply backs off its own type, since 
`bean(AvailableLocaleResolver).conditionalOnMissingBean()` already says 
everything `.conditionalOnMissingBean(AvailableLocaleResolver)` would repeat.
+* Chaining `.conditionalOnMissingBeanName(...)` is the name-based counterpart: 
"register this bean unless a bean with *this bean's name* already exists". The 
condition's `name` member is set from the bean's own (explicit or 
convention-derived) name, so 
`bean(MessageSource).conditionalOnMissingBeanName(search: 
SearchStrategy.CURRENT) { ... }` states `messageSource` once and it feeds both 
`@Bean` and the condition — the two strings the explicit form has to keep in 
sync cannot diverge. It accepts the annotation's other attributes (`search:`, 
`ignored:`, ...) but rejects `name:` and types, which would contradict its 
purpose; use `.conditionalOnMissingBean(...)` for those.
+* Chaining `.primary()`, `.lazy()`, and/or `.scope("name")` adds `@Primary`, 
`@Lazy`, and `@Scope("name")` respectively.
+* Chaining `.staticMethod()` makes the generated factory method `static` — 
Spring's recommended shape for `BeanFactoryPostProcessor` and 
`BeanPostProcessor` beans, which must be creatable without instantiating their 
declaring configuration class. A static bean's closure cannot reference 
`field(...)` or `method(...)` members, since those are instance members; under 
`@CompileStatic` that mistake is a compile error.
+* Chaining `.annotate(AnnotationType[, attr: value, ...])` attaches any other 
single-valued annotation directly — any `@Conditional*` (built-in or a custom 
`Condition`), `@Order`, `@DependsOn`, or anything else a hand-written `@Bean` 
method could carry. It's repeatable, so several different annotations can be 
chained onto the same bean: `bean('special', Special).annotate(Order, value: 
1).annotate(ConditionalOnProperty, prefix: 'myapp', name: 'enabled', 
havingValue: 'true') { ... }`.
+* Any combination of the above can be chained together, in any order — for 
example `bean('slowGreeter', SlowGreeter).lazy().scope('prototype') { ... }`.
+* Typed closure parameters (`{ MessageSource messageSource -> ... }`) become 
the generated method's parameters, so other beans are injected the same way a 
hand-written `@Bean` method would declare them — and a parameter-level 
annotation such as `{ @Qualifier('special') Greeter g -> ... }` carries 
straight through too, since the closure's own parameters become the generated 
method's parameters directly.
+
+Two more statement kinds can appear inside `beans { }` alongside `bean(...)`, 
for state and logic shared across bean methods — the same role a hand-written 
`@Configuration` class's own fields and private methods play:
+
+* `field(["name", ] Type)`, optionally chained with `.value(...)` and/or 
(repeatably) `.annotate(AnnotationType[, attr: value, ...])`, declares a 
private field on the generated class. The usual case is injected configuration, 
and `.value(...)` covers it directly: `field('encoding', 
String).value('grails.gsp.view.encoding', 'UTF-8')` compiles to 
`@Value("${grails.gsp.view.encoding:UTF-8}")`. The two-argument form takes a 
config key and default — and because the DSL builds the placeholder itself, the 
key may be a bare constant reference (`.value(Settings.GSP_VIEW_ENCODING, 
'UTF-8')`), the one shape a directly-written annotation value can't accept. The 
one-argument form takes a bare config key with no default — 
`.value('grails.web.linkGenerator.useCache')` compiles to 
`@Value("${grails.web.linkGenerator.useCache}")` — while a string already 
containing a `${...}` placeholder or a `#{...}` SpEL expression passes through 
verbatim: `.value('#{T(java.lang.Runtime).getRuntime().availab
 leProcessors()}')`.

Review Comment:
   Fixed in `0067d15e23`. The key does not exist; the doc and the spec fixture 
both now use `grails.views.gsp.encoding`, which is what 
`Settings.GSP_VIEW_ENCODING` holds.



##########
grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc:
##########
@@ -119,6 +119,143 @@ class I18nGrailsPlugin extends Plugin {
 A plugin may override either `doWithSpring()` or `doWithSpring(BeanBuilder)`, 
but not both. The closure-returning `doWithSpring()` remains fully supported 
for backwards compatibility. Defining both forms in the same plugin is an 
error: the plugin will fail to load with an exception, since the two forms are 
alternatives and defining both indicates an incomplete migration between them. 
Consolidate the bean definitions into a single form.
 
 
+==== Compiling Bean Definitions into a Spring Boot AutoConfiguration
+
+
+`beanRegistrar()` and `doWithSpring` register beans at a single, fixed point 
in startup: before Spring Boot processes any 
`{springbootapi}org/springframework/boot/autoconfigure/AutoConfiguration.html[AutoConfiguration]`.
 That is enough for a bean to win against a Boot default guarded by 
`@ConditionalOnMissingBean`, but it cannot express ordering *relative to a 
specific* auto-configuration — running before `WebMvcAutoConfiguration` but 
after `MessageSourceAutoConfiguration`, say, regardless of where those two land 
in Boot's overall sort. Only a real `AutoConfiguration` class can declare that 
ordering, via `@AutoConfiguration(before = ..., after = ...)`.
+
+The link:{api}grails/compiler/beans/GrailsBeans.html[GrailsBeans] annotation 
lets you author bean definitions with DSL syntax similar to the classic Beans 
DSL, while compiling them at build time into genuine `@Bean` factory methods on 
a plain `AutoConfiguration` class — no closures, and nothing DSL-specific, 
survive into the compiled bytecode. Add `org.apache.grails:grails-beans-dsl` as 
a dependency and annotate a class whose `beans` property is a closure of 
`bean(["name", ] Type) { ... }` statements:
+
+[source,groovy]
+----
+import grails.compiler.beans.GrailsBeans
+import org.springframework.boot.autoconfigure.AutoConfiguration
+import 
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
+import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
+import org.springframework.context.MessageSource
+import 
org.springframework.context.support.ReloadableResourceBundleMessageSource
+import org.springframework.web.servlet.i18n.CookieLocaleResolver
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
+
+@GrailsBeans
+@AutoConfiguration(before = [MessageSourceAutoConfiguration, 
WebMvcAutoConfiguration])
+class I18nBeans {
+
+    def beans = {
+        bean('messageSource', ReloadableResourceBundleMessageSource) {
+            new ReloadableResourceBundleMessageSource(basename: 
'WEB-INF/grails-app/i18n/messages')
+        }
+
+        bean('localeResolver', 
CookieLocaleResolver).conditionalOnMissingBean(CookieLocaleResolver) {
+            new CookieLocaleResolver('locale')
+        }
+
+        bean('localeChangeInterceptor', LocaleChangeInterceptor) { 
MessageSource messageSource ->
+            new LocaleChangeInterceptor(paramName: 'lang')
+        }
+    }
+}
+----
+
+Each `bean(["name", ] Type)` statement compiles to a public method returning 
the declared type and annotated `@Bean("name")`. The generated method's name is 
an implementation detail — Spring resolves the bean by its `@Bean("name")` 
value, never by the method name. It matches the bean name when that is a valid 
Java identifier not already taken by an existing member (declared, inherited, 
or defined elsewhere in the DSL — a bean named `toString` will not override 
`Object.toString()`); otherwise a synthesized `<type>$N` name (`service$0`, 
`service$1`, ...) is used instead, so any legal, non-blank Spring bean name 
compiles:
+
+* The bean name defaults to the decapitalized simple class name when omitted.
+* The same bean name may be declared by more than one `bean(...)` statement — 
the standard Spring Boot pattern for mutually exclusive variants of one bean, 
such as the framework's own `grailsUrlConverter` selected by 
`@ConditionalOnProperty` — provided every declaration with the name carries its 
own discriminating condition (such as `.annotate(ConditionalOnProperty, ...)`), 
so that at most one of them registers at runtime. Duplicating a bean name 
without that is a compile-time error, since Spring would keep the first 
definition and silently skip the rest.
+* Chaining `.conditionalOnMissingBean(...)` adds a `@ConditionalOnMissingBean` 
annotation to the generated method, matching Spring Boot's usual back-off 
semantics. It takes positional types (`.conditionalOnMissingBean(Greeter)`), 
the annotation's own named attributes (`.conditionalOnMissingBean(name: 
'localeResolver', search: SearchStrategy.CURRENT)`), or both. With no arguments 
at all it compiles to the bare annotation, letting Spring Boot infer the 
back-off type from the method's return type — the recommended form when a bean 
simply backs off its own type, since 
`bean(AvailableLocaleResolver).conditionalOnMissingBean()` already says 
everything `.conditionalOnMissingBean(AvailableLocaleResolver)` would repeat.
+* Chaining `.conditionalOnMissingBeanName(...)` is the name-based counterpart: 
"register this bean unless a bean with *this bean's name* already exists". The 
condition's `name` member is set from the bean's own (explicit or 
convention-derived) name, so 
`bean(MessageSource).conditionalOnMissingBeanName(search: 
SearchStrategy.CURRENT) { ... }` states `messageSource` once and it feeds both 
`@Bean` and the condition — the two strings the explicit form has to keep in 
sync cannot diverge. It accepts the annotation's other attributes (`search:`, 
`ignored:`, ...) but rejects `name:` and types, which would contradict its 
purpose; use `.conditionalOnMissingBean(...)` for those.
+* Chaining `.primary()`, `.lazy()`, and/or `.scope("name")` adds `@Primary`, 
`@Lazy`, and `@Scope("name")` respectively.
+* Chaining `.staticMethod()` makes the generated factory method `static` — 
Spring's recommended shape for `BeanFactoryPostProcessor` and 
`BeanPostProcessor` beans, which must be creatable without instantiating their 
declaring configuration class. A static bean's closure cannot reference 
`field(...)` or `method(...)` members, since those are instance members; under 
`@CompileStatic` that mistake is a compile error.
+* Chaining `.annotate(AnnotationType[, attr: value, ...])` attaches any other 
single-valued annotation directly — any `@Conditional*` (built-in or a custom 
`Condition`), `@Order`, `@DependsOn`, or anything else a hand-written `@Bean` 
method could carry. It's repeatable, so several different annotations can be 
chained onto the same bean: `bean('special', Special).annotate(Order, value: 
1).annotate(ConditionalOnProperty, prefix: 'myapp', name: 'enabled', 
havingValue: 'true') { ... }`.
+* Any combination of the above can be chained together, in any order — for 
example `bean('slowGreeter', SlowGreeter).lazy().scope('prototype') { ... }`.
+* Typed closure parameters (`{ MessageSource messageSource -> ... }`) become 
the generated method's parameters, so other beans are injected the same way a 
hand-written `@Bean` method would declare them — and a parameter-level 
annotation such as `{ @Qualifier('special') Greeter g -> ... }` carries 
straight through too, since the closure's own parameters become the generated 
method's parameters directly.
+
+Two more statement kinds can appear inside `beans { }` alongside `bean(...)`, 
for state and logic shared across bean methods — the same role a hand-written 
`@Configuration` class's own fields and private methods play:
+
+* `field(["name", ] Type)`, optionally chained with `.value(...)` and/or 
(repeatably) `.annotate(AnnotationType[, attr: value, ...])`, declares a 
private field on the generated class. The usual case is injected configuration, 
and `.value(...)` covers it directly: `field('encoding', 
String).value('grails.gsp.view.encoding', 'UTF-8')` compiles to 
`@Value("${grails.gsp.view.encoding:UTF-8}")`. The two-argument form takes a 
config key and default — and because the DSL builds the placeholder itself, the 
key may be a bare constant reference (`.value(Settings.GSP_VIEW_ENCODING, 
'UTF-8')`), the one shape a directly-written annotation value can't accept. The 
one-argument form takes a bare config key with no default — 
`.value('grails.web.linkGenerator.useCache')` compiles to 
`@Value("${grails.web.linkGenerator.useCache}")` — while a string already 
containing a `${...}` placeholder or a `#{...}` SpEL expression passes through 
verbatim: `.value('#{T(java.lang.Runtime).getRuntime().availab
 leProcessors()}')`.
+* `method(["name", ] Type) { ... }`, chainable with `.annotate(...)` only 
(`.value(...)` is field-specific), declares a private helper method the same 
way `bean(...)` declares a public one — typed closure parameters become method 
parameters, and the closure body becomes the method body.
+
+Both are ordinary private members of the generated class, so a `bean(...)` 
closure reads a `field(...)` or calls a `method(...)` exactly like a 
hand-written `@Bean` method would reference a sibling field or method on its 
own class:
+
+[source,groovy]
+----
+field('suffix', String).value('myapp.greeting-suffix', '!')
+
+method('buildGreeting', String) { String name ->
+    "Hello, ${name}${suffix}"
+}
+
+bean('greeting', Greeting) {
+    new Greeting(buildGreeting('World'))
+}
+----
+
+Because the result is an ordinary `AutoConfiguration` class, it must be 
registered the same way any Spring Boot auto-configuration is: listed in 
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`.
 Modules built as part of this project can apply the 
`org.apache.grails.buildsrc.autoconfiguration-imports` Gradle convention plugin 
to generate that file automatically, by scanning the module's own compiled 
classes for `@AutoConfiguration` at build time. Projects outside this build 
register the class the same way any hand-written `AutoConfiguration` is 
registered — by listing it in that file directly.
+
+NOTE: `@GrailsBeans` does not require extending `Plugin` at all — it produces 
a plain Spring Boot `AutoConfiguration`, so it can be used by any Spring Boot 
module, not just a Grails plugin descriptor. Applications can use it too, 
including directly on the `Application` class — where no imports-file 
registration is needed, since Spring Boot processes the application class 
itself — see link:spring.html#springdslAdditional[Configuring Additional 
Beans]. Within a plugin, reach for it specifically when a bean's registration 
needs to be ordered against a particular other auto-configuration; otherwise 
`beanRegistrar()` remains the recommended way to register beans from a `Plugin`.
+
+The DSL still covers a narrower surface than `doWithSpring(BeanBuilder)`:
+
+* Each top-level statement inside `beans { }` must be exactly `bean(...)`, 
`field(...)`, or `method(...)`, with their respective chaining — an `if`, a 
`for` loop, or any other imperative Groovy directly inside `beans { }` is a 
compile-time error, not a runtime one. Note that this is about the DSL's own 
top-level statements, not the bodies of `bean(...)`/`method(...)` closures 
themselves, which accept arbitrary Groovy — and `field(...)`/`method(...)` 
already cover the common reasons to want shared state or logic in the first 
place.
+* `.annotate(...)` attribute values must be simple compile-time constants 
(strings, numbers, booleans, class literals, enum constants, or arrays of 
these) — an annotation attribute that itself takes a nested annotation as a 
value isn't supported.

Review Comment:
   Rewritten in `0067d15e23` along the lines your table establishes: a bare 
constant reference does fold, and the two shapes that do not are a `static 
final` on a Groovy *class* (a property whose getter `@CompileStatic` rewrites 
the reference into) and a concatenation of constants.
   
   The consequence you predicted is fixed too, in `af64be699c`: 
`UrlMappingsGrailsPlugin` is back on `Settings.WEB_URL_CONVERTER` and 
`Settings.SETTING_CORS_FILTER`, and the comment justifying the literals is gone.



##########
grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc:
##########
@@ -119,6 +119,143 @@ class I18nGrailsPlugin extends Plugin {
 A plugin may override either `doWithSpring()` or `doWithSpring(BeanBuilder)`, 
but not both. The closure-returning `doWithSpring()` remains fully supported 
for backwards compatibility. Defining both forms in the same plugin is an 
error: the plugin will fail to load with an exception, since the two forms are 
alternatives and defining both indicates an incomplete migration between them. 
Consolidate the bean definitions into a single form.
 
 
+==== Compiling Bean Definitions into a Spring Boot AutoConfiguration
+
+
+`beanRegistrar()` and `doWithSpring` register beans at a single, fixed point 
in startup: before Spring Boot processes any 
`{springbootapi}org/springframework/boot/autoconfigure/AutoConfiguration.html[AutoConfiguration]`.
 That is enough for a bean to win against a Boot default guarded by 
`@ConditionalOnMissingBean`, but it cannot express ordering *relative to a 
specific* auto-configuration — running before `WebMvcAutoConfiguration` but 
after `MessageSourceAutoConfiguration`, say, regardless of where those two land 
in Boot's overall sort. Only a real `AutoConfiguration` class can declare that 
ordering, via `@AutoConfiguration(before = ..., after = ...)`.
+
+The link:{api}grails/compiler/beans/GrailsBeans.html[GrailsBeans] annotation 
lets you author bean definitions with DSL syntax similar to the classic Beans 
DSL, while compiling them at build time into genuine `@Bean` factory methods on 
a plain `AutoConfiguration` class — no closures, and nothing DSL-specific, 
survive into the compiled bytecode. Add `org.apache.grails:grails-beans-dsl` as 
a dependency and annotate a class whose `beans` property is a closure of 
`bean(["name", ] Type) { ... }` statements:
+
+[source,groovy]
+----
+import grails.compiler.beans.GrailsBeans
+import org.springframework.boot.autoconfigure.AutoConfiguration
+import 
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
+import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
+import org.springframework.context.MessageSource
+import 
org.springframework.context.support.ReloadableResourceBundleMessageSource
+import org.springframework.web.servlet.i18n.CookieLocaleResolver
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
+
+@GrailsBeans
+@AutoConfiguration(before = [MessageSourceAutoConfiguration, 
WebMvcAutoConfiguration])
+class I18nBeans {
+
+    def beans = {
+        bean('messageSource', ReloadableResourceBundleMessageSource) {
+            new ReloadableResourceBundleMessageSource(basename: 
'WEB-INF/grails-app/i18n/messages')
+        }
+
+        bean('localeResolver', 
CookieLocaleResolver).conditionalOnMissingBean(CookieLocaleResolver) {
+            new CookieLocaleResolver('locale')
+        }
+
+        bean('localeChangeInterceptor', LocaleChangeInterceptor) { 
MessageSource messageSource ->
+            new LocaleChangeInterceptor(paramName: 'lang')
+        }
+    }
+}
+----
+
+Each `bean(["name", ] Type)` statement compiles to a public method returning 
the declared type and annotated `@Bean("name")`. The generated method's name is 
an implementation detail — Spring resolves the bean by its `@Bean("name")` 
value, never by the method name. It matches the bean name when that is a valid 
Java identifier not already taken by an existing member (declared, inherited, 
or defined elsewhere in the DSL — a bean named `toString` will not override 
`Object.toString()`); otherwise a synthesized `<type>$N` name (`service$0`, 
`service$1`, ...) is used instead, so any legal, non-blank Spring bean name 
compiles:
+
+* The bean name defaults to the decapitalized simple class name when omitted.
+* The same bean name may be declared by more than one `bean(...)` statement — 
the standard Spring Boot pattern for mutually exclusive variants of one bean, 
such as the framework's own `grailsUrlConverter` selected by 
`@ConditionalOnProperty` — provided every declaration with the name carries its 
own discriminating condition (such as `.annotate(ConditionalOnProperty, ...)`), 
so that at most one of them registers at runtime. Duplicating a bean name 
without that is a compile-time error, since Spring would keep the first 
definition and silently skip the rest.
+* Chaining `.conditionalOnMissingBean(...)` adds a `@ConditionalOnMissingBean` 
annotation to the generated method, matching Spring Boot's usual back-off 
semantics. It takes positional types (`.conditionalOnMissingBean(Greeter)`), 
the annotation's own named attributes (`.conditionalOnMissingBean(name: 
'localeResolver', search: SearchStrategy.CURRENT)`), or both. With no arguments 
at all it compiles to the bare annotation, letting Spring Boot infer the 
back-off type from the method's return type — the recommended form when a bean 
simply backs off its own type, since 
`bean(AvailableLocaleResolver).conditionalOnMissingBean()` already says 
everything `.conditionalOnMissingBean(AvailableLocaleResolver)` would repeat.
+* Chaining `.conditionalOnMissingBeanName(...)` is the name-based counterpart: 
"register this bean unless a bean with *this bean's name* already exists". The 
condition's `name` member is set from the bean's own (explicit or 
convention-derived) name, so 
`bean(MessageSource).conditionalOnMissingBeanName(search: 
SearchStrategy.CURRENT) { ... }` states `messageSource` once and it feeds both 
`@Bean` and the condition — the two strings the explicit form has to keep in 
sync cannot diverge. It accepts the annotation's other attributes (`search:`, 
`ignored:`, ...) but rejects `name:` and types, which would contradict its 
purpose; use `.conditionalOnMissingBean(...)` for those.
+* Chaining `.primary()`, `.lazy()`, and/or `.scope("name")` adds `@Primary`, 
`@Lazy`, and `@Scope("name")` respectively.
+* Chaining `.staticMethod()` makes the generated factory method `static` — 
Spring's recommended shape for `BeanFactoryPostProcessor` and 
`BeanPostProcessor` beans, which must be creatable without instantiating their 
declaring configuration class. A static bean's closure cannot reference 
`field(...)` or `method(...)` members, since those are instance members; under 
`@CompileStatic` that mistake is a compile error.
+* Chaining `.annotate(AnnotationType[, attr: value, ...])` attaches any other 
single-valued annotation directly — any `@Conditional*` (built-in or a custom 
`Condition`), `@Order`, `@DependsOn`, or anything else a hand-written `@Bean` 
method could carry. It's repeatable, so several different annotations can be 
chained onto the same bean: `bean('special', Special).annotate(Order, value: 
1).annotate(ConditionalOnProperty, prefix: 'myapp', name: 'enabled', 
havingValue: 'true') { ... }`.
+* Any combination of the above can be chained together, in any order — for 
example `bean('slowGreeter', SlowGreeter).lazy().scope('prototype') { ... }`.
+* Typed closure parameters (`{ MessageSource messageSource -> ... }`) become 
the generated method's parameters, so other beans are injected the same way a 
hand-written `@Bean` method would declare them — and a parameter-level 
annotation such as `{ @Qualifier('special') Greeter g -> ... }` carries 
straight through too, since the closure's own parameters become the generated 
method's parameters directly.
+
+Two more statement kinds can appear inside `beans { }` alongside `bean(...)`, 
for state and logic shared across bean methods — the same role a hand-written 
`@Configuration` class's own fields and private methods play:
+
+* `field(["name", ] Type)`, optionally chained with `.value(...)` and/or 
(repeatably) `.annotate(AnnotationType[, attr: value, ...])`, declares a 
private field on the generated class. The usual case is injected configuration, 
and `.value(...)` covers it directly: `field('encoding', 
String).value('grails.gsp.view.encoding', 'UTF-8')` compiles to 
`@Value("${grails.gsp.view.encoding:UTF-8}")`. The two-argument form takes a 
config key and default — and because the DSL builds the placeholder itself, the 
key may be a bare constant reference (`.value(Settings.GSP_VIEW_ENCODING, 
'UTF-8')`), the one shape a directly-written annotation value can't accept. The 
one-argument form takes a bare config key with no default — 
`.value('grails.web.linkGenerator.useCache')` compiles to 
`@Value("${grails.web.linkGenerator.useCache}")` — while a string already 
containing a `${...}` placeholder or a `#{...}` SpEL expression passes through 
verbatim: `.value('#{T(java.lang.Runtime).getRuntime().availab
 leProcessors()}')`.
+* `method(["name", ] Type) { ... }`, chainable with `.annotate(...)` only 
(`.value(...)` is field-specific), declares a private helper method the same 
way `bean(...)` declares a public one — typed closure parameters become method 
parameters, and the closure body becomes the method body.
+
+Both are ordinary private members of the generated class, so a `bean(...)` 
closure reads a `field(...)` or calls a `method(...)` exactly like a 
hand-written `@Bean` method would reference a sibling field or method on its 
own class:
+
+[source,groovy]
+----
+field('suffix', String).value('myapp.greeting-suffix', '!')
+
+method('buildGreeting', String) { String name ->
+    "Hello, ${name}${suffix}"
+}
+
+bean('greeting', Greeting) {
+    new Greeting(buildGreeting('World'))
+}
+----
+
+Because the result is an ordinary `AutoConfiguration` class, it must be 
registered the same way any Spring Boot auto-configuration is: listed in 
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`.
 Modules built as part of this project can apply the 
`org.apache.grails.buildsrc.autoconfiguration-imports` Gradle convention plugin 
to generate that file automatically, by scanning the module's own compiled 
classes for `@AutoConfiguration` at build time. Projects outside this build 
register the class the same way any hand-written `AutoConfiguration` is 
registered — by listing it in that file directly.
+
+NOTE: `@GrailsBeans` does not require extending `Plugin` at all — it produces 
a plain Spring Boot `AutoConfiguration`, so it can be used by any Spring Boot 
module, not just a Grails plugin descriptor. Applications can use it too, 
including directly on the `Application` class — where no imports-file 
registration is needed, since Spring Boot processes the application class 
itself — see link:spring.html#springdslAdditional[Configuring Additional 
Beans]. Within a plugin, reach for it specifically when a bean's registration 
needs to be ordered against a particular other auto-configuration; otherwise 
`beanRegistrar()` remains the recommended way to register beans from a `Plugin`.
+
+The DSL still covers a narrower surface than `doWithSpring(BeanBuilder)`:
+
+* Each top-level statement inside `beans { }` must be exactly `bean(...)`, 
`field(...)`, or `method(...)`, with their respective chaining — an `if`, a 
`for` loop, or any other imperative Groovy directly inside `beans { }` is a 
compile-time error, not a runtime one. Note that this is about the DSL's own 
top-level statements, not the bodies of `bean(...)`/`method(...)` closures 
themselves, which accept arbitrary Groovy — and `field(...)`/`method(...)` 
already cover the common reasons to want shared state or logic in the first 
place.
+* `.annotate(...)` attribute values must be simple compile-time constants 
(strings, numbers, booleans, class literals, enum constants, or arrays of 
these) — an annotation attribute that itself takes a nested annotation as a 
value isn't supported.
+* Dependencies on other beans are expressed only through typed closure 
parameters (autowired by type), not by name — there is no `ref('beanName')` 
lookup.

Review Comment:
   Added in `0067d15e23`, and the "arbitrary Groovy" bullet now points at it 
rather than encouraging the mistake.
   
   Two tests: the `@CompileStatic` diagnostic for a body reading `config`, and 
the shape of the generated class that makes it so — extends `Object`, no 
`Plugin` in its hierarchy, only a no-arg constructor, no field that could hold 
one.



##########
grails-beans-dsl-plugin-example/src/main/groovy/beandsl/example/plugin/GreetingGrailsPlugin.groovy:
##########
@@ -0,0 +1,52 @@
+/*
+ *  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 beandsl.example.plugin
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.AutoConfiguration
+
+import grails.compiler.beans.GrailsBeans
+import grails.plugins.Plugin
+
+/**
+ * Demonstrates {@code field(...)} and {@code method(...)} - the same shape 
real autoconfigurations
+ * like the built-in i18n plugin's need: injected configuration shared across 
beans, and a private
+ * helper factored out of a bean's construction logic. Both compile onto the 
generated
+ * {@code GreetingGrailsPluginAutoConfiguration} sibling as ordinary private 
members, exactly like

Review Comment:
   Fixed in `af64be699c`. The suffix is replaced, so it is 
`GreetingAutoConfiguration`.
   
   Your diagnosis of why nothing caught it was exactly right, so the Greeting 
spec now asserts the FQCN the way the Farewell one does, plus that the 
`field(...)` and `method(...)` members landed on the sibling rather than the 
plugin class.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GenerateAutoConfigurationImportsTask.groovy:
##########
@@ -0,0 +1,133 @@
+/*
+ *  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.apache.grails.buildsrc
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.IgnoreEmptyDirectories
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Scans this project's own compiled main classes for {@code 
@AutoConfiguration} and writes
+ * {@code 
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports},
 so the
+ * file never needs to be hand-maintained. Mirrors how {@code 
META-INF/grails-plugin.xml} is already
+ * generated from scanned {@code *GrailsPlugin} classes elsewhere in this 
build.
+ *
+ * <p>Classes are inspected via a scratch {@link URLClassLoader} over this 
project's own runtime
+ * classpath, isolated from the Gradle daemon's classpath (parent set to the 
platform loader) so a
+ * different Spring/Groovy version on the daemon's classpath cannot shadow the 
project's own.
+ */
+abstract class GenerateAutoConfigurationImportsTask extends DefaultTask {
+
+    static final String IMPORTS_RESOURCE_PATH =
+            
'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports'
+
+    static final String AUTO_CONFIGURATION_ANNOTATION = 
'org.springframework.boot.autoconfigure.AutoConfiguration'
+
+    @InputFiles
+    @IgnoreEmptyDirectories
+    @PathSensitive(PathSensitivity.RELATIVE)
+    abstract ConfigurableFileCollection getClassesDirs()
+
+    /**
+     * The classpath the scratch classloader resolves annotations/supertypes 
against. Deliberately
+     * built from the project's compile classpath (dependencies only) rather 
than its runtime
+     * classpath: the runtime classpath includes the source set's own output, 
which - once this
+     * task's generated directory is registered on that same output - would 
make this task depend
+     * on itself.
+     */
+    @Classpath
+    abstract ConfigurableFileCollection getScanClasspath()
+
+    @OutputDirectory
+    abstract DirectoryProperty getOutputDirectory()
+
+    @TaskAction
+    void generate() {
+        SortedSet<String> discovered = scan(classesDirs.files, 
scanClasspath.files) { String className, Throwable failure ->
+            logger.warn('generateAutoConfigurationImports: could not inspect 
{} - it will be excluded from ' +
+                    'the generated imports file even if it is genuinely 
annotated @AutoConfiguration. Cause: {}',
+                    className, failure.toString())
+        }
+        File importsFile = 
outputDirectory.file(IMPORTS_RESOURCE_PATH).get().asFile
+        importsFile.parentFile.mkdirs()
+        importsFile.text = discovered.isEmpty() ? '' : discovered.join('\n') + 
'\n'
+    }
+
+    /**
+     * Package-private so it is directly unit-testable without running a real 
Gradle task.
+     *
+     * @param classesDirs directories of compiled {@code .class} files to 
inspect (a project's own
+     * output only - dependency jars on {@code classpathFiles} are never 
scanned for candidates)
+     * @param classpathFiles the classpath the scratch classloader resolves 
supertypes/annotations
+     * against; must include {@code classesDirs} plus every runtime dependency
+     * @param onUnresolvable invoked with (className, failure) for every 
candidate class that could
+     * not be loaded against {@code classpathFiles}. Defaults to a no-op so 
existing callers are
+     * unaffected; {@link #generate()} passes a callback that logs a build 
warning, since a class
+     * that fails to load here is silently excluded from the generated imports 
file - which, if it
+     * was genuinely a real {@code @AutoConfiguration}, means its beans would 
never be registered
+     * with no other signal that anything went wrong.
+     * @return the fully-qualified names of every top-level class annotated 
{@code @AutoConfiguration},
+     * sorted for a deterministic, diff-friendly output file
+     */
+    static SortedSet<String> scan(Set<File> classesDirs, Set<File> 
classpathFiles,
+            Closure<?> onUnresolvable = { String className, Throwable failure 
-> }) {
+        URL[] urls = classpathFiles.collect { it.toURI().toURL() } as URL[]
+        URLClassLoader scanLoader = new URLClassLoader(urls, 
ClassLoader.systemClassLoader.parent)
+        try {
+            Class<?> autoConfigurationAnnotation = 
Class.forName(AUTO_CONFIGURATION_ANNOTATION, false, scanLoader)
+            SortedSet<String> discovered = new TreeSet<>()
+            classesDirs.each { dir -> scanDirectory(dir, scanLoader, 
autoConfigurationAnnotation, discovered, onUnresolvable) }
+            discovered
+        }
+        finally {
+            scanLoader.close()
+        }
+    }
+
+    private static void scanDirectory(File dir, URLClassLoader scanLoader, 
Class<?> autoConfigurationAnnotation,
+            SortedSet<String> discovered, Closure<?> onUnresolvable) {
+        if (!dir.exists()) {
+            return
+        }
+        dir.eachFileRecurse(groovy.io.FileType.FILES) { file ->
+            if (!file.name.endsWith('.class') || file.name.contains('$')) {
+                return
+            }
+            String relative = dir.toPath().relativize(file.toPath()).toString()
+            String className = relative.replace(File.separator, '.') - '.class'

Review Comment:
   Fixed in `62b74b9a3f`. The extension is dropped by length now. Test uses a 
`fixture/classloading` package — the exact mangling you described — and asserts 
both that the class is found and that the unresolvable-class callback does not 
fire.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/AutoConfigurationImportsPlugin.groovy:
##########
@@ -0,0 +1,66 @@
+/*
+ *  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.apache.grails.buildsrc
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Convention plugin that wires {@link GenerateAutoConfigurationImportsTask} 
into the {@code main}
+ * source set's output, so a module authoring {@code @AutoConfiguration} 
classes never has to
+ * hand-maintain {@code 
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}.
+ *
+ * <p>The generated resources directory is registered via {@code 
sourceSets.main.output.dir(...)}
+ * rather than routed through {@code processResources}, so it lands on the 
compile/runtime/test
+ * classpath and in the final jar without depending on {@code Copy}-task 
ordering semantics.
+ */
+@CompileStatic
+class AutoConfigurationImportsPlugin implements Plugin<Project> {
+
+    static final String TASK_NAME = 'generateAutoConfigurationImports'
+    static final String GENERATED_RESOURCES_PATH = 
'generated/resources/autoConfigurationImports'
+
+    @Override
+    void apply(Project project) {
+        project.pluginManager.apply('java-base')
+
+        SourceSet main = 
project.extensions.getByType(JavaPluginExtension).sourceSets.getByName('main')
+        Provider<Directory> generatedDir = 
project.layout.buildDirectory.dir(GENERATED_RESOURCES_PATH)
+
+        TaskProvider<GenerateAutoConfigurationImportsTask> task = 
project.tasks.register(
+                TASK_NAME, GenerateAutoConfigurationImportsTask) { 
GenerateAutoConfigurationImportsTask t ->
+            t.group = 'build'
+            t.description = 'Generates 
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
 ' +
+                    'by scanning compiled classes for @AutoConfiguration'
+            t.classesDirs.from(main.output.classesDirs)
+            t.scanClasspath.from(main.compileClasspath, 
main.output.classesDirs)
+            t.outputDirectory.set(generatedDir)
+        }
+
+        main.output.dir(Collections.<String, Object> singletonMap('builtBy', 
task), generatedDir)

Review Comment:
   Both halves addressed in `62b74b9a3f`.
   
   The task now fails when a hand-maintained copy exists in the module's 
resources, naming the path and saying to delete it, so the two-copies state 
cannot be reached silently.
   
   On the second half: `grails-databinding` now applies the convention plugin 
and its hand-maintained imports file is deleted. The generated file is 
byte-identical to the one removed, the scan reports no unresolvable classes, 
and the jar carries the resource exactly once. That also answers the coverage 
gap you raised on `settings.gradle` — the plugin now has a consumer inside 
core-only CI, which matters once the example modules move out of it.



##########
grails-url-mappings/src/main/groovy/org/grails/plugins/web/mapping/UrlMappingsGrailsPlugin.groovy:
##########
@@ -51,15 +72,55 @@ class UrlMappingsGrailsPlugin extends Plugin {
     def dependsOn = [core: version]
     def loadAfter = ['controllers']
 
+    def beans = {
+        field('cacheUrls', 
Boolean).value(Settings.WEB_LINK_GENERATOR_USE_CACHE, '#{null}')
+        field('serverURL', String).value(Settings.SERVER_URL, '#{null}')
+
+        // The two mutually exclusive grailsUrlConverter variants share one 
bean name; the
+        // @ConditionalOnProperty selects which registers. Its name: attribute 
must be an inline
+        // constant, so the property names below are literals mirroring 
Settings.WEB_URL_CONVERTER

Review Comment:
   Correct, and fixed in `af64be699c`. `Settings` is an interface, so the 
constants fold under `@CompileStatic`; `Settings.WEB_URL_CONVERTER` and 
`Settings.SETTING_CORS_FILTER` are used here now and the comment justifying the 
literals is gone.
   
   The documentation bullet that sent me to literals in the first place is 
rewritten too — see my reply on `hookingIntoRuntimeConfiguration.adoc:202`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to