diff --git a/src/main/java/org/apache/groovy/util/HiddenClassDefiner.java b/src/main/java/org/apache/groovy/util/HiddenClassDefiner.java
new file mode 100644
index 00000000000..4da39dd3142
--- /dev/null
+++ b/src/main/java/org/apache/groovy/util/HiddenClassDefiner.java
@@ -0,0 +1,282 @@
+/*
+ * 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 org.apache.groovy.util;
+
+import groovy.transform.Internal;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.SimpleRemapper;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.util.Collections;
+
+/**
+ * Central facility for defining hidden classes
+ * (JEP 371).
+ *
+ *
Lookup ownership
+ *
{@link MethodHandles#lookup()} is caller-sensitive: it returns a
+ * full-privilege lookup only for the class that literally contains the call.
+ * Production call sites capture a lookup on the intended nest host:
+ *
+ * {@link Lookup#lookupClass()} fixes the hidden class's defining loader,
+ * package, protection domain, and nest host. Lookups from different runtime
+ * classes are not interchangeable as nest hosts even though they share the same
+ * module rights (see below).
+ *
+ *
The lookup captured inside this class is used only as the caller
+ * argument to {@link MethodHandles#privateLookupIn(Class, Lookup)} for the
+ * foreign-host overload — never as a production nest host.
+ *
+ *
Module rights vs nest host
+ *
Every production call site lives in the Groovy runtime, so {@code lookup()}
+ * always grants the same module-level access. Capturing it in different runtime
+ * classes does not open a third-party module that never opened itself to the
+ * runtime. What differs is the nest host (package / loader / nest membership),
+ * which still matters for unloadability and linkage.
+ *
+ *
What nestmates cover (modules A / B / C)
+ *
With modules A (Java library), B (Groovy program), C (Groovy runtime):
+ *
+ *
Caller-owned lookup
+ * ({@link #tryDefineNestmate(Lookup, byte[], boolean)}): nestmate of a
+ * runtime class. Works when every type named by the bytecode is resolvable
+ * from the runtime loader. No private access into foreign modules.
+ *
Foreign host
+ * ({@link #tryDefineNestmate(Class, byte[], boolean)}): best-effort
+ * {@code privateLookupIn}. Succeeds when the host package is open to the
+ * runtime (typical for unnamed-module application classes); not for
+ * strongly encapsulated packages such as {@code java.lang}
+ * ({@link String} is the counter-example). Callers must handle
+ * {@code null} and fall back to {@link ClassLoader#defineClass}.
+ *
+ * Nestmates cover C and often open/unnamed B. They do not tunnel private access
+ * into A. Visible {@code defineClass} is the intentional safety net.
+ *
+ *
Soft-fail contract
+ *
{@code try*} methods return {@code null} on expected failures
+ * ({@link IllegalAccessException}, {@link SecurityException}, {@link LinkageError},
+ * invalid class-file / ASM exceptions, GraalVM {@code UnsupportedFeatureError}
+ * matched by class name). Other {@link Error}s are rethrown.
+ *
+ *
Enablement
+ *
{@link #isEnabled()} is evaluated per call so
+ * {@code -Dgroovy.hidden.classes.disable=true} works under native-image
+ * build-time init, and is always {@code false} when
+ * {@code org.graalvm.nativeimage.imagecode=runtime}.
+ *
+ * @since 6.0.0
+ * @see Lookup#defineHiddenClass(byte[], boolean, Lookup.ClassOption...)
+ */
+public final class HiddenClassDefiner {
+
+ /** System property that disables hidden-class definitions at run time. */
+ public static final String PROPERTY_DISABLE = "groovy.hidden.classes.disable";
+
+ /**
+ * GraalVM property set in native images. Value {@code "runtime"} means the
+ * current process is executing an already-built native image. Not a public
+ * configuration surface — use {@link #isEnabled()}.
+ */
+ static final String PROPERTY_NATIVE_IMAGE_CODE = "org.graalvm.nativeimage.imagecode";
+
+ /** Value of {@link #PROPERTY_NATIVE_IMAGE_CODE} while executing a native image. */
+ static final String NATIVE_IMAGE_CODE_RUNTIME = "runtime";
+
+ /**
+ * Fully-qualified name of GraalVM's "feature not supported at runtime" error.
+ * Matched by name to avoid a compile-time dependency on {@code org.graalvm.*}.
+ */
+ static final String UNSUPPORTED_FEATURE_ERROR =
+ "com.oracle.svm.core.jdk.UnsupportedFeatureError";
+
+ /**
+ * Lookup for this class only — caller argument to
+ * {@link MethodHandles#privateLookupIn(Class, Lookup)}, never a production
+ * nest host.
+ */
+ private static final Lookup LOOKUP = MethodHandles.lookup();
+
+ /**
+ * Nestmate with the default (weak) lifecycle. Default hidden classes are
+ * weakly held; {@link Lookup.ClassOption#STRONG} is not requested.
+ */
+ private static final Lookup.ClassOption[] NESTMATE_WEAK =
+ new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE};
+
+ private HiddenClassDefiner() {
+ }
+
+ /**
+ * Whether hidden-class definition may be attempted in this process.
+ *
+ * @return {@code false} when the kill switch is set or when running inside
+ * a GraalVM native image at run time
+ */
+ public static boolean isEnabled() {
+ if (isNativeImageRuntime()) {
+ return false;
+ }
+ return !SystemUtil.getBooleanSafe(PROPERTY_DISABLE, false);
+ }
+
+ /**
+ * Defines {@code bytes} as a hidden nestmate of {@code lookup.lookupClass()}
+ * with a weak lifecycle. The class-file package is rewritten to match the
+ * lookup class before definition.
+ *
+ * @param lookup full-privilege lookup for the nest host
+ * @param bytes class-file bytes
+ * @param initialize {@code true} to run {@code } immediately
+ * @return the hidden class, or {@code null} if definition is not possible
+ */
+ public static Class> tryDefineNestmate(
+ final Lookup lookup,
+ final byte[] bytes,
+ final boolean initialize) {
+ if (!isEnabled() || lookup == null || bytes == null) {
+ return null;
+ }
+ try {
+ final byte[] aligned = alignPackage(bytes, lookup.lookupClass());
+ return lookup.defineHiddenClass(aligned, initialize, NESTMATE_WEAK).lookupClass();
+ } catch (IllegalAccessException | SecurityException | LinkageError e) {
+ return null;
+ } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
+ // Invalid class-file bytes (including ASM ClassReader).
+ return null;
+ } catch (Error e) {
+ return softFailOrRethrow(e);
+ }
+ }
+
+ /**
+ * Best-effort definition of a hidden nestmate of a foreign host.
+ * Uses {@code privateLookupIn} from this class; gated by
+ * {@link #canAttemptPrivateLookup(Class)}. Not a substitute for a host-owned
+ * lookup.
+ *
+ * @param host nest host and class-loader / package donor
+ * @param bytes class-file bytes
+ * @param initialize {@code true} to run {@code } immediately
+ * @return the hidden class, or {@code null} if private lookup or definition fails
+ */
+ public static Class> tryDefineNestmate(
+ final Class> host,
+ final byte[] bytes,
+ final boolean initialize) {
+ if (!isEnabled() || !canAttemptPrivateLookup(host) || bytes == null) {
+ return null;
+ }
+ try {
+ final Lookup hostLookup = MethodHandles.privateLookupIn(host, LOOKUP);
+ return tryDefineNestmate(hostLookup, bytes, initialize);
+ } catch (IllegalAccessException | SecurityException e) {
+ return null;
+ } catch (Error e) {
+ return softFailOrRethrow(e);
+ }
+ }
+
+ /**
+ * Internal policy: whether {@code privateLookupIn} from this utility into
+ * {@code host} is worth attempting.
+ *
+ *
Returns {@code false} for unusable host shapes and for named-module
+ * packages that are not open to the Groovy runtime (e.g. {@code String} in
+ * {@code java.base}). A {@code true} result does not guarantee success.
+ *
+ *
Not a stable user API — for runtime define policy and tests only.
+ *
+ * @param host candidate foreign nest host
+ * @return {@code true} when a private-lookup attempt is not known to be futile
+ * @since 6.0.0
+ */
+ @Internal
+ public static boolean canAttemptPrivateLookup(final Class> host) {
+ if (!isUsableHost(host)) {
+ return false;
+ }
+ final Module hostModule = host.getModule();
+ final Module callerModule = LOOKUP.lookupClass().getModule();
+ return hostModule.isOpen(host.getPackageName(), callerModule);
+ }
+
+ // -------------------------------------------------------------------------
+ // Internals (package-visible where tests need them)
+ // -------------------------------------------------------------------------
+
+ /** {@code true} when executing inside a GraalVM native image (not during image build). */
+ static boolean isNativeImageRuntime() {
+ return NATIVE_IMAGE_CODE_RUNTIME.equals(System.getProperty(PROPERTY_NATIVE_IMAGE_CODE));
+ }
+
+ /** {@code true} when {@code e} is GraalVM's unsupported-feature error (name match). */
+ static boolean isUnsupportedFeatureError(final Error e) {
+ return e != null && UNSUPPORTED_FEATURE_ERROR.equals(e.getClass().getName());
+ }
+
+ /**
+ * Soft-fails GraalVM unsupported-feature errors as {@code null}; rethrows
+ * every other {@link Error}.
+ */
+ static Class> softFailOrRethrow(final Error e) {
+ if (isUnsupportedFeatureError(e)) {
+ return null;
+ }
+ throw e;
+ }
+
+ /**
+ * Rewrites {@code this_class} (and internal references to it) into
+ * {@code host}'s run-time package. No-op when already aligned.
+ */
+ private static byte[] alignPackage(final byte[] bytes, final Class> host) {
+ final String hostPkg = host.getPackageName();
+ final ClassReader reader = new ClassReader(bytes);
+ final String oldInternal = reader.getClassName();
+ final int slash = oldInternal.lastIndexOf('/');
+ final String simple = slash < 0 ? oldInternal : oldInternal.substring(slash + 1);
+ final String newInternal = hostPkg.isEmpty()
+ ? simple
+ : hostPkg.replace('.', '/') + '/' + simple;
+ if (oldInternal.equals(newInternal)) {
+ return bytes;
+ }
+ final ClassWriter writer = new ClassWriter(reader, 0);
+ reader.accept(new ClassRemapper(writer,
+ new SimpleRemapper(
+ CompilerConfiguration.ASM_API_VERSION,
+ Collections.singletonMap(oldInternal, newInternal))), 0);
+ return writer.toByteArray();
+ }
+
+ private static boolean isUsableHost(final Class> host) {
+ return host != null
+ && !host.isPrimitive()
+ && !host.isArray()
+ && !host.isHidden();
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java b/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
index 874495f64b0..2c611252071 100644
--- a/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
+++ b/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
@@ -18,71 +18,140 @@
*/
package org.codehaus.groovy.reflection;
+import org.apache.groovy.util.HiddenClassDefiner;
+
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
+import java.security.ProtectionDomain;
import java.util.concurrent.atomic.AtomicInteger;
+/**
+ * A specialized {@link ClassLoader} used to define per-class artifact
+ * classes such as generated meta-method dispatchers.
+ *
+ *
Since Groovy 6.0 this loader first attempts to define each artifact as a
+ * hidden nestmate of the target via
+ * {@link HiddenClassDefiner#tryDefineNestmate(Class, byte[], boolean)}. When
+ * that soft-fails (unopened package, unsuitable host, …) it falls back to
+ * {@link ClassLoader#defineClass} with the protection domain captured at
+ * construction. Module / nest-host policy is documented on
+ * {@link HiddenClassDefiner}.
+ *
+ * @see HiddenClassDefiner
+ */
public class ClassLoaderForClassArtifacts extends ClassLoader {
+
+ /** Soft reference to the class for which artifacts are generated. */
public final SoftReference klazz;
+
+ /** Binary name of the target, retained even if {@link #klazz} is cleared. */
+ private final String className;
+
+ /**
+ * Protection domain of the target, captured strongly so fallback
+ * {@code defineClass} stays deterministic if the soft reference is cleared.
+ */
+ private final ProtectionDomain protectionDomain;
+
private final AtomicInteger classNamesCounter = new AtomicInteger(-1);
- public ClassLoaderForClassArtifacts(Class klazz) {
+ /**
+ * Creates a new artifact class loader for the specified class.
+ *
+ * @param klazz the class whose artifact classes are to be defined via this loader
+ */
+ public ClassLoaderForClassArtifacts(final Class klazz) {
super(klazz.getClassLoader());
this.klazz = new SoftReference<>(klazz);
+ this.className = klazz.getName();
+ this.protectionDomain = klazz.getProtectionDomain();
}
- public Class define(String name, byte[] bytes) {
- Class cls = defineClass(name, bytes, 0, bytes.length, klazz.get().getProtectionDomain());
+ /**
+ * Defines a class from bytecode, preferring a hidden nestmate of the target.
+ *
+ * @param name the binary name used for the fallback (visible-class) path
+ * @param bytes the class-file bytes
+ * @return the defined class
+ */
+ public Class define(final String name, final byte[] bytes) {
+ final Class> host = klazz.get();
+ if (host != null) {
+ // Foreign host: best-effort privateLookupIn (see HiddenClassDefiner).
+ final Class> hidden = HiddenClassDefiner.tryDefineNestmate(host, bytes, false);
+ if (hidden != null) {
+ return hidden;
+ }
+ }
+
+ final Class> cls = defineClass(name, bytes, 0, bytes.length, protectionDomain);
resolveClass(cls);
return cls;
}
+ /**
+ * Defines a class from bytecode and returns the public constructor
+ * matching the given parameter types, or {@code null} if definition or
+ * lookup fails.
+ *
+ *
Uses {@link Class#getConstructor(Class[])} so the returned constructor
+ * is always publicly accessible (same contract as before hidden-class support).
+ *
+ * @param name the binary name (for fallback visible-class definition)
+ * @param bytes the class-file bytes
+ * @param parameterTypes the constructor parameter types to look up
+ * @return the matching public constructor, or {@code null}
+ */
+ public Constructor defineClassAndGetConstructor(
+ final String name,
+ final byte[] bytes,
+ final Class>... parameterTypes) {
+ try {
+ final Class> cls = define(name, bytes);
+ return cls.getConstructor(parameterTypes);
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
@Override
- public Class loadClass(String name) throws ClassNotFoundException {
- Class cls = findLoadedClass(name);
- if (cls != null)
+ public Class loadClass(final String name) throws ClassNotFoundException {
+ final Class cls = findLoadedClass(name);
+ if (cls != null) {
return cls;
-
+ }
return super.loadClass(name);
}
- public String createClassName(Method method) {
+ /**
+ * Generates a unique class name for an artifact associated with the given method.
+ *
+ * @param method the method for which the artifact is generated
+ * @return a unique class name
+ */
+ public String createClassName(final Method method) {
return createClassName(method.getName());
}
- public String createClassName(String methodName) {
- final String name;
- final String clsName = klazz.get().getName();
- if (clsName.startsWith("java."))
- name = clsName.replace('.', '_') + "$" + methodName;
- else
- name = clsName + "$" + methodName;
- int suffix = classNamesCounter.getAndIncrement();
- return suffix == -1 ? name : name + "$" + suffix;
- }
-
/**
- * Defines a class from bytecode and returns a constructor matching {@code parameterTypes}.
+ * Generates a unique class name for an artifact associated with the given
+ * method name.
*
- * @param name the binary name of the class to define
- * @param bytes the class file bytes
- * @param parameterTypes the constructor parameter types to look up
- * @return the matching constructor, or {@code null} if definition or lookup fails
+ *
For classes in the {@code java.*} package hierarchy the name is
+ * prefixed to avoid the restricted {@code java.} namespace. The counter
+ * suffix ensures uniqueness when multiple artifacts share the same logical
+ * name.
+ *
+ * @param methodName the method name component of the artifact class name
+ * @return a unique class name
*/
- public Constructor defineClassAndGetConstructor(final String name, final byte[] bytes, final Class>... parameterTypes) {
- final Class cls = definePrivileged(name, bytes);
-
- if (cls != null) {
- try {
- return cls.getConstructor(parameterTypes);
- } catch (NoSuchMethodException e) { //
- }
- }
- return null;
- }
-
- private Class definePrivileged(String name, byte[] bytes) {
- return define(name, bytes);
+ public String createClassName(final String methodName) {
+ final String base = className.startsWith("java.")
+ ? className.replace('.', '_') + "$" + methodName
+ : className + "$" + methodName;
+ final int suffix = classNamesCounter.getAndIncrement();
+ return suffix == -1 ? base : base + "$" + suffix;
}
}
diff --git a/src/main/java/org/codehaus/groovy/runtime/ProxyClassDefiner.java b/src/main/java/org/codehaus/groovy/runtime/ProxyClassDefiner.java
new file mode 100644
index 00000000000..447a183d8ce
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/runtime/ProxyClassDefiner.java
@@ -0,0 +1,267 @@
+/*
+ * 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 org.codehaus.groovy.runtime;
+
+import groovy.lang.GroovyObject;
+import org.apache.groovy.util.HiddenClassDefiner;
+
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Constructor;
+import java.util.Collection;
+import java.util.function.BiFunction;
+
+/**
+ * Defines proxy classes for {@link ProxyGeneratorAdapter}, preferring a JEP 371
+ * hidden nestmate when safe and falling back to a visible {@link ClassLoader}
+ * definition otherwise.
+ *
+ *
Extracted so {@link ProxyGeneratorAdapter} stays focused on bytecode
+ * generation. Nest-host and module policy live in
+ * {@link HiddenClassDefiner}; this class only applies proxy-specific rules.
+ *
+ *
When a proxy stays visible
+ *
+ *
hidden classes are disabled;
+ *
any named dependency is itself hidden;
+ *
interface-style aggregates ({@code Object} super, no typed delegate,
+ * at least one user interface) — must stay nameable for MockFor re-wrap.
+ *
+ *
+ *
Nest host selection
+ *
+ *
{@code delegateClass} if a usable foreign host (open package + loader
+ * can see every dependency);
+ *
else concrete {@code superClass} (not {@code Object}) under the same rule;
+ *
else caller-owned {@link Lookup} from {@link ProxyGeneratorAdapter}
+ * when that loader can see every dependency.
+ *
+ * Interfaces are not used as nest hosts. Module A/B/C coverage is documented on
+ * {@link HiddenClassDefiner}.
+ *
+ * @since 6.0.0
+ * @see HiddenClassDefiner
+ */
+final class ProxyClassDefiner {
+
+ private ProxyClassDefiner() {
+ }
+
+ /**
+ * Result of a define attempt.
+ */
+ static final class Result {
+ final Class> type;
+ final boolean hidden;
+ final Constructor> constructor;
+
+ Result(final Class> type, final boolean hidden, final Constructor> constructor) {
+ this.type = type;
+ this.hidden = hidden;
+ this.constructor = constructor;
+ }
+ }
+
+ /**
+ * Defines {@code bytecode} as a hidden nestmate when policy allows, else via
+ * {@code visibleDefine}.
+ *
+ * @param bytecode generated class-file bytes
+ * @param binaryName binary name for the visible fallback path
+ * @param superClass proxy superclass (already normalised; may be Object)
+ * @param delegateClass typed {@code $delegate} field type, or {@code null}
+ * @param implClasses super + interfaces + related types named by the bytecode
+ * @param ownLookup {@link MethodHandles#lookup()} from {@link ProxyGeneratorAdapter}
+ * @param visibleDefine {@code (binaryName, bytes) -> Class} fallback (e.g. InnerLoader)
+ * @param ctorArgs constructor parameter types to resolve after define
+ * @return defined class, whether it is hidden, and the matching public ctor (or null)
+ */
+ static Result define(
+ final byte[] bytecode,
+ final String binaryName,
+ final Class> superClass,
+ final Class> delegateClass,
+ final Collection extends Class>> implClasses,
+ final Lookup ownLookup,
+ final BiFunction> visibleDefine,
+ final Class>[] ctorArgs) {
+
+ Class> type = null;
+
+ if (mayDefineHidden(superClass, delegateClass, implClasses)) {
+ final Class> foreign = preferredForeignHost(superClass, delegateClass, implClasses);
+ if (foreign != null) {
+ type = accept(HiddenClassDefiner.tryDefineNestmate(foreign, bytecode, true), ctorArgs);
+ }
+ if (type == null && canResolveAll(ownLookup.lookupClass(), superClass, delegateClass, implClasses)) {
+ type = accept(HiddenClassDefiner.tryDefineNestmate(ownLookup, bytecode, true), ctorArgs);
+ }
+ if (type != null) {
+ return new Result(type, true, resolvePublicConstructor(type, ctorArgs));
+ }
+ }
+
+ type = visibleDefine.apply(binaryName, bytecode);
+ return new Result(type, false, resolvePublicConstructor(type, ctorArgs));
+ }
+
+ // -------------------------------------------------------------------------
+ // Policy
+ // -------------------------------------------------------------------------
+
+ /**
+ * {@code true} when attempting a hidden definition is worthwhile.
+ */
+ static boolean mayDefineHidden(
+ final Class> superClass,
+ final Class> delegateClass,
+ final Collection extends Class>> implClasses) {
+ if (!HiddenClassDefiner.isEnabled()) {
+ return false;
+ }
+ if (isUnusableNamedType(superClass) || isUnusableNamedType(delegateClass)) {
+ return false;
+ }
+ if (implClasses != null) {
+ for (Class> impl : implClasses) {
+ if (isUnusableNamedType(impl)) {
+ return false;
+ }
+ }
+ }
+ return delegateClass != null || superClass != Object.class || !hasUserInterface(implClasses);
+ }
+
+ /**
+ * Single preferred foreign nest host, or {@code null} to skip to own Lookup.
+ * Unopened platform packages are rejected via
+ * {@link HiddenClassDefiner#canAttemptPrivateLookup(Class)}.
+ */
+ static Class> preferredForeignHost(
+ final Class> superClass,
+ final Class> delegateClass,
+ final Collection extends Class>> implClasses) {
+ if (isCandidateHost(delegateClass)
+ && canResolveAll(delegateClass, superClass, delegateClass, implClasses)) {
+ return delegateClass;
+ }
+ if (superClass != Object.class
+ && isCandidateHost(superClass)
+ && canResolveAll(superClass, superClass, delegateClass, implClasses)) {
+ return superClass;
+ }
+ return null;
+ }
+
+ // -------------------------------------------------------------------------
+ // Loader visibility
+ // -------------------------------------------------------------------------
+
+ /**
+ * Every type the proxy bytecode names must be resolvable from {@code host}'s
+ * defining loader (child loaders see parents; parents do not see children).
+ */
+ static boolean canResolveAll(
+ final Class> host,
+ final Class> superClass,
+ final Class> delegateClass,
+ final Collection extends Class>> implClasses) {
+ if (!loaderCanResolve(host, superClass) || !loaderCanResolve(host, delegateClass)) {
+ return false;
+ }
+ if (implClasses != null) {
+ for (Class> impl : implClasses) {
+ if (!loaderCanResolve(host, impl)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ static boolean loaderCanResolve(final Class> host, final Class> type) {
+ if (type == null || type.isPrimitive()) {
+ return true;
+ }
+ final ClassLoader defining = type.getClassLoader(); // null ⇒ bootstrap
+ if (defining == null) {
+ return true;
+ }
+ for (ClassLoader cl = host.getClassLoader(); cl != null; cl = cl.getParent()) {
+ if (cl == defining) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private static Class> accept(final Class> candidate, final Class>[] ctorArgs) {
+ if (candidate == null) {
+ return null;
+ }
+ if (resolvePublicConstructor(candidate, ctorArgs) == null) {
+ return null;
+ }
+ return candidate;
+ }
+
+ /**
+ * Public constructor only — preserves the historical
+ * {@link Class#getConstructor(Class[])} contract used by proxy instantiation.
+ */
+ static Constructor> resolvePublicConstructor(final Class> type, final Class>[] args) {
+ try {
+ return type.getConstructor(args);
+ } catch (NoSuchMethodException | LinkageError e) {
+ return null;
+ }
+ }
+
+ /**
+ * Proxy-specific exclusions plus {@link HiddenClassDefiner#canAttemptPrivateLookup}
+ * (shape + module-open). {@code Object} / this adapter / sealed types are
+ * never foreign hosts.
+ */
+ private static boolean isCandidateHost(final Class> type) {
+ return type != null
+ && type != Object.class
+ && type != ProxyGeneratorAdapter.class
+ && !type.isSealed()
+ && HiddenClassDefiner.canAttemptPrivateLookup(type);
+ }
+
+ private static boolean isUnusableNamedType(final Class> type) {
+ return type != null && (type.isPrimitive() || type.isArray() || type.isHidden());
+ }
+
+ private static boolean hasUserInterface(final Collection extends Class>> implClasses) {
+ if (implClasses == null) {
+ return false;
+ }
+ for (Class> impl : implClasses) {
+ if (impl != null && impl != Object.class && impl != GroovyObject.class) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
index dd6d6708db8..0593062177e 100644
--- a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
+++ b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
@@ -41,6 +41,8 @@
import org.objectweb.asm.Type;
import java.io.Serial;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -125,6 +127,12 @@ public class ProxyGeneratorAdapter extends ClassVisitor {
private static final String DELEGATE_OBJECT_FIELD = "$delegate";
private static final AtomicLong PROXY_COUNTER = new AtomicLong();
+ /**
+ * Full-privilege lookup for this class — last-resort nest host when no
+ * foreign host is suitable. See {@link org.apache.groovy.util.HiddenClassDefiner}.
+ */
+ private static final Lookup LOOKUP = MethodHandles.lookup();
+
private static final List OBJECT_METHODS = getInheritedMethods(Object.class, new ArrayList<>());
private static final List GROOVYOBJECT_METHODS = getInheritedMethods(GroovyObject.class, new ArrayList<>());
private static final Set GROOVYOBJECT_METHOD_NAMES;
@@ -139,7 +147,18 @@ public class ProxyGeneratorAdapter extends ClassVisitor {
private final String proxyName;
private final Class superClass;
private final Class delegateClass;
+ /**
+ * Class loader used for intermediate Groovy compilation (e.g. trait adapter
+ * classes in {@link #adjustSuperClass}) and as a fallback when hidden-class
+ * definition is not possible.
+ */
private final InnerLoader innerLoader;
+ /**
+ * Whether the final proxy class was defined as a hidden class.
+ * {@code true} means {@link #cachedClass} is a hidden nestmate;
+ * {@code false} means it was defined via the classic {@link ClassLoader} path.
+ */
+ private final boolean proxyIsHidden;
private final Set implClasses;
private final Set