import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.base.VerifyException;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.UncheckedExecutionException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
+import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
BindingDOMCodecServices {
private static final Logger LOG = LoggerFactory.getLogger(BindingCodecContext.class);
private static final @NonNull NodeIdentifier FAKE_NODEID = new NodeIdentifier(QName.create("fake", "fake"));
- private static final File BYTECODE_DIRECTORY;
+ private static final BindingClassLoader.@NonNull Builder BCL_BUILDER;
static {
- final String dir = System.getProperty("org.opendaylight.mdsal.binding.dom.codec.loader.bytecodeDumpDirectory");
- BYTECODE_DIRECTORY = Strings.isNullOrEmpty(dir) ? null : new File(dir);
+ final var builder = BindingClassLoader.builder(BindingCodecContext.class);
+ final var dir = System.getProperty("org.opendaylight.mdsal.binding.dom.codec.loader.bytecodeDumpDirectory");
+ if (dir != null && !dir.isEmpty()) {
+ builder.dumpBytecode(Path.of(dir));
+ }
+ BCL_BUILDER = builder;
}
/**
}
});
- private final @NonNull BindingClassLoader loader =
- BindingClassLoader.create(BindingCodecContext.class, BYTECODE_DIRECTORY);
+ private final @NonNull BindingClassLoader loader = BCL_BUILDER.build();
private final @NonNull InstanceIdentifierCodec instanceIdentifierCodec;
private final @NonNull IdentityCodec identityCodec;
private final @NonNull BindingRuntimeContext context;
import org.opendaylight.yangtools.binding.loader.BindingClassLoader.GeneratorResult;
public class CodecClassLoaderTest {
- private final BindingClassLoader codecClassLoader = BindingClassLoader.create(CodecClassLoaderTest.class, null);
+ private final BindingClassLoader codecClassLoader = BindingClassLoader.ofRootClass(CodecClassLoaderTest.class);
@ParameterizedTest(name = "Generate class within namespace: {0}")
@MethodSource("generateClassWithinNamespaceArgs")
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
+import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
import java.util.HexFormat;
import org.slf4j.LoggerFactory;
/**
- * A ClassLoader hosting types generated for a particular type. A root instance is attached to a
- * BindingCodecContext instance, so any generated classes from it can be garbage-collected when the context
- * is destroyed, as well as to prevent two contexts trampling over each other.
+ * A {@link ClassLoader} hosting types generated for a particular type. A root instance is attached to a particular user
+ * a root class loader and should be used to load classes, which are used by a particular user instance. When used
+ * correctly, the classes loaded through this instance become eligible for GC when the user instance becomes
+ * unreachable.
*
* <p>It semantically combines two class loaders: the class loader in which this class is loaded and the class loader in
* which a target Binding interface/class is loaded. This inherently supports multi-classloader environments -- the root
*/
public abstract sealed class BindingClassLoader extends ClassLoader
permits LeafBindingClassLoader, RootBindingClassLoader {
+ /**
+ * A builder of {@link BindingClassLoader} instances.
+ */
+ public static final class Builder {
+ private final @NonNull ClassLoader parentLoader;
+
+ private @Nullable Path dumpDirectory;
+
+ Builder(final ClassLoader parentLoader) {
+ this.parentLoader = requireNonNull(parentLoader);
+ }
+
+ public Builder dumpBytecode(final Path toDirectory) {
+ this.dumpDirectory = requireNonNull(toDirectory);
+ return this;
+ }
+
+ public @NonNull BindingClassLoader build() {
+ return SecuritySupport.get(() -> new RootBindingClassLoader(parentLoader, dumpDirectory));
+ }
+ }
+
/**
* A class generator, generating a class of a particular type.
*
private final @Nullable File dumpDir;
- BindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
+ private BindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
super(parentLoader);
this.dumpDir = dumpDir;
}
+ BindingClassLoader(final ClassLoader parentLoader, final @Nullable Path dumpDir) {
+ this(parentLoader, dumpDir != null ? dumpDir.toFile() : null);
+ }
+
BindingClassLoader(final BindingClassLoader parentLoader) {
this(parentLoader, parentLoader.dumpDir);
}
* @param dumpDir Directory in which to dump loaded bytecode
* @return A new BindingClassLoader.
* @throws NullPointerException if {@code parentLoader} is {@code null}
+ * @deprecated Use {@link #builder(Class)} instead
*/
+ @Deprecated(since = "14.0.7")
public static @NonNull BindingClassLoader create(final Class<?> rootClass, final @Nullable File dumpDir) {
- final var parentLoader = rootClass.getClassLoader();
- return AccessController.doPrivileged(
- (PrivilegedAction<BindingClassLoader>)() -> new RootBindingClassLoader(parentLoader, dumpDir));
+ final var builder = builder(rootClass);
+ if (dumpDir != null) {
+ builder.dumpBytecode(dumpDir.toPath());
+ }
+ return builder.build();
+ }
+
+ public static @NonNull Builder builder(final Class<?> rootClass) {
+ return new Builder(rootClass.getClassLoader());
+ }
+
+ /**
+ * Instantiate a new BindingClassLoader, which serves as the root of generated code loading.
+ *
+ * @param rootClass Class from which to derive the class loader
+ * @return A new BindingClassLoader.
+ * @throws NullPointerException if {@code parentLoader} is {@code null}
+ */
+ public static @NonNull BindingClassLoader ofRootClass(final Class<?> rootClass) {
+ return builder(rootClass).build();
}
/**
import static com.google.common.base.Verify.verify;
import com.google.common.collect.ImmutableMap;
-import java.io.File;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
+import java.nio.file.Path;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
private volatile ImmutableMap<ClassLoader, BindingClassLoader> loaders = ImmutableMap.of();
- RootBindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
+ RootBindingClassLoader(final ClassLoader parentLoader, final @Nullable Path dumpDir) {
super(parentLoader, dumpDir);
}
final @NonNull BindingClassLoader found;
if (!isOurClass(bindingClass)) {
verifyStaticLinkage(target);
- found = AccessController.doPrivileged(
- (PrivilegedAction<BindingClassLoader>)() -> new LeafBindingClassLoader(this, target));
+ found = SecuritySupport.get(() -> new LeafBindingClassLoader(this, target));
LOG.debug("Allocated {} for {}", found, target);
} else {
found = this;
--- /dev/null
+/*
+ * Copyright (c) 2025 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.binding.loader;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.function.Supplier;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Internal machinery to deal with {@link AccessController}: if we detect Java 24+ at runtime, we do not touch
+ * {@code AccessController} and rely on <a href="https://openjdk.org/jeps/486">JEP-486</a> semantics instead.
+ */
+// TODO: can we simplify this with a multi-release jar?
+@NonNullByDefault
+abstract sealed class SecuritySupport {
+ /**
+ * Java >=24: {@link AccessController#doPrivileged(PrivilegedAction)} is a no-op wrapper. Inline its behaviour
+ * without touching it, as it may disappear in later versions of Java.
+ */
+ // FIXME: assume this behaviour and eliminate this entire abstract once we require Java 24+
+ private static final class NoAccessController extends SecuritySupport {
+ @Override
+ <T> T privilegedGet(final Supplier<T> supplier) {
+ return supplier.get();
+ }
+ }
+
+ /**
+ * Java <24: defer to {@link AccessController#doPrivileged(PrivilegedAction)}.
+ */
+ private static final class WithAccessController extends SecuritySupport {
+ @Override
+ @SuppressWarnings({ "deprecation", "removal" })
+ <T> T privilegedGet(final Supplier<T> supplier) {
+ return AccessController.doPrivileged((PrivilegedAction<T>) supplier::get);
+ }
+ }
+
+ private static final SecuritySupport INSTANCE;
+
+ static {
+ final String str;
+ if (Runtime.version().feature() >= 24) {
+ str = ">=24";
+ INSTANCE = new NoAccessController();
+ } else {
+ str = "<24";
+ INSTANCE = new WithAccessController();
+ }
+ LoggerFactory.getLogger(SecuritySupport.class).debug("Assuming Java {} AccessController semantics", str);
+ }
+
+ static final <T> T get(final Supplier<T> supplier) {
+ return INSTANCE.privilegedGet(supplier);
+ }
+
+ abstract <T> T privilegedGet(Supplier<T> supplier);
+}
\ No newline at end of file