diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc b/grails-doc/src/en/guide/introduction/whatsNew.adoc index 482b962915d..5404d41832b 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 7b38630f87d..dd2bede6be0 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 e97342e3041..049eb068397 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 37e6b139e90..750d23eab50 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 e0aaa04a5bc..3e813cf6b9c 100644 --- a/grails-spring/build.gradle +++ b/grails-spring/build.gradle @@ -62,4 +62,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 12b0f637875..78b3375c9a9 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.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 binding = Collections.emptyMap(); private ClassLoader classLoader = null; - private NamespaceHandlerResolver namespaceHandlerResolver; private Map namespaceHandlers = new HashMap<>(); - private XmlBeanDefinitionReader xmlBeanDefinitionReader; private Map 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 void setResourcePatternResolver(ResourcePatternResolver resourcePatternRe } 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 void importBeans(Resource resource) { 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 @@ else if (resource.getFilename().endsWith(".xml")) { * @param definition The definition */ public void xmlns(Map definition) { - Assert.notNull(namespaceHandlerResolver, "You cannot define a Spring namespace without a [namespaceHandlerResolver] set"); if (definition.isEmpty()) { return; } @@ -234,11 +218,11 @@ public void xmlns(Map definition) { 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(beanBuildResource))); } namespaceHandlers.put(namespace, namespaceHandler); namespaces.put(namespace, uri); @@ -952,8 +936,7 @@ public Object getProperty(String name) { 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 @@ else if (!decorator) { 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 00000000000..520102e294e --- /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 00000000000..4ddb56392d3 --- /dev/null +++ b/grails-spring/src/test/groovy/grails/spring/BeanBuilderTests.groovy @@ -0,0 +1,126 @@ +/* + * 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.beans.factory.parsing.BeanDefinitionParsingException +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.assertThrows +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(''' + + + + + + '''.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') + } + + @Test + void testUnknownNamespaceDoesNotInitializeXmlReaderForErrorLocation() { + def springConfig = new DefaultRuntimeSpringConfiguration() { + @Override + ApplicationContext getUnrefreshedApplicationContext() { + throw new AssertionError('XML reader should not be initialized when namespace handler is missing') + } + } + def beanBuilder = new BeanBuilder(null, springConfig, getClass().classLoader) + + def exception = assertThrows(BeanDefinitionParsingException) { + beanBuilder.beans { + xmlns missing: 'https://example.invalid/schema/missing' + } + } + + assertTrue exception.message.contains('No namespace handler found for URI') + } + + 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 + } + } +}