This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch fix/14915-beanbuilder-spring7
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 994311865b7d4c4d2c50c6d96c49c750b4b600be
Author: t <t@t>
AuthorDate: Sat Jul 4 13:45:34 2026 -0400
Keep BeanBuilder DSL independent of XML setup
Avoid eager Spring XML reader setup when evaluating ordinary BeanBuilder
DSLs so resources.groovy remains compatible with Spring Framework 7. Move XML
reader and namespace support behind a lazy helper used only by XML import and
xmlns paths.
Add grails-spring regression coverage for plain DSL, XML import, and
namespace support, and wire the module into the shared test configuration so
those tests execute. Document the compatibility behavior in the Grails 8
upgrade and Spring DSL guides.
Assisted-by: Hephaestus:openai/gpt-5.5 codex-review
---
grails-doc/src/en/guide/introduction/whatsNew.adoc | 4 +
.../src/en/guide/spring/springdslAdditional.adoc | 3 +
.../guide/spring/theBeanBuilderDSLExplained.adoc | 4 +
.../src/en/guide/upgrading/upgrading80x.adoc | 8 ++
grails-spring/build.gradle | 3 +-
.../src/main/groovy/grails/spring/BeanBuilder.java | 46 ++++-----
.../grails/spring/BeanBuilderXmlSupport.java | 99 +++++++++++++++++++
.../groovy/grails/spring/BeanBuilderTests.groovy | 105 +++++++++++++++++++++
8 files changed, 243 insertions(+), 29 deletions(-)
diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc
b/grails-doc/src/en/guide/introduction/whatsNew.adoc
index b0d4d9b49d..117cc85b34 100644
--- a/grails-doc/src/en/guide/introduction/whatsNew.adoc
+++ b/grails-doc/src/en/guide/introduction/whatsNew.adoc
@@ -39,6 +39,10 @@ This brings the Spring Boot 4 modular artifact layout,
Spring Framework 7 API re
The Grails 8 upgrade guide calls out the major application-impacting changes
and links to the Spring Boot 4.0 migration guide, Spring Boot 4.1 release notes
and Spring Framework 7.0 release notes.
+Grails 8 keeps the `resources.groovy` BeanBuilder DSL backward compatible on
Spring Framework 7.
+The standard BeanBuilder DSL path registers programmatic Spring
`BeanDefinition` instances and does not initialize Spring XML infrastructure
unless an application explicitly imports XML bean definitions or uses Spring
XML namespace DSL support through `xmlns`.
+The `resources.xml` file and BeanBuilder namespace support remain available
for compatibility, but `resources.groovy` is the recommended path for custom
bean definitions.
+
==== GSP Tag Library Improvements
Grails {grailsMajorVersion} continues the move toward method-based TagLib
handlers while preserving compatibility with existing closure-based tags.
diff --git a/grails-doc/src/en/guide/spring/springdslAdditional.adoc
b/grails-doc/src/en/guide/spring/springdslAdditional.adoc
index 7b38630f87..dd2bede6be 100644
--- a/grails-doc/src/en/guide/spring/springdslAdditional.adoc
+++ b/grails-doc/src/en/guide/spring/springdslAdditional.adoc
@@ -113,6 +113,9 @@ NOTE: If you define a bean in `resources.groovy` with the
same name as one previ
Beans can also be configured using a `grails-app/conf/spring/resources.xml`.
In earlier versions of Grails this file was automatically generated for you by
the `run-app` script, but the DSL in `resources.groovy` is the preferred
approach now so it isn't automatically generated now. But it is still supported
- you just need to create it yourself.
+In Grails 8, `resources.xml` remains supported for backward compatibility on
Spring Framework 7.
+For new bean definitions, prefer `resources.groovy`; BeanBuilder registers
standard Spring bean definitions programmatically and only initializes Spring
XML infrastructure when XML imports or Spring XML namespaces are used.
+
This file is typical Spring XML file and the Spring documentation has an
{springreference}core/beans/introduction.html[excellent reference] on how to
configure Spring beans.
The `myBean` bean that we configured using the DSL would be configured with
this syntax in the XML file:
diff --git a/grails-doc/src/en/guide/spring/theBeanBuilderDSLExplained.adoc
b/grails-doc/src/en/guide/spring/theBeanBuilderDSLExplained.adoc
index e97342e304..049eb06839 100644
--- a/grails-doc/src/en/guide/spring/theBeanBuilderDSLExplained.adoc
+++ b/grails-doc/src/en/guide/spring/theBeanBuilderDSLExplained.adoc
@@ -303,6 +303,10 @@ In this example we create an abstract bean of type
`KnightOfTheRoundTable` and u
Since Spring 2.0, users of Spring have had easier access to key features via
XML namespaces. You can use a Spring namespace in BeanBuilder by declaring it
with this syntax:
+Spring namespace support in BeanBuilder is a compatibility feature backed by
Spring's XML namespace handlers.
+BeanBuilder only initializes that XML namespace infrastructure when `xmlns` is
used.
+For ordinary bean definitions in `resources.groovy`, BeanBuilder registers
programmatic Spring bean definitions without using Spring XML configuration.
+
[source,groovy]
----
xmlns context:"https://www.springframework.org/schema/context"
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index c17cb0d63b..4bcd007475 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -57,6 +57,14 @@ For full details, consult:
3.
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.1-Release-Notes[Spring
Boot 4.1 Release Notes]
4.
https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-7.0-Release-Notes[Spring
Framework 7.0 Release Notes]
+===== BeanBuilder and Spring XML configuration
+
+No application changes are required for existing
`grails-app/conf/spring/resources.groovy` BeanBuilder DSL files.
+Grails 8 keeps that DSL backed by programmatic Spring bean definitions and
avoids Spring XML infrastructure unless an application explicitly uses
`importBeans` for XML resources or Spring XML namespace DSL support through
`xmlns`.
+
+Existing `grails-app/conf/spring/resources.xml` files remain supported for
compatibility.
+For new custom bean definitions, prefer `resources.groovy` so applications
stay on Grails' programmatic bean registration path.
+
==== 3. Spring Boot Autoconfigure Modularization
Spring Boot 4 split the monolithic `spring-boot-autoconfigure` module into
domain-specific modules.
diff --git a/grails-spring/build.gradle b/grails-spring/build.gradle
index e5318b54cd..e7f0edd4b6 100644
--- a/grails-spring/build.gradle
+++ b/grails-spring/build.gradle
@@ -61,4 +61,5 @@ dependencies {
apply {
from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
-}
\ No newline at end of file
+ from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+}
diff --git a/grails-spring/src/main/groovy/grails/spring/BeanBuilder.java
b/grails-spring/src/main/groovy/grails/spring/BeanBuilder.java
index 12b0f63787..a495e713cd 100644
--- a/grails-spring/src/main/groovy/grails/spring/BeanBuilder.java
+++ b/grails-spring/src/main/groovy/grails/spring/BeanBuilder.java
@@ -43,23 +43,14 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import
org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
-import org.springframework.beans.factory.parsing.EmptyReaderEventListener;
-import org.springframework.beans.factory.parsing.FailFastProblemReporter;
import org.springframework.beans.factory.parsing.Location;
-import org.springframework.beans.factory.parsing.NullSourceExtractor;
import org.springframework.beans.factory.parsing.Problem;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
-import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
-import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
-import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
-import org.springframework.beans.factory.xml.ParserContext;
-import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
-import org.springframework.beans.factory.xml.XmlReaderContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ByteArrayResource;
@@ -121,13 +112,11 @@ public class BeanBuilder extends GroovyObjectSupport {
private ApplicationContext parentCtx;
private Map<String, Object> binding = Collections.emptyMap();
private ClassLoader classLoader = null;
- private NamespaceHandlerResolver namespaceHandlerResolver;
private Map<String, NamespaceHandler> namespaceHandlers = new HashMap<>();
- private XmlBeanDefinitionReader xmlBeanDefinitionReader;
private Map<String, String> namespaces = new HashMap<>();
private Resource beanBuildResource = new ByteArrayResource(new byte[0]);
- private XmlReaderContext readerContext;
private ResourcePatternResolver resourcePatternResolver = new
PathMatchingResourcePatternResolver();
+ private BeanBuilderXmlSupport xmlSupport;
public BeanBuilder() {
this(null, null);
@@ -158,24 +147,22 @@ public class BeanBuilder extends GroovyObjectSupport {
}
protected void initializeSpringConfig() {
- xmlBeanDefinitionReader = new
XmlBeanDefinitionReader((GenericApplicationContext)
springConfig.getUnrefreshedApplicationContext());
- initializeBeanBuilderForClassLoader(classLoader);
+ xmlSupport = null;
}
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader == null ? getClass().getClassLoader() :
classLoader;
- initializeBeanBuilderForClassLoader(classLoader);
+ if (xmlSupport != null) {
+ xmlSupport.setClassLoader(this.classLoader);
+ }
}
protected void initializeBeanBuilderForClassLoader(ClassLoader
classLoader) {
- xmlBeanDefinitionReader.setBeanClassLoader(classLoader);
- namespaceHandlerResolver = new
DefaultNamespaceHandlerResolver(this.classLoader);
- readerContext = new XmlReaderContext(beanBuildResource, new
FailFastProblemReporter(), new EmptyReaderEventListener(),
- new NullSourceExtractor(), xmlBeanDefinitionReader,
namespaceHandlerResolver);
+ getXmlSupport().setClassLoader(classLoader);
}
public void setNamespaceHandlerResolver(NamespaceHandlerResolver
namespaceHandlerResolver) {
- this.namespaceHandlerResolver = namespaceHandlerResolver;
+ getXmlSupport().setNamespaceHandlerResolver(namespaceHandlerResolver);
}
protected RuntimeSpringConfiguration
createRuntimeSpringConfiguration(ApplicationContext parent, ClassLoader cl) {
@@ -207,9 +194,7 @@ public class BeanBuilder extends GroovyObjectSupport {
loadBeans(resource);
}
else if (resource.getFilename().endsWith(".xml")) {
- SimpleBeanDefinitionRegistry beanRegistry = new
SimpleBeanDefinitionRegistry();
- XmlBeanDefinitionReader beanReader = new
XmlBeanDefinitionReader(beanRegistry);
- beanReader.loadBeanDefinitions(resource);
+ BeanDefinitionRegistry beanRegistry =
getXmlSupport().loadBeanDefinitions(resource);
String[] beanNames = beanRegistry.getBeanDefinitionNames();
for (String beanName : beanNames) {
springConfig.addBeanDefinition(beanName,
beanRegistry.getBeanDefinition(beanName));
@@ -223,7 +208,6 @@ public class BeanBuilder extends GroovyObjectSupport {
* @param definition The definition
*/
public void xmlns(Map<String, String> definition) {
- Assert.notNull(namespaceHandlerResolver, "You cannot define a Spring
namespace without a [namespaceHandlerResolver] set");
if (definition.isEmpty()) {
return;
}
@@ -234,11 +218,11 @@ public class BeanBuilder extends GroovyObjectSupport {
Assert.notNull(uri, "Namespace definition cannot supply a null
URI");
- final NamespaceHandler namespaceHandler =
namespaceHandlerResolver.resolve(uri);
+ final NamespaceHandler namespaceHandler =
getXmlSupport().resolveNamespaceHandler(uri);
if (namespaceHandler == null) {
throw new BeanDefinitionParsingException(
new Problem("No namespace handler found for URI: " + uri,
- new Location(readerContext.getResource())));
+ new
Location(getXmlSupport().getReaderContext(beanBuildResource).getResource())));
}
namespaceHandlers.put(namespace, namespaceHandler);
namespaces.put(namespace, uri);
@@ -952,8 +936,7 @@ public class BeanBuilder extends GroovyObjectSupport {
protected DynamicElementReader createDynamicElementReader(String
namespace, final boolean decorator) {
NamespaceHandler handler = namespaceHandlers.get(namespace);
- ParserContext parserContext = new ParserContext(readerContext, new
BeanDefinitionParserDelegate(readerContext));
- final DynamicElementReader dynamicElementReader = new
DynamicElementReader(namespace, namespaces, handler, parserContext) {
+ final DynamicElementReader dynamicElementReader = new
DynamicElementReader(namespace, namespaces, handler,
getXmlSupport().createParserContext(beanBuildResource)) {
@Override
protected void afterInvocation() {
if (!decorator) {
@@ -973,6 +956,13 @@ public class BeanBuilder extends GroovyObjectSupport {
return dynamicElementReader;
}
+ private BeanBuilderXmlSupport getXmlSupport() {
+ if (xmlSupport == null) {
+ xmlSupport = new BeanBuilderXmlSupport(springConfig, classLoader);
+ }
+ return xmlSupport;
+ }
+
/**
* Sets the binding (the variables available in the scope of the
BeanBuilder).
* @param b The Binding instance
diff --git
a/grails-spring/src/main/groovy/grails/spring/BeanBuilderXmlSupport.java
b/grails-spring/src/main/groovy/grails/spring/BeanBuilderXmlSupport.java
new file mode 100644
index 0000000000..520102e294
--- /dev/null
+++ b/grails-spring/src/main/groovy/grails/spring/BeanBuilderXmlSupport.java
@@ -0,0 +1,99 @@
+/*
+ * 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 grails.spring;
+
+import org.springframework.beans.factory.parsing.EmptyReaderEventListener;
+import org.springframework.beans.factory.parsing.FailFastProblemReporter;
+import org.springframework.beans.factory.parsing.NullSourceExtractor;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
+import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
+import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
+import org.springframework.beans.factory.xml.NamespaceHandler;
+import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.beans.factory.xml.XmlReaderContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.Resource;
+
+import org.grails.spring.RuntimeSpringConfiguration;
+
+final class BeanBuilderXmlSupport {
+
+ private final RuntimeSpringConfiguration springConfig;
+ private ClassLoader classLoader;
+ private NamespaceHandlerResolver namespaceHandlerResolver;
+ private XmlBeanDefinitionReader xmlBeanDefinitionReader;
+ private XmlReaderContext readerContext;
+ private Resource readerContextResource;
+
+ BeanBuilderXmlSupport(RuntimeSpringConfiguration springConfig, ClassLoader
classLoader) {
+ this.springConfig = springConfig;
+ setClassLoader(classLoader);
+ }
+
+ void setClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ namespaceHandlerResolver = new
DefaultNamespaceHandlerResolver(classLoader);
+ xmlBeanDefinitionReader = null;
+ readerContext = null;
+ readerContextResource = null;
+ }
+
+ void setNamespaceHandlerResolver(NamespaceHandlerResolver
namespaceHandlerResolver) {
+ this.namespaceHandlerResolver = namespaceHandlerResolver;
+ readerContext = null;
+ readerContextResource = null;
+ }
+
+ NamespaceHandler resolveNamespaceHandler(String uri) {
+ return namespaceHandlerResolver.resolve(uri);
+ }
+
+ ParserContext createParserContext(Resource resource) {
+ XmlReaderContext currentReaderContext = getReaderContext(resource);
+ return new ParserContext(currentReaderContext, new
BeanDefinitionParserDelegate(currentReaderContext));
+ }
+
+ XmlReaderContext getReaderContext(Resource resource) {
+ if (readerContext == null || !resource.equals(readerContextResource)) {
+ readerContext = new XmlReaderContext(resource, new
FailFastProblemReporter(), new EmptyReaderEventListener(),
+ new NullSourceExtractor(), getXmlBeanDefinitionReader(),
namespaceHandlerResolver);
+ readerContextResource = resource;
+ }
+ return readerContext;
+ }
+
+ BeanDefinitionRegistry loadBeanDefinitions(Resource resource) {
+ SimpleBeanDefinitionRegistry beanRegistry = new
SimpleBeanDefinitionRegistry();
+ XmlBeanDefinitionReader beanReader = new
XmlBeanDefinitionReader(beanRegistry);
+ beanReader.setBeanClassLoader(classLoader);
+ beanReader.loadBeanDefinitions(resource);
+ return beanRegistry;
+ }
+
+ private XmlBeanDefinitionReader getXmlBeanDefinitionReader() {
+ if (xmlBeanDefinitionReader == null) {
+ xmlBeanDefinitionReader = new
XmlBeanDefinitionReader((GenericApplicationContext)
springConfig.getUnrefreshedApplicationContext());
+ xmlBeanDefinitionReader.setBeanClassLoader(classLoader);
+ }
+ return xmlBeanDefinitionReader;
+ }
+}
diff --git
a/grails-spring/src/test/groovy/grails/spring/BeanBuilderTests.groovy
b/grails-spring/src/test/groovy/grails/spring/BeanBuilderTests.groovy
new file mode 100644
index 0000000000..a2ebd7f3f6
--- /dev/null
+++ b/grails-spring/src/test/groovy/grails/spring/BeanBuilderTests.groovy
@@ -0,0 +1,105 @@
+/*
+ * 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 grails.spring
+
+import org.grails.spring.DefaultRuntimeSpringConfiguration
+import org.junit.jupiter.api.Test
+import org.springframework.context.ApplicationContext
+import org.springframework.core.io.ByteArrayResource
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+class BeanBuilderTests {
+
+ @Test
+ void testPlainBeanBuilderDslDoesNotInitializeXmlSupport() {
+ def springConfig = new DefaultRuntimeSpringConfiguration() {
+ @Override
+ ApplicationContext getUnrefreshedApplicationContext() {
+ throw new AssertionError('XML support should not be
initialized for plain BeanBuilder DSL')
+ }
+ }
+ def beanBuilder = new BeanBuilder(null, springConfig,
getClass().classLoader)
+
+ beanBuilder.beans {
+ bean1(Bean1) {
+ person = 'homer'
+ }
+ }
+
+ assertTrue springConfig.containsBean('bean1')
+ assertEquals Bean1,
springConfig.createBeanDefinition('bean1').beanClass
+ }
+
+ @Test
+ void testImportBeansXmlInitializesXmlSupportOnDemand() {
+ def springConfig = new DefaultRuntimeSpringConfiguration()
+ def beanBuilder = new BeanBuilder(null, springConfig,
getClass().classLoader)
+
+ beanBuilder.beans {
+ importBeans new NamedByteArrayResource('''
+ <beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
+ <bean id="xmlBean" class="java.lang.String">
+ <constructor-arg value="hello"/>
+ </bean>
+ </beans>
+ '''.bytes, 'test.xml')
+ }
+
+ assertTrue springConfig.containsBean('xmlBean')
+ assertEquals String,
springConfig.createBeanDefinition('xmlBean').beanClass
+ }
+
+ @Test
+ void testXmlnsInitializesNamespaceSupportOnDemand() {
+ def beanBuilder = new BeanBuilder()
+
+ beanBuilder.beans {
+ xmlns util: 'http://www.springframework.org/schema/util'
+
+ util.list(id: 'letters') {
+ value 'one'
+ value 'two'
+ }
+ }
+
+ assertEquals ['one', 'two'],
beanBuilder.createApplicationContext().getBean('letters')
+ }
+
+ static class Bean1 {
+ String person
+ }
+
+ private static class NamedByteArrayResource extends ByteArrayResource {
+ private final String filename
+
+ NamedByteArrayResource(byte[] byteArray, String filename) {
+ super(byteArray)
+ this.filename = filename
+ }
+
+ @Override
+ String getFilename() {
+ filename
+ }
+ }
+}