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..d7d602ece5a
--- /dev/null
+++ b/src/main/java/org/apache/groovy/util/HiddenClassDefiner.java
@@ -0,0 +1,315 @@
+/*
+ * 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 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.lang.reflect.Constructor;
+
+/**
+ * Central facility for defining hidden classes
+ * (JEP 371).
+ *
+ *
Why a single entry point
+ *
Every dynamic class generator in Groovy (proxies, reflectors, per-class
+ * meta-method artifacts, …) should obtain hidden classes through this type so
+ * that:
+ *
+ *
the {@code NESTMATE} / {@code STRONG} policy lives in one place;
+ *
the package of the supplied class-file is automatically aligned with
+ * the lookup class (a hard requirement of
+ * {@link Lookup#defineHiddenClass});
+ *
callers can use a soft {@code try*} API that never throws on the
+ * expected failure modes (module access denied, package mismatch after
+ * rewrite, linkage errors because the host class loader cannot see a
+ * referenced type) and simply returns {@code null} for transparent
+ * fall-back to {@link ClassLoader#defineClass}.
The host is obtained via {@link MethodHandles#privateLookupIn(Class, Lookup)}
+ * using a full-privilege lookup captured inside this (Java) class. That makes
+ * the result independent of Groovy's indy / {@code $$InjectedInvoker}
+ * caller-sensitive quirks.
+ *
+ *
Lifecycle
+ *
+ *
weak (default for nestmates) — the JVM may unload the class as
+ * soon as its {@link Class} object becomes unreachable;
+ *
strong — lifetime is tied to the defining class loader.
+ *
+ *
+ *
Kill switch
+ *
{@code -Dgroovy.hidden.classes.disable=true} forces every {@code try*}
+ * method to return {@code null} (and makes {@link #isEnabled()} false) so
+ * diagnostics and legacy environments can fall back without code changes.
+ *
+ * @since 6.0.0
+ * @see Lookup#defineHiddenClass(byte[], boolean, Lookup.ClassOption...)
+ */
+public final class HiddenClassDefiner {
+
+ /** System property that disables hidden-class definitions. */
+ public static final String PROPERTY_DISABLE = "groovy.hidden.classes.disable";
+
+ /**
+ * {@code true} when hidden-class definitions are globally disabled.
+ * Evaluated once at class-init so hot paths pay no property-lookup cost.
+ */
+ public static final boolean HIDDEN_CLASSES_DISABLED =
+ SystemUtil.getBooleanSafe(PROPERTY_DISABLE, false);
+
+ /**
+ * Full-privilege lookup for this Java class, captured during
+ * {@code }. Used solely as the caller argument to
+ * {@link MethodHandles#privateLookupIn(Class, Lookup)}; it is never used
+ * as the nest host of user-generated classes.
+ */
+ private static final Lookup TRUSTED_LOOKUP = MethodHandles.lookup();
+
+ // Pre-allocated option arrays — defineHiddenClass is on the meta-class hot path.
+ private static final Lookup.ClassOption[] OPT_NESTMATE =
+ new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE};
+ private static final Lookup.ClassOption[] OPT_STRONG =
+ new Lookup.ClassOption[]{Lookup.ClassOption.STRONG};
+ private static final Lookup.ClassOption[] OPT_NESTMATE_STRONG =
+ new Lookup.ClassOption[]{Lookup.ClassOption.NESTMATE, Lookup.ClassOption.STRONG};
+ private static final Lookup.ClassOption[] OPT_NONE = new Lookup.ClassOption[0];
+
+ private HiddenClassDefiner() {
+ throw new AssertionError("HiddenClassDefiner is a utility class");
+ }
+
+ // -------------------------------------------------------------------------
+ // Status
+ // -------------------------------------------------------------------------
+
+ /**
+ * @return {@code true} when hidden-class definition is enabled
+ * (the default unless {@value #PROPERTY_DISABLE} is set)
+ */
+ public static boolean isEnabled() {
+ return !HIDDEN_CLASSES_DISABLED;
+ }
+
+ // -------------------------------------------------------------------------
+ // Soft (best-effort) API — preferred by production call sites
+ // -------------------------------------------------------------------------
+
+ /**
+ * Attempts to define {@code bytes} as a hidden nestmate of
+ * {@code host} with a weak (eager-unloading) lifecycle.
+ *
+ *
On success the returned class:
+ *
+ *
is non-discoverable by name ({@link Class#isHidden()} is true);
+ *
shares the defining loader / package / protection domain of
+ * {@code host};
+ *
is a nestmate of {@code host} (mutual private access).
+ *
+ *
+ *
Returns {@code null} when hidden classes are disabled, {@code host}
+ * is unsuitable (null / primitive / array / hidden), private lookup is
+ * refused, the class file is invalid, or linkage fails (for example the
+ * host loader cannot resolve a supertype referenced by the bytecode).
+ * Callers are expected to fall back to {@link ClassLoader#defineClass}.
+ *
+ * @param host nest host and class-loader / package donor; must be a
+ * normal (non-hidden) reference type
+ * @param bytes class-file bytes; {@code this_class} is rewritten into
+ * {@code host}'s package when needed
+ * @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 Class> host,
+ final byte[] bytes,
+ final boolean initialize) {
+ if (HIDDEN_CLASSES_DISABLED || !isUsableHost(host) || bytes == null) {
+ return null;
+ }
+ try {
+ final Lookup hostLookup = MethodHandles.privateLookupIn(host, TRUSTED_LOOKUP);
+ final byte[] aligned = alignPackage(bytes, host);
+ return hostLookup.defineHiddenClass(aligned, initialize, OPT_NESTMATE).lookupClass();
+ } catch (IllegalAccessException | IllegalArgumentException | SecurityException | LinkageError e) {
+ return null;
+ } catch (RuntimeException e) {
+ // ASM rewrite failures, unexpected JVM checks, etc.
+ return null;
+ }
+ }
+
+ /**
+ * Soft variant of {@link #define(Lookup, byte[], boolean, boolean, boolean)}.
+ * Returns {@code null} instead of throwing for the expected failure modes.
+ *
+ *
The bytecode package is aligned to {@code lookup.lookupClass()} before
+ * definition. The lookup itself is not replaced — callers that need a
+ * specific nest host should obtain it via
+ * {@link MethodHandles#privateLookupIn(Class, Lookup)} (or use
+ * {@link #tryDefineNestmate(Class, byte[], boolean)}).
+ */
+ public static Class> tryDefine(
+ final Lookup lookup,
+ final byte[] bytes,
+ final boolean initialize,
+ final boolean nestmate,
+ final boolean strong) {
+ if (HIDDEN_CLASSES_DISABLED || lookup == null || bytes == null) {
+ return null;
+ }
+ try {
+ return define(lookup, bytes, initialize, nestmate, strong);
+ } catch (IllegalAccessException | IllegalArgumentException | SecurityException | LinkageError e) {
+ return null;
+ } catch (RuntimeException e) {
+ return null;
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Strict API — for tests and callers that want the original exception
+ // -------------------------------------------------------------------------
+
+ /**
+ * Defines a hidden class with the given options.
+ *
+ *
The class-file's {@code this_class} package is rewritten to match
+ * {@code lookup.lookupClass()} when they differ — this is required by
+ * {@link Lookup#defineHiddenClass}.
+ *
+ * @param lookup full-privilege lookup whose lookup-class supplies the
+ * defining loader, package, protection domain and
+ * (when {@code nestmate}) nest
+ * @param bytes class-file bytes
+ * @param initialize whether to initialize the class immediately
+ * @param nestmate whether to inject the class into the lookup class's nest
+ * @param strong whether the class's lifetime is tied to the loader
+ * @return the defined hidden class
+ * @throws IllegalAccessException if the lookup lacks the required access
+ * @throws IllegalArgumentException if the bytes are not a valid class file
+ * (after package alignment)
+ * @throws LinkageError if a dependency of the new class cannot
+ * be resolved in the lookup class's loader
+ */
+ public static Class> define(
+ final Lookup lookup,
+ final byte[] bytes,
+ final boolean initialize,
+ final boolean nestmate,
+ final boolean strong) throws IllegalAccessException {
+ final byte[] aligned = alignPackage(bytes, lookup.lookupClass());
+ return lookup.defineHiddenClass(aligned, initialize, options(nestmate, strong)).lookupClass();
+ }
+
+ /**
+ * Strict nestmate + weak convenience overload.
+ *
+ * @see #define(Lookup, byte[], boolean, boolean, boolean)
+ */
+ public static Class> defineNestmate(
+ final Lookup lookup,
+ final byte[] bytes,
+ final boolean initialize) throws IllegalAccessException {
+ return define(lookup, bytes, initialize, true, false);
+ }
+
+ /**
+ * Strict non-nestmate + strong convenience overload.
+ *
+ * @see #define(Lookup, byte[], boolean, boolean, boolean)
+ */
+ public static Class> defineStrong(
+ final Lookup lookup,
+ final byte[] bytes,
+ final boolean initialize) throws IllegalAccessException {
+ return define(lookup, bytes, initialize, false, true);
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns a declared constructor of {@code type}, wrapping a missing
+ * constructor as {@link IllegalStateException} (bytecode-generation bug).
+ */
+ @SuppressWarnings("unchecked")
+ public static Constructor findConstructor(
+ final Class type,
+ final Class>... parameterTypes) {
+ try {
+ return type.getDeclaredConstructor(parameterTypes);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ "Class " + type.getName() + " is missing the expected constructor", e);
+ }
+ }
+
+ /**
+ * Rewrites {@code this_class} (and all internal references to it) so the
+ * class lives in {@code host}'s run-time package. No-op when already aligned.
+ *
+ *
Visible for testing.
+ */
+ 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(oldInternal, newInternal)), 0);
+ return writer.toByteArray();
+ }
+
+ private static boolean isUsableHost(final Class> host) {
+ return host != null
+ && !host.isPrimitive()
+ && !host.isArray()
+ && !host.isHidden();
+ }
+
+ private static Lookup.ClassOption[] options(final boolean nestmate, final boolean strong) {
+ if (nestmate) {
+ return strong ? OPT_NESTMATE_STRONG : OPT_NESTMATE;
+ }
+ return strong ? OPT_STRONG : OPT_NONE;
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java b/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
index 874495f64b0..066b5558d70 100644
--- a/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
+++ b/src/main/java/org/codehaus/groovy/reflection/ClassLoaderForClassArtifacts.java
@@ -18,71 +18,146 @@
*/
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.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 class ({@link HiddenClassDefiner}):
+ *
+ *
non-discoverable by name — no class-space pollution;
+ *
same defining loader / package / protection domain as the target, so
+ * references to the target resolve correctly even under custom loaders;
+ *
nestmate of the target (mutual private access);
+ *
weak lifecycle — eligible for eager unloading once the {@link Class}
+ * object is unreachable, reducing metaspace pressure in long-running
+ * applications that generate many per-class artifacts.
+ *
+ *
+ *
If hidden-class definition is disabled or fails (module not open, target
+ * is a hidden/primitive/array type, linkage error, …) the loader falls back to
+ * the traditional {@link ClassLoader#defineClass} path transparently.
+ */
public class ClassLoaderForClassArtifacts extends ClassLoader {
+
+ /** Soft reference to the class for which artifacts are generated. */
public final SoftReference klazz;
+
+ /**
+ * Counter used to ensure unique class names when multiple artifacts are
+ * generated for the same method name.
+ */
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);
}
- public Class define(String name, byte[] bytes) {
- Class cls = defineClass(name, bytes, 0, bytes.length, klazz.get().getProtectionDomain());
+ // -------------------------------------------------------------------------
+ // Class definition
+ // -------------------------------------------------------------------------
+
+ /**
+ * 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) {
+ final Class> hidden = HiddenClassDefiner.tryDefineNestmate(host, bytes, false);
+ if (hidden != null) {
+ return hidden;
+ }
+ }
+
+ // Fallback: visible class with the target's protection domain
+ final Class> cls = defineClass(
+ name, bytes, 0, bytes.length,
+ host != null ? host.getProtectionDomain() : null);
resolveClass(cls);
return cls;
}
+ /**
+ * Defines a class from bytecode and returns the constructor matching the
+ * given parameter types, or {@code null} if definition or lookup fails.
+ *
+ * @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 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.getDeclaredConstructor(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) {
- 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;
- }
+ // -------------------------------------------------------------------------
+ // Name generation
+ // -------------------------------------------------------------------------
/**
- * 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.
*
- * @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
+ * @param method the method for which the artifact is generated
+ * @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;
+ public String createClassName(final Method method) {
+ return createClassName(method.getName());
}
- private Class definePrivileged(String name, byte[] bytes) {
- return define(name, bytes);
+ /**
+ * Generates a unique class name for an artifact associated with the given
+ * method name.
+ *
+ *
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 String createClassName(final String methodName) {
+ final Class> host = klazz.get();
+ final String clsName = host != null ? host.getName() : "unknown";
+ final String base = clsName.startsWith("java.")
+ ? clsName.replace('.', '_') + "$" + methodName
+ : clsName + "$" + methodName;
+ final int suffix = classNamesCounter.getAndIncrement();
+ return suffix == -1 ? base : base + "$" + suffix;
}
}
diff --git a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
index dd6d6708db8..5e55a169884 100644
--- a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
+++ b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java
@@ -23,6 +23,7 @@
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyRuntimeException;
+import org.apache.groovy.util.HiddenClassDefiner;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
@@ -139,7 +140,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