2 * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.mdsal.binding.loader;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.collect.ImmutableMap;
15 import com.google.common.collect.ImmutableSet;
17 import java.io.IOException;
18 import java.security.AccessController;
19 import java.security.PrivilegedAction;
20 import java.util.Collection;
21 import java.util.HashSet;
23 import java.util.function.Supplier;
24 import net.bytebuddy.dynamic.DynamicType.Unloaded;
25 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * A ClassLoader hosting types generated for a particular type. A root instance is attached to a
33 * BindingCodecContext instance, so any generated classes from it can be garbage-collected when the context
34 * is destroyed, as well as to prevent two contexts trampling over each other.
37 * It semantically combines two class loaders: the class loader in which this class is loaded and the class loader in
38 * which a target Binding interface/class is loaded. This inherently supports multi-classloader environments -- the root
39 * instance has visibility only into codec classes and for each classloader we encounter when presented with a binding
40 * class we create a leaf instance and cache it in the root instance. Leaf instances are using the root loader as their
41 * parent, but consult the binding class's class loader if the root loader fails to load a particular class.
43 * <p>In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf
46 public abstract sealed class BindingClassLoader extends ClassLoader
47 permits LeafBindingClassLoader, RootBindingClassLoader {
49 * A class generator, generating a class of a particular type.
51 * @param <T> Type of generated class
53 public interface ClassGenerator<T> {
57 * @param fqcn Generated class Fully-qualified class name
58 * @param bindingInterface Binding interface for which the class is being generated
61 GeneratorResult<T> generateClass(BindingClassLoader loader, String fqcn, Class<?> bindingInterface);
64 * Run the specified loader in a customized environment. The environment customizations must be cleaned up by
65 * the time this method returns. The default implementation performs no customization.
67 * @param loader Class loader to execute
68 * @return Class returned by the loader
70 default Class<T> customizeLoading(final @NonNull Supplier<Class<T>> loader) {
76 * Result of class generation.
78 * @param <T> Type of generated class.
80 public static final class GeneratorResult<T> {
81 private final @NonNull ImmutableSet<Class<?>> dependecies;
82 private final @NonNull Unloaded<T> result;
84 GeneratorResult(final Unloaded<T> result, final ImmutableSet<Class<?>> dependecies) {
85 this.result = requireNonNull(result);
86 this.dependecies = requireNonNull(dependecies);
89 public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result) {
90 return new GeneratorResult<>(result, ImmutableSet.of());
93 public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result,
94 final Collection<Class<?>> dependencies) {
95 return dependencies.isEmpty() ? of(result) : new GeneratorResult<>(result,
96 ImmutableSet.copyOf(dependencies));
99 @NonNull Unloaded<T> getResult() {
103 @NonNull ImmutableSet<Class<?>> getDependencies() {
108 private static final ClassLoadingStrategy<BindingClassLoader> STRATEGY = (classLoader, types) -> {
109 verify(types.size() == 1, "Unexpected multiple types", types);
110 final var entry = types.entrySet().iterator().next();
111 return ImmutableMap.of(entry.getKey(), classLoader.loadClass(entry.getKey().getName(), entry.getValue()));
115 verify(ClassLoader.registerAsParallelCapable());
118 private static final Logger LOG = LoggerFactory.getLogger(BindingClassLoader.class);
120 private final @Nullable File dumpDir;
122 BindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
124 this.dumpDir = dumpDir;
127 BindingClassLoader(final BindingClassLoader parentLoader) {
128 this(parentLoader, parentLoader.dumpDir);
132 * Instantiate a new BindingClassLoader, which serves as the root of generated code loading.
134 * @param rootClass Class from which to derive the class loader
135 * @param dumpDir Directory in which to dump loaded bytecode
136 * @return A new BindingClassLoader.
137 * @throws NullPointerException if {@code parentLoader} is {@code null}
139 public static @NonNull BindingClassLoader create(final Class<?> rootClass, final @Nullable File dumpDir) {
140 final var parentLoader = rootClass.getClassLoader();
141 return AccessController.doPrivileged(
142 (PrivilegedAction<BindingClassLoader>)() -> new RootBindingClassLoader(parentLoader, dumpDir));
146 * The name of the target class is formed through concatenation of the name of a {@code bindingInterface} and
147 * specified {@code suffix}.
149 * @param <T> Type of generated class
150 * @param bindingInterface Binding compile-time-generated interface
151 * @param suffix Suffix to use
152 * @param generator Code generator to run
153 * @return A generated class object
154 * @throws NullPointerException if any argument is null
156 public final <T> Class<T> generateClass(final Class<?> bindingInterface, final String suffix,
157 final ClassGenerator<T> generator) {
158 return findClassLoader(requireNonNull(bindingInterface)).doGenerateClass(bindingInterface, suffix, generator);
161 public final @NonNull Class<?> getGeneratedClass(final Class<?> bindingInterface, final String suffix) {
162 final var loader = findClassLoader(requireNonNull(bindingInterface));
163 final var fqcn = generatedClassName(bindingInterface, suffix);
166 synchronized (loader.getClassLoadingLock(fqcn)) {
167 ret = loader.findLoadedClass(fqcn);
170 checkArgument(ret != null, "Failed to find generated class %s for %s of %s", fqcn, suffix, bindingInterface);
175 * Append specified loaders to this class loader for the purposes of looking up generated classes. Note that the
176 * loaders are expected to have required classes already loaded. This is required to support generation of
177 * inter-dependent structures, such as those used for streaming binding interfaces.
179 * @param newLoaders Loaders to append
180 * @throws NullPointerException if {@code loaders} is null
182 abstract void appendLoaders(@NonNull Set<LeafBindingClassLoader> newLoaders);
185 * Find the loader responsible for holding classes related to a binding class.
187 * @param bindingClass Class to locate
188 * @return a Loader instance
189 * @throws NullPointerException if {@code bindingClass} is null
191 abstract @NonNull BindingClassLoader findClassLoader(@NonNull Class<?> bindingClass);
193 private <T> Class<T> doGenerateClass(final Class<?> bindingInterface, final String suffix,
194 final ClassGenerator<T> generator) {
195 final var fqcn = generatedClassName(bindingInterface, suffix);
197 synchronized (getClassLoadingLock(fqcn)) {
198 // Attempt to find a loaded class
199 final var existing = findLoadedClass(fqcn);
200 if (existing != null) {
201 return (Class<T>) existing;
204 final var result = generator.generateClass(this, fqcn, bindingInterface);
205 final var unloaded = result.getResult();
206 verify(fqcn.equals(unloaded.getTypeDescription().getName()), "Unexpected class in %s", unloaded);
207 verify(unloaded.getAuxiliaryTypes().isEmpty(), "Auxiliary types present in %s", unloaded);
208 dumpBytecode(unloaded);
210 processDependencies(result.getDependencies());
211 return generator.customizeLoading(() -> (Class<T>) unloaded.load(this, STRATEGY).getLoaded());
215 final Class<?> loadClass(final String fqcn, final byte[] byteCode) {
216 synchronized (getClassLoadingLock(fqcn)) {
217 final var existing = findLoadedClass(fqcn);
218 verify(existing == null, "Attempted to load existing %s", existing);
219 return defineClass(fqcn, byteCode, 0, byteCode.length);
223 private void processDependencies(final Collection<Class<?>> deps) {
224 final var depLoaders = new HashSet<LeafBindingClassLoader>();
225 for (var dep : deps) {
226 final var depLoader = dep.getClassLoader();
227 verify(depLoader instanceof BindingClassLoader, "Dependency %s is not a generated class", dep);
228 if (equals(depLoader)) {
234 loadClass(dep.getName());
235 } catch (ClassNotFoundException e) {
236 LOG.debug("Cannot find {} in local loader, attempting to compensate", dep, e);
237 // Root loader is always visible from a leaf, hence the dependency can only be a leaf
238 verify(depLoader instanceof LeafBindingClassLoader, "Dependency loader %s is not a leaf", depLoader);
239 depLoaders.add((LeafBindingClassLoader) depLoader);
243 if (!depLoaders.isEmpty()) {
244 appendLoaders(depLoaders);
248 private void dumpBytecode(final Unloaded<?> unloaded) {
249 final var dir = dumpDir;
252 unloaded.saveIn(dir);
253 } catch (IOException | IllegalArgumentException e) {
254 LOG.info("Failed to save {}", unloaded.getTypeDescription().getName(), e);
259 private static String generatedClassName(final Class<?> bindingInterface, final String suffix) {
260 return bindingInterface.getName() + "$$$" + suffix;