org.opendaylight.yangtools.binding.loader.BindingClassLoader Maven / Gradle / Ivy
Show all versions of binding-loader Show documentation
/*
* Copyright (c) 2019 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 static com.google.common.base.Verify.verify;
import static java.util.Objects.requireNonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
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.util.Collection;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.Set;
import java.util.function.Supplier;
import net.bytebuddy.dynamic.DynamicType.Unloaded;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
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.
*
* 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
* instance has visibility only into codec classes and for each classloader we encounter when presented with a binding
* class we create a leaf instance and cache it in the root instance. Leaf instances are using the root loader as their
* parent, but consult the binding class's class loader if the root loader fails to load a particular class.
*
*
In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf
* loader is created.
*/
public abstract sealed class BindingClassLoader extends ClassLoader
permits LeafBindingClassLoader, RootBindingClassLoader {
/**
* A class generator, generating a class of a particular type.
*
* @param Type of generated class
*/
public interface ClassGenerator {
/**
* Generate a class.
*
* @param fqcn Generated class Fully-qualified class name
* @param bindingInterface Binding interface for which the class is being generated
* @return A result.
*/
GeneratorResult generateClass(BindingClassLoader loader, String fqcn, Class> bindingInterface);
/**
* Run the specified loader in a customized environment. The environment customizations must be cleaned up by
* the time this method returns. The default implementation performs no customization.
*
* @param loader Class loader to execute
* @return Class returned by the loader
*/
default Class customizeLoading(final @NonNull Supplier> loader) {
return loader.get();
}
}
/**
* Result of class generation.
*
* @param Type of generated class.
*/
public static final class GeneratorResult {
private final @NonNull ImmutableSet> dependecies;
private final @NonNull Unloaded result;
GeneratorResult(final Unloaded result, final ImmutableSet> dependecies) {
this.result = requireNonNull(result);
this.dependecies = requireNonNull(dependecies);
}
public static @NonNull GeneratorResult of(final Unloaded result) {
return new GeneratorResult<>(result, ImmutableSet.of());
}
public static @NonNull GeneratorResult of(final Unloaded result,
final Collection> dependencies) {
return dependencies.isEmpty() ? of(result) : new GeneratorResult<>(result,
ImmutableSet.copyOf(dependencies));
}
@NonNull Unloaded getResult() {
return result;
}
@NonNull ImmutableSet> getDependencies() {
return dependecies;
}
}
private static final ClassLoadingStrategy STRATEGY = (classLoader, types) -> {
verify(types.size() == 1, "Unexpected multiple types", types);
final var entry = types.entrySet().iterator().next();
return ImmutableMap.of(entry.getKey(), classLoader.loadClass(entry.getKey().getName(), entry.getValue()));
};
static {
verify(ClassLoader.registerAsParallelCapable());
}
private static final Logger LOG = LoggerFactory.getLogger(BindingClassLoader.class);
private final @Nullable File dumpDir;
BindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
super(parentLoader);
this.dumpDir = dumpDir;
}
BindingClassLoader(final BindingClassLoader parentLoader) {
this(parentLoader, parentLoader.dumpDir);
}
/**
* Instantiate a new BindingClassLoader, which serves as the root of generated code loading.
*
* @param rootClass Class from which to derive the class loader
* @param dumpDir Directory in which to dump loaded bytecode
* @return A new BindingClassLoader.
* @throws NullPointerException if {@code parentLoader} is {@code null}
*/
public static @NonNull BindingClassLoader create(final Class> rootClass, final @Nullable File dumpDir) {
final var parentLoader = rootClass.getClassLoader();
return AccessController.doPrivileged(
(PrivilegedAction)() -> new RootBindingClassLoader(parentLoader, dumpDir));
}
/**
* Generate a class which is related to specified compile-type-generated interface.
*
* @param Type of generated class
* @param bindingInterface Binding compile-time-generated interface
* @param fqcn Fully-Qualified Class Name of the generated class
* @param generator Code generator to run
* @return A generated class object
* @throws NullPointerException if any argument is null
*/
public final @NonNull Class generateClass(final Class> bindingInterface, final String fqcn,
final ClassGenerator generator) {
return findClassLoader(requireNonNull(bindingInterface)).doGenerateClass(bindingInterface, fqcn, generator);
}
public final @NonNull Class> getGeneratedClass(final Class> bindingInterface, final String fqcn) {
final var loader = findClassLoader(requireNonNull(bindingInterface));
final Class> ret;
synchronized (loader.getClassLoadingLock(fqcn)) {
ret = loader.findLoadedClass(fqcn);
}
if (ret == null) {
throw new IllegalArgumentException("Failed to find generated class " + fqcn + " for " + bindingInterface);
}
return ret;
}
@Override
public final int hashCode() {
return super.hashCode();
}
@Override
public final boolean equals(final Object obj) {
return super.equals(obj);
}
@Override
public final String toString() {
return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
}
ToStringHelper addToStringAttributes(final ToStringHelper helper) {
return helper.add("identity", HexFormat.of().toHexDigits(hashCode())).add("parent", getParent());
}
/**
* Append specified loaders to this class loader for the purposes of looking up generated classes. Note that the
* loaders are expected to have required classes already loaded. This is required to support generation of
* inter-dependent structures, such as those used for streaming binding interfaces.
*
* @param newLoaders Loaders to append
* @throws NullPointerException if {@code loaders} is null
*/
abstract void appendLoaders(@NonNull Set newLoaders);
/**
* Find the loader responsible for holding classes related to a binding class.
*
* @param bindingClass Class to locate
* @return a Loader instance
* @throws NullPointerException if {@code bindingClass} is null
*/
abstract @NonNull BindingClassLoader findClassLoader(@NonNull Class> bindingClass);
private @NonNull Class doGenerateClass(final Class> bindingInterface, final String fqcn,
final ClassGenerator generator) {
synchronized (getClassLoadingLock(fqcn)) {
// Attempt to find a loaded class
final var existing = findLoadedClass(fqcn);
if (existing != null) {
return (Class) existing;
}
final var result = generator.generateClass(this, fqcn, bindingInterface);
final var unloaded = result.getResult();
verify(fqcn.equals(unloaded.getTypeDescription().getName()), "Unexpected class in %s", unloaded);
verify(unloaded.getAuxiliaryTypes().isEmpty(), "Auxiliary types present in %s", unloaded);
dumpBytecode(unloaded);
processDependencies(result.getDependencies());
return generator.customizeLoading(() -> (Class) unloaded.load(this, STRATEGY).getLoaded());
}
}
final Class> loadClass(final String fqcn, final byte[] byteCode) {
synchronized (getClassLoadingLock(fqcn)) {
final var existing = findLoadedClass(fqcn);
verify(existing == null, "Attempted to load existing %s", existing);
return defineClass(fqcn, byteCode, 0, byteCode.length);
}
}
private void processDependencies(final Collection> deps) {
final var depLoaders = new HashSet();
for (var dep : deps) {
final var depLoader = dep.getClassLoader();
verify(depLoader instanceof BindingClassLoader, "Dependency %s is not a generated class", dep);
if (equals(depLoader)) {
// Same loader, skip
continue;
}
try {
loadClass(dep.getName());
} catch (ClassNotFoundException e) {
LOG.debug("Cannot find {} in local loader, attempting to compensate", dep, e);
// Root loader is always visible from a leaf, hence the dependency can only be a leaf
verify(depLoader instanceof LeafBindingClassLoader, "Dependency loader %s is not a leaf", depLoader);
depLoaders.add((LeafBindingClassLoader) depLoader);
}
}
if (!depLoaders.isEmpty()) {
appendLoaders(depLoaders);
}
}
private void dumpBytecode(final Unloaded> unloaded) {
final var dir = dumpDir;
if (dir != null) {
try {
unloaded.saveIn(dir);
} catch (IOException | IllegalArgumentException e) {
LOG.info("Failed to save {}", unloaded.getTypeDescription().getName(), e);
}
}
}
}