GROOVY-5410: handle method selection for proxy object more carefully
getMetaClass() is passed on to InvocationHandler and $ProvyN is lost:
def dp = Proxy.newProxyInstance(..., invocationHandler)
def mc = dp.getMetaClass()
def c = mc.getTheClass()
"c" refers to class of whatever the handler returned a meta class for
diff --git a/src/main/java/org/codehaus/groovy/runtime/callsite/CallSiteArray.java b/src/main/java/org/codehaus/groovy/runtime/callsite/CallSiteArray.java
index 5fe58fb..a4ffc89 100644
--- a/src/main/java/org/codehaus/groovy/runtime/callsite/CallSiteArray.java
+++ b/src/main/java/org/codehaus/groovy/runtime/callsite/CallSiteArray.java
@@ -26,6 +26,7 @@
import org.codehaus.groovy.runtime.GroovyCategorySupport;
import org.codehaus.groovy.runtime.InvokerHelper;
+import java.lang.reflect.Proxy;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.stream.IntStream;
@@ -109,6 +110,16 @@
return site;
}
+ private static CallSite createProxySite(final CallSite callSite, final Object receiver) {
+ if (receiver instanceof GroovyObject) {
+ return new PogoInterceptableSite(callSite);
+ } else {
+ ClassInfo classInfo = ClassInfo.getClassInfo(receiver.getClass());
+ MetaClass metaClass = classInfo.getMetaClass(receiver);
+ return new PojoMetaClassSite(callSite, metaClass);
+ }
+ }
+
// for MetaClassImpl we try to pick meta method,
// otherwise or if method doesn't exist we make call via POJO meta class
private static CallSite createPojoSite(CallSite callSite, Object receiver, Object[] args) {
@@ -154,6 +165,8 @@
CallSite site;
if (receiver instanceof Class) {
site = createCallStaticSite(callSite, (Class) receiver, args);
+ } else if (Proxy.isProxyClass(receiver.getClass())) {
+ site = createProxySite(callSite, receiver);
} else if (receiver instanceof GroovyObject) {
site = createPogoSite(callSite, receiver, args);
} else {
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
index f0761e1..aecfba8 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -62,6 +62,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -568,15 +569,20 @@
Object receiver = args[0];
if (receiver == null) {
mc = NullObject.getNullObject().getMetaClass();
- } else if (receiver instanceof GroovyObject) {
- mc = ((GroovyObject) receiver).getMetaClass();
} else if (receiver instanceof Class) {
Class<?> c = (Class<?>) receiver;
mc = GroovySystem.getMetaClassRegistry().getMetaClass(c);
- this.cache &= !ClassInfo.getClassInfo(c).hasPerInstanceMetaClasses();
+ cache &= !ClassInfo.getClassInfo(c).hasPerInstanceMetaClasses();
+ } else if (Proxy.isProxyClass(receiver.getClass())) {
+ // GROOVY-5410: receiver.getMetaClass() returns meta class for proxied object;
+ // later when metaClass.getTheClass() is called, the $ProxyN reference is lost
+ mc = GroovySystem.getMetaClassRegistry().getMetaClass(receiver.getClass());
+ cache = false;
+ } else if (receiver instanceof GroovyObject) {
+ mc = ((GroovyObject) receiver).getMetaClass();
} else {
mc = ((MetaClassRegistryImpl) GroovySystem.getMetaClassRegistry()).getMetaClass(receiver);
- this.cache &= !ClassInfo.getClassInfo(receiver.getClass()).hasPerInstanceMetaClasses();
+ cache &= !ClassInfo.getClassInfo(receiver.getClass()).hasPerInstanceMetaClasses();
}
mc.initialize();
diff --git a/src/test/groovy/bugs/Groovy5410.groovy b/src/test/groovy/bugs/Groovy5410.groovy
new file mode 100644
index 0000000..6365ab0
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy5410.groovy
@@ -0,0 +1,209 @@
+/*
+ * 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
+ *
+ * http://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 groovy.bugs
+
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.tools.javac.JavaAwareCompilationUnit
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized)
+final class Groovy5410 {
+
+ @Parameterized.Parameter
+ public boolean invokeDynamic
+
+ @Parameterized.Parameters
+ static Iterable parameters() {
+ [Boolean.FALSE, Boolean.TRUE]
+ }
+
+ @Test
+ void testDynamicProxy() {
+ def config = new CompilerConfiguration(
+ targetDirectory: File.createTempDir(),
+ optimizationOptions: [indy: invokeDynamic],
+ jointCompilationOptions: [memStub: Boolean.TRUE]
+ )
+ def parentDir = File.createTempDir()
+ try {
+ def a = new File(parentDir, 'A.java')
+ a.write '''
+ interface JavaParentIfc<X> {
+ void parentMethod(X input);
+ void parentMethod2(ChildGroovyObject input);
+ }
+
+ interface JavaChildExtendsJavaParentIfc extends JavaParentIfc<ChildGroovyObject> {
+ void childMethod(ChildGroovyObject input);
+ }
+
+ class JavaChildExtendsJavaParentImpl implements JavaChildExtendsJavaParentIfc {
+ @Override
+ public void childMethod(ChildGroovyObject input) {
+ System.out.println("in child method impl with input: " + input.getTestString());
+ }
+ @Override
+ public void parentMethod(ChildGroovyObject input) {
+ System.out.println("in parent method impl with input: " + input.getTestString());
+ }
+ @Override
+ public void parentMethod2(ChildGroovyObject input) {
+ System.out.println("in parent method 2(not generic) impl with input: " + input.getTestString());
+ }
+ }
+
+ class ProxyInvocationHandler implements java.lang.reflect.InvocationHandler {
+ public ProxyInvocationHandler(Object realChild) {
+ this.realChild = realChild;
+ }
+ Object realChild;
+
+ @Override
+ public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
+ try {
+ return method.invoke(realChild, args);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ }
+ '''
+ def b = new File(parentDir, 'B.groovy')
+ b.write '''
+ class ChildGroovyObject {
+ String testString
+ }
+
+ interface GroovyParentIfc<Y> {
+ void parentMethod(Y input)
+ void parentMethod2(ChildGroovyObject input)
+ }
+
+ interface GroovyChildExtendsGroovyParentIfc extends GroovyParentIfc<ChildGroovyObject> {
+ void childMethod(ChildGroovyObject input)
+ }
+
+ class GroovyChildExtendsGroovyParentImpl implements GroovyChildExtendsGroovyParentIfc {
+ @Override
+ void childMethod(ChildGroovyObject input) {
+ println "in child method impl with input: $input.testString"
+ }
+ @Override
+ void parentMethod(ChildGroovyObject input) {
+ println "in parent method impl with input: $input.testString"
+ }
+ @Override
+ void parentMethod2(ChildGroovyObject input) {
+ println "in parent method 2(not generic) impl with input: $input.testString"
+ }
+ }
+
+ interface GroovyChildExtendsJavaParentIfc extends JavaParentIfc<ChildGroovyObject> {
+ void childMethod(ChildGroovyObject input)
+ }
+
+ class GroovyChildExtendsJavaParentImpl implements GroovyChildExtendsJavaParentIfc {
+ @Override
+ void childMethod(ChildGroovyObject input) {
+ println "in child method impl with input: $input.testString"
+ }
+ @Override
+ void parentMethod(ChildGroovyObject input) {
+ println "in parent method impl with input: $input.testString"
+ }
+ @Override
+ void parentMethod2(ChildGroovyObject input) {
+ println "in parent method 2(not generic) impl with input: $input.testString"
+ }
+ }
+ '''
+ def c = new File(parentDir, 'C.groovy')
+ c.write '''
+ import static java.lang.reflect.Proxy.newProxyInstance
+
+ void 'WORKS - call concrete child method'() {
+ def proxiedChild = new GroovyChildExtendsGroovyParentImpl()
+ ProxyInvocationHandler handler = new ProxyInvocationHandler(proxiedChild)
+ Class[] interfacesArray = [GroovyChildExtendsGroovyParentIfc, GroovyParentIfc, GroovyObject]
+ GroovyChildExtendsGroovyParentIfc child = (GroovyChildExtendsGroovyParentIfc) newProxyInstance(GroovyChildExtendsGroovyParentIfc.class.classLoader, interfacesArray, handler)
+
+ child.childMethod(new ChildGroovyObject(testString: 'calling child method from spec'))
+ }
+
+ void 'WORKS - call concrete parent method'() {
+ def proxiedChild = new GroovyChildExtendsGroovyParentImpl()
+ ProxyInvocationHandler handler = new ProxyInvocationHandler(proxiedChild)
+ Class[] interfacesArray = [GroovyChildExtendsGroovyParentIfc, GroovyParentIfc, GroovyObject]
+ GroovyChildExtendsGroovyParentIfc child = (GroovyChildExtendsGroovyParentIfc) newProxyInstance(GroovyChildExtendsGroovyParentIfc.class.classLoader, interfacesArray, handler)
+
+ child.parentMethod2(new ChildGroovyObject(testString: 'calling child method from spec'))
+ }
+
+ void 'ISSUE - call generic groovy parent interface method that throws a ClassCastException but should not'() {
+ def proxiedChild = new GroovyChildExtendsGroovyParentImpl()
+ ProxyInvocationHandler handler = new ProxyInvocationHandler(proxiedChild)
+ Class[] interfacesArray = [GroovyChildExtendsGroovyParentIfc, GroovyParentIfc, GroovyObject]
+ GroovyChildExtendsGroovyParentIfc child = (GroovyChildExtendsGroovyParentIfc) newProxyInstance(GroovyChildExtendsGroovyParentIfc.class.classLoader, interfacesArray, handler)
+
+ child.parentMethod(new ChildGroovyObject(testString: 'calling parent method from spec')) // line 28
+ }
+
+ void 'ISSUE - call generic java parent interface method that throws a ClassCastException but should not'() {
+ def proxiedChild = new GroovyChildExtendsJavaParentImpl()
+ ProxyInvocationHandler handler = new ProxyInvocationHandler(proxiedChild)
+ Class[] interfacesArray = [GroovyChildExtendsJavaParentIfc, JavaParentIfc, GroovyObject]
+ GroovyChildExtendsJavaParentIfc child = (GroovyChildExtendsJavaParentIfc) newProxyInstance(GroovyChildExtendsJavaParentIfc.class.classLoader, interfacesArray, handler)
+
+ child.parentMethod(new ChildGroovyObject(testString: 'calling parent method from spec')) // line 37
+ }
+
+ void 'ISSUE - call generic java parent interface method when the child interface is also java and this also throws a ClassCastException because GroovyObject is passed as an interface to the Proxy'() {
+ def proxiedChild = new JavaChildExtendsJavaParentImpl()
+ ProxyInvocationHandler handler = new ProxyInvocationHandler(proxiedChild)
+
+ // It might seem strange that I'm including GroovyObject as an interface here, but when Spring proxies it's beans,
+ // it looks at the interfaces of the bean class which happens to be written in Groovy and therefore is an implicit
+ // implementation of a GroovyObject
+ Class[] interfacesArray = [JavaChildExtendsJavaParentIfc, JavaParentIfc, GroovyObject]
+ JavaChildExtendsJavaParentIfc child = (JavaChildExtendsJavaParentIfc) newProxyInstance(JavaChildExtendsJavaParentIfc.class.classLoader, interfacesArray, handler)
+
+ child.parentMethod(new ChildGroovyObject(testString: 'calling parent method from spec')) // line 50
+ }
+
+ 'WORKS - call concrete child method'()
+ 'WORKS - call concrete parent method'()
+ 'ISSUE - call generic groovy parent interface method that throws a ClassCastException but should not'()
+ 'ISSUE - call generic java parent interface method that throws a ClassCastException but should not'()
+ 'ISSUE - call generic java parent interface method when the child interface is also java and this also throws a ClassCastException because GroovyObject is passed as an interface to the Proxy'()
+ '''
+
+ def loader = new GroovyClassLoader(this.class.classLoader)
+ def cu = new JavaAwareCompilationUnit(config, loader)
+ cu.addSources(a, b, c)
+ cu.compile()
+
+ loader.loadClass('C', true).main()
+ } finally {
+ config.targetDirectory.deleteDir()
+ parentDir.deleteDir()
+ }
+ }
+}