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]