Skip to content

Commit 9d5fd19

Browse files
committed
GROOVY-11999: ProxyGeneratorAdapter NPE when proxy interfaces mix bootstrap and user classloaders
1 parent 566c8f2 commit 9d5fd19

3 files changed

Lines changed: 62 additions & 3 deletions

File tree

src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -840,11 +840,16 @@ protected InnerLoader(final ClassLoader parent, final Class[] interfaces) {
840840
super(parent);
841841
if (interfaces != null) {
842842
for (Class<?> c : interfaces) {
843-
if (c.getClassLoader() != parent) {
843+
// GROOVY-11999: bootstrap-loaded classes (e.g., java.lang.Runnable,
844+
// java.io.Serializable) report a null classloader. Don't store
845+
// null in the extras list — bootstrap is reachable via every
846+
// non-null parent's delegation chain anyway.
847+
ClassLoader cl = c.getClassLoader();
848+
if (cl != null && cl != parent) {
844849
if (internalClassLoaders == null)
845850
internalClassLoaders = new ArrayList<>(interfaces.length);
846-
if (!internalClassLoaders.contains(c.getClassLoader())) {
847-
internalClassLoaders.add(c.getClassLoader());
851+
if (!internalClassLoaders.contains(cl)) {
852+
internalClassLoaders.add(cl);
848853
}
849854
}
850855
}
@@ -884,6 +889,7 @@ public Class<?> loadClass(final String name) throws ClassNotFoundException {
884889
// Not loaded, try to load it
885890
if (internalClassLoaders != null) {
886891
for (ClassLoader i : internalClassLoaders) {
892+
if (i == null) continue; // GROOVY-11999: defensive, see InnerLoader ctor
887893
try {
888894
// Ignore parent delegation and just try to load locally
889895
loadedClass = i.loadClass(name);

src/test/groovy/groovy/lang/IntersectionClosureLiteralTest.groovy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ final class IntersectionClosureLiteralTest {
9696
''')
9797
}
9898
99+
// GROOVY-11999: this previously NPE'd inside ProxyGeneratorAdapter when
100+
// interfaces list mixed bootstrap (Runnable) and user (MyMarker) loaders.
101+
@Test
102+
void 'dynamic closure as (Runnable & MyMarker) builds a multi-interface proxy'() {
103+
def shell = new GroovyShell()
104+
shell.evaluate('''
105+
interface MyMarker {}
106+
def c = ({ -> "hi" } as (Runnable & MyMarker))
107+
assert c instanceof Runnable
108+
assert c instanceof MyMarker
109+
c.run()
110+
''')
111+
}
112+
99113
@Test
100114
void 'static cast form succeeds where dynamic would need a runtime proxy'() {
101115
// Without PR5, (R & MyMarker) cast form on a closure literal would throw

src/test/groovy/groovy/util/ProxyGeneratorAdapterTest.groovy

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,43 @@ class ProxyGeneratorAdapterTest {
244244
static interface OtherInterface {
245245
int calc(int x)
246246
}
247+
248+
static interface UserMarker {} // user-defined marker; classloader is the test classloader
249+
250+
// GROOVY-11999: building a proxy whose interface list mixes a bootstrap-loaded
251+
// interface (Runnable/Serializable) with a user-defined one used to NPE in
252+
// InnerLoader because the bootstrap classloader (null) was added to the
253+
// internalClassLoaders list and dereferenced during class definition.
254+
@Test
255+
void testProxyMixingBootstrapAndUserInterfaces() {
256+
def closure = { -> /* doCall */ }
257+
def closureMap = ['*': closure]
258+
def adapter = new ProxyGeneratorAdapter(
259+
closureMap,
260+
Object,
261+
[Runnable, UserMarker] as Class[],
262+
this.class.classLoader,
263+
false,
264+
null)
265+
def obj = adapter.proxy(closureMap, null)
266+
assert obj instanceof Runnable
267+
assert obj instanceof UserMarker
268+
obj.run() // does not throw
269+
}
270+
271+
// GROOVY-11999: same scenario via the public ProxyGenerator entry point used
272+
// by the runtime intersection-cast path (IntersectionCastSupport.asType).
273+
@Test
274+
void testInstantiateAggregateMixingBootstrapAndUserInterfaces() {
275+
def calls = 0
276+
def proxy = ProxyGenerator.INSTANCE.instantiateAggregate(
277+
['run': { -> calls++ }],
278+
[Runnable, java.io.Serializable, UserMarker] as List<Class>)
279+
assert proxy instanceof Runnable
280+
assert proxy instanceof java.io.Serializable
281+
assert proxy instanceof UserMarker
282+
proxy.run()
283+
proxy.run()
284+
assert calls == 2
285+
}
247286
}

0 commit comments

Comments
 (0)