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
+        }
+    }
+}

Reply via email to