Improve BindingClassLoader.create(...) 21/114821/10
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 6 Jan 2025 16:59:26 +0000 (17:59 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 7 Jan 2025 00:01:54 +0000 (01:01 +0100)
We have an mandatory and an optional part here. Let's use a Builder with
java.nio.Path internally.

This ends up making BindingCodecContext loading a tad more obvious, as
we configure a JVM-wide Builder and then just call build() -- amortizing
internal loading stuff.

Also improve forward compatibility: when we run with Java 24, we assume
AccessController indirection is a no-op as per JEP-486 and do not touch
it -- thus making the code immune to AccessController not being
available in future versions.

JIRA: YANGTOOLS-1653
Change-Id: Ifc63b1fe4d85f8aef1d0d43f6535e849bebb9bf4
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/binding-data-codec-dynamic/src/main/java/org/opendaylight/yangtools/binding/data/codec/impl/BindingCodecContext.java
binding/binding-data-codec-dynamic/src/test/java/org/opendaylight/yangtools/binding/data/codec/impl/CodecClassLoaderTest.java
binding/binding-loader/src/main/java/org/opendaylight/yangtools/binding/loader/BindingClassLoader.java
binding/binding-loader/src/main/java/org/opendaylight/yangtools/binding/loader/RootBindingClassLoader.java
binding/binding-loader/src/main/java/org/opendaylight/yangtools/binding/loader/SecuritySupport.java [new file with mode: 0644]

index fa4f13da64ba27afc0f4a5f913717dacbb09068c..a7f053793cd2a92ec41e88b20db1065611295d3f 100644 (file)
@@ -14,7 +14,6 @@ import static com.google.common.base.Verify.verifyNotNull;
 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;
@@ -24,12 +23,12 @@ import com.google.common.collect.ImmutableList;
 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;
@@ -137,11 +136,15 @@ public final class BindingCodecContext extends AbstractBindingNormalizedNodeSeri
                    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;
     }
 
     /**
@@ -361,8 +364,7 @@ public final class BindingCodecContext extends AbstractBindingNormalizedNodeSeri
             }
         });
 
-    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;
index 490e80c4ca0946f71cb0b238a2f9660e1aa432f3..24e93d5c91159282b2d5a4ad4dbd948f728d4f49 100644 (file)
@@ -22,7 +22,7 @@ import org.opendaylight.yangtools.binding.loader.BindingClassLoader;
 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")
index 89f9ae389e9837385b2614723b8e5394993d8f79..ef98540c91476dad86bea07a2fb9e22207ab22d4 100644 (file)
@@ -16,8 +16,7 @@ import com.google.common.collect.ImmutableMap;
 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;
@@ -31,9 +30,10 @@ import org.slf4j.Logger;
 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
@@ -46,6 +46,28 @@ import org.slf4j.LoggerFactory;
  */
 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.
      *
@@ -120,11 +142,15 @@ public abstract sealed class BindingClassLoader extends ClassLoader
 
     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);
     }
@@ -136,11 +162,30 @@ public abstract sealed class BindingClassLoader extends ClassLoader
      * @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();
     }
 
     /**
index dfb7c6a15fb864bbe67bdfe80f9f971aed3ab993..9e61ca54c6193e7798121311b22cdc4f9ad3bcc5 100644 (file)
@@ -10,11 +10,9 @@ package org.opendaylight.yangtools.binding.loader;
 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;
@@ -39,7 +37,7 @@ final class RootBindingClassLoader extends BindingClassLoader {
 
     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);
     }
 
@@ -65,8 +63,7 @@ final class RootBindingClassLoader extends BindingClassLoader {
         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;
diff --git a/binding/binding-loader/src/main/java/org/opendaylight/yangtools/binding/loader/SecuritySupport.java b/binding/binding-loader/src/main/java/org/opendaylight/yangtools/binding/loader/SecuritySupport.java
new file mode 100644 (file)
index 0000000..0e9843d
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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