Port GroovyDynamicElementReader to Java

Prior to this commit, GroovyDynamicElementReader was implemented in
Groovy, which required that developers have Groovy language support
installed and configured in their IDEs.

This commit ports GroovyDynamicElementReader from Groovy to Java,
making the compilation much simpler and allowing developers to open the
project in Eclipse or VSCode (or other IDEs without Groovy support)
without compiler errors.

This commit also converts related tests from Groovy to Java.

Closes gh-27945
This commit is contained in:
Dave Syer 2022-01-18 11:18:15 +00:00 committed by Sam Brannen
parent 912bb16e44
commit f7c3706361
9 changed files with 1465 additions and 1230 deletions

View File

@ -1,6 +1,5 @@
description = "Spring Beans"
apply plugin: "groovy"
apply plugin: "kotlin"
dependencies {
@ -14,25 +13,4 @@ dependencies {
testImplementation("jakarta.annotation:jakarta.annotation-api")
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
testFixturesImplementation("org.assertj:assertj-core")
}
// This module does joint compilation for Java and Groovy code with the compileGroovy task.
sourceSets {
main.groovy.srcDirs += "src/main/java"
main.java.srcDirs = []
}
compileGroovy {
options.compilerArgs += "-Werror"
}
// This module also builds Kotlin code and the compileKotlin task naturally depends on
// compileJava. We need to redefine dependencies to break task cycles.
tasks.named('compileGroovy') {
// Groovy only needs the declared dependencies (and not the result of Java compilation)
classpath = sourceSets.main.compileClasspath
}
tasks.named('compileKotlin') {
// Kotlin also depends on the result of Groovy compilation
classpath += files(sourceSets.main.groovy.classesDirectory)
}
}

View File

@ -1,125 +0,0 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed 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.springframework.beans.factory.groovy
import groovy.xml.StreamingMarkupBuilder
import org.springframework.beans.factory.config.BeanDefinitionHolder
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate
import org.w3c.dom.Element
/**
* Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression
* in the Groovy DSL.
*
* @author Jeff Brown
* @author Juergen Hoeller
* @since 4.0
*/
@groovy.transform.PackageScope
class GroovyDynamicElementReader extends GroovyObjectSupport {
private final String rootNamespace
private final Map<String, String> xmlNamespaces
private final BeanDefinitionParserDelegate delegate
private final GroovyBeanDefinitionWrapper beanDefinition
protected final Boolean decorating;
private boolean callAfterInvocation = true
public GroovyDynamicElementReader(String namespace, Map<String, String> namespaceMap,
BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) {
super();
this.rootNamespace = namespace
this.xmlNamespaces = namespaceMap
this.delegate = delegate
this.beanDefinition = beanDefinition;
this.decorating = decorating;
}
@Override
public Object invokeMethod(String name, Object args) {
if (name.equals("doCall")) {
def callable = args[0]
callable.resolveStrategy = Closure.DELEGATE_FIRST
callable.delegate = this
def result = callable.call()
if (this.callAfterInvocation) {
afterInvocation()
this.callAfterInvocation = false
}
return result
}
else {
StreamingMarkupBuilder builder = new StreamingMarkupBuilder();
def myNamespace = this.rootNamespace
def myNamespaces = this.xmlNamespaces
def callable = {
for (namespace in myNamespaces) {
mkp.declareNamespace([(namespace.key):namespace.value])
}
if (args && (args[-1] instanceof Closure)) {
args[-1].resolveStrategy = Closure.DELEGATE_FIRST
args[-1].delegate = builder
}
delegate."$myNamespace"."$name"(*args)
}
callable.resolveStrategy = Closure.DELEGATE_FIRST
callable.delegate = builder
def writable = builder.bind(callable)
def sw = new StringWriter()
writable.writeTo(sw)
Element element = this.delegate.readerContext.readDocumentFromString(sw.toString()).documentElement
this.delegate.initDefaults(element)
if (this.decorating) {
BeanDefinitionHolder holder = this.beanDefinition.beanDefinitionHolder;
holder = this.delegate.decorateIfRequired(element, holder, null)
this.beanDefinition.setBeanDefinitionHolder(holder)
}
else {
def beanDefinition = this.delegate.parseCustomElement(element)
if (beanDefinition) {
this.beanDefinition.setBeanDefinition(beanDefinition)
}
}
if (this.callAfterInvocation) {
afterInvocation()
this.callAfterInvocation = false
}
return element
}
}
/**
* Hook that subclass or anonymous classes can overwrite to implement custom behavior
* after invocation completes.
*/
protected void afterInvocation() {
// NOOP
}
}

View File

@ -541,9 +541,8 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
* @param ann the Autowired annotation
* @return whether the annotation indicates that a dependency is required
*/
@SuppressWarnings("cast")
protected boolean determineRequiredStatus(MergedAnnotation<?> ann) {
// Cast to (AnnotationAttributes) is required. Otherwise, the :spring-beans:compileGroovy
// task fails in the Gradle build.
return determineRequiredStatus((AnnotationAttributes)
ann.asMap(mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType())));
}

View File

@ -0,0 +1,144 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed 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.springframework.beans.factory.groovy;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import groovy.lang.Closure;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.Writable;
import groovy.xml.StreamingMarkupBuilder;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
/**
* Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression
* in the Groovy DSL.
*
* @author Jeff Brown
* @author Juergen Hoeller
* @author Dave Syer
* @since 4.0
*/
class GroovyDynamicElementReader extends GroovyObjectSupport {
private final String rootNamespace;
private final Map<String, String> xmlNamespaces;
private final BeanDefinitionParserDelegate delegate;
private final GroovyBeanDefinitionWrapper beanDefinition;
protected final boolean decorating;
private boolean callAfterInvocation = true;
public GroovyDynamicElementReader(String namespace, Map<String, String> namespaceMap,
BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) {
super();
this.rootNamespace = namespace;
this.xmlNamespaces = namespaceMap;
this.delegate = delegate;
this.beanDefinition = beanDefinition;
this.decorating = decorating;
}
@Override
public Object invokeMethod(String name, Object obj) {
Object[] args = (Object[]) obj;
if (name.equals("doCall")) {
@SuppressWarnings("unchecked")
Closure<Object> callable = (Closure<Object>) args[0];
callable.setResolveStrategy(Closure.DELEGATE_FIRST);
callable.setDelegate(this);
Object result = callable.call();
if (this.callAfterInvocation) {
afterInvocation();
this.callAfterInvocation = false;
}
return result;
}
else {
StreamingMarkupBuilder builder = new StreamingMarkupBuilder();
String myNamespace = this.rootNamespace;
Map<String, String> myNamespaces = this.xmlNamespaces;
Closure<Object> callable = new Closure<>(this) {
@Override
public Object call(Object... arguments) {
((GroovyObject) getProperty("mkp")).invokeMethod("declareNamespace", new Object[] {myNamespaces});
int len = args.length;
if (len > 0 && args[len-1] instanceof Closure<?> callable) {
callable.setResolveStrategy(Closure.DELEGATE_FIRST);
callable.setDelegate(builder);
}
return ((GroovyObject) ((GroovyObject) getDelegate()).getProperty(myNamespace)).invokeMethod(name, args);
}
};
callable.setResolveStrategy(Closure.DELEGATE_FIRST);
callable.setDelegate(builder);
Writable writable = (Writable) builder.bind(callable);
StringWriter sw = new StringWriter();
try {
writable.writeTo(sw);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
Element element = this.delegate.getReaderContext().readDocumentFromString(sw.toString()).getDocumentElement();
this.delegate.initDefaults(element);
if (this.decorating) {
BeanDefinitionHolder holder = this.beanDefinition.getBeanDefinitionHolder();
holder = this.delegate.decorateIfRequired(element, holder, null);
this.beanDefinition.setBeanDefinitionHolder(holder);
}
else {
BeanDefinition beanDefinition = this.delegate.parseCustomElement(element);
if (beanDefinition != null) {
this.beanDefinition.setBeanDefinition((AbstractBeanDefinition) beanDefinition);
}
}
if (this.callAfterInvocation) {
afterInvocation();
this.callAfterInvocation = false;
}
return element;
}
}
/**
* Hook that subclasses or anonymous classes can override to implement custom behavior
* after invocation completes.
*/
protected void afterInvocation() {
// NOOP
}
}

View File

@ -1935,6 +1935,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
*
* @since 5.3
*/
@SuppressWarnings("serial")
private class BeanPostProcessorCacheAwareList extends CopyOnWriteArrayList<BeanPostProcessor> {
@Override

View File

@ -1,6 +1,5 @@
description = "Spring Context"
apply plugin: "groovy"
apply plugin: "kotlin"
dependencies {

View File

@ -14,14 +14,16 @@
* limitations under the License.
*/
package org.springframework.context.groovy
package org.springframework.context.groovy;
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException
import org.springframework.context.support.GenericGroovyApplicationContext
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.support.GenericGroovyApplicationContext;
import static groovy.test.GroovyAssert.*
import static groovy.test.GroovyAssert.assertEquals;
import static groovy.test.GroovyAssert.assertNotNull;
import static groovy.test.GroovyAssert.assertThrows;
/**
* @author Jeff Brown
@ -31,24 +33,26 @@ class GroovyApplicationContextDynamicBeanPropertyTests {
@Test
void testAccessDynamicBeanProperties() {
def ctx = new GenericGroovyApplicationContext();
ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy");
ctx.refresh()
var ctx = new GenericGroovyApplicationContext();
ctx.getReader().loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy");
ctx.refresh();
def framework = ctx.framework
assertNotNull 'could not find framework bean', framework
assertEquals 'Grails', framework
var framework = ctx.getProperty("framework");
assertNotNull("could not find framework bean", framework);
assertEquals("Grails", framework);
ctx.close();
}
@Test
void testAccessingNonExistentBeanViaDynamicProperty() {
def ctx = new GenericGroovyApplicationContext();
ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy");
ctx.refresh()
var ctx = new GenericGroovyApplicationContext();
ctx.getReader().loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy");
ctx.refresh();
def err = shouldFail NoSuchBeanDefinitionException, { ctx.someNonExistentBean }
var err = assertThrows(NoSuchBeanDefinitionException.class, () -> ctx.getProperty("someNonExistentBean"));
assertEquals "No bean named 'someNonExistentBean' available", err.message
assertEquals("No bean named 'someNonExistentBean' available", err.getMessage());
ctx.close();
}
}