io.activej.codegen.DefiningClassLoader Maven / Gradle / Ivy
Show all versions of activej-codegen Show documentation
/*
* Copyright (C) 2020 ActiveJ LLC.
*
* Licensed 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 io.activej.codegen;
import io.activej.common.builder.AbstractBuilder;
import org.jetbrains.annotations.Nullable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import static io.activej.codegen.util.Utils.getPathSetting;
import static java.util.function.UnaryOperator.identity;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
/**
* A {@link ClassLoader} for defining dynamically generated classes.
* Also supports in memory caching as well as persistent caching of defined classes.
*
* To simply define a new class from a bytecode use {@link #defineClass(String, byte[])} method.
*
* For in memory caching of classes use {@link #ensureClass(ClassKey, Supplier)} and
* {@link #ensureClass(ClassKey, Function)} methods.
*
* As an alternative for in memory caching you may use {@link #ensureClass(String, Supplier)} and
* {@link #ensureClass(String, BiFunction)} methods without specifying a {@link #bytecodeStorage}.
*
* For persistent caching of classes you need to use {@link #ensureClass(String, Supplier)} and
* {@link #ensureClass(String, BiFunction)} methods and also specify a persistent {@link IBytecodeStorage} using
* {@link Builder#withBytecodeStorage(IBytecodeStorage)} method.
*/
@SuppressWarnings("WeakerAccess")
public final class DefiningClassLoader extends ClassLoader implements DefiningClassLoaderMBean {
public static final Path DEFAULT_DEBUG_OUTPUT_DIR = getPathSetting(DefiningClassLoader.class, "debugOutputDir", null);
private final Map> definedClasses = new ConcurrentHashMap<>();
private final Map, AtomicReference>> cachedClasses = new ConcurrentHashMap<>();
private @Nullable IBytecodeStorage bytecodeStorage;
private Path debugOutputDir = DEFAULT_DEBUG_OUTPUT_DIR;
private DefiningClassLoader() {
}
private DefiningClassLoader(ClassLoader parent) {
super(parent);
}
/**
* Creates a new instance of {@code DefiningClassLoader}
* with system class loader as a parent class loader
*
* @return a new instance of a {@code DefiningClassLoader}
*/
public static DefiningClassLoader create() {
return builder().build();
}
/**
* Creates a new instance of {@code DefiningClassLoader}
* with given class loader as a parent class loader
*
* @param parent parent class loader
* @return a new instance of a {@code DefiningClassLoader}
*/
public static DefiningClassLoader create(ClassLoader parent) {
return builder(parent).build();
}
public static Builder builder() {
return new DefiningClassLoader().new Builder();
}
public static Builder builder(ClassLoader parent) {
return new DefiningClassLoader(parent).new Builder();
}
public final class Builder extends AbstractBuilder {
private Builder() {}
/**
* Adds a persistent cache for the bytecode that is used for defining classes.
*
* @param bytecodeStorage a persistent storage of bytecode
* @see IBytecodeStorage
*/
public Builder withBytecodeStorage(IBytecodeStorage bytecodeStorage) {
checkNotBuilt(this);
DefiningClassLoader.this.bytecodeStorage = bytecodeStorage;
return this;
}
/**
* Writes all classes to the specified directory once a class is defined.
*
* If a directory does not exist when class is defined, a runtime error will be thrown.
*
* @param debugOutputDir directory where bytecode would be written to for debug purposes
*/
public Builder withDebugOutputDir(Path debugOutputDir) {
checkNotBuilt(this);
DefiningClassLoader.this.debugOutputDir = debugOutputDir;
return this;
}
@Override
protected DefiningClassLoader doBuild() {
return DefiningClassLoader.this;
}
}
/**
* Defines a class using a given class name and bytecode
*
* @param className name of a defined class
* @param bytecode bytecode of a defined class
* @return newly defined class
*/
public Class> defineClass(String className, byte[] bytecode) {
Class> aClass = super.defineClass(className, bytecode, 0, bytecode.length);
definedClasses.put(className, aClass);
if (debugOutputDir != null) {
try (FileOutputStream fos = new FileOutputStream(debugOutputDir.resolve(className + ".class").toFile())) {
fos.write(bytecode);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return aClass;
}
/**
* Ensures that a class of a given name is present
*
* @see #ensureClass(String, BiFunction)
*/
public Class ensureClass(String className, Supplier> classGenerator) {
return ensureClass(className, (cl, s) -> classGenerator.get().generateBytecode(cl, s));
}
/**
* Ensures that a class of a given name is present
*
* @see #ensureClass(String, BiFunction)
*/
public Class ensureClass(ClassKey key, Supplier> classGenerator) {
return ensureClass(key, classLoader -> classGenerator.get().generateBytecode(classLoader));
}
/**
* Ensures that a class of a given name is present
*
* @see #ensureClass(String, BiFunction)
*/
public T ensureClassAndCreateInstance(
String className, Supplier> classGenerator, Object... arguments
) {
return createInstance(ensureClass(className, classGenerator), arguments);
}
/**
* Ensures that a class of a given name is present and creates a new instance of such class
*
* @see #ensureClass(String, BiFunction)
*/
public T ensureClassAndCreateInstance(
ClassKey key, Supplier> classGenerator, Object... arguments
) {
Class aClass = ensureClass(key, classGenerator);
return createInstance(aClass, arguments);
}
/**
* Ensures that a class of a given name is present either by loading it from a {@link IBytecodeStorage} or
* by creating the class using given bytecode factory.
*
* If a persistent {@link IBytecodeStorage} is not set, a built-in in memory cache will be used. Which means that
* a bytecode factory will be called at most once.
*
* If a persistent {@link IBytecodeStorage} is set, a generated bytecode would be stored in the storage. This way
* the cache would survive application restarts, which would allow optimizing startup time.
*
* @param className a desired name of a class
* @param bytecodeBuilder factory that creates a {@link GeneratedBytecode} out of {@code this} {@link DefiningClassLoader}
* and a class name
* @param type parameter that represents ensured class
* @return an ensured class
*/
@SuppressWarnings("unchecked")
public Class ensureClass(String className, BiFunction bytecodeBuilder) {
try {
return (Class) loadClass(className, false);
} catch (ClassNotFoundException ignored) {
}
synchronized (getClassLoadingLock(className)) {
if (bytecodeStorage != null) {
byte[] bytecode = bytecodeStorage.loadBytecode(className).orElse(null);
if (bytecode != null) {
return (Class) defineClass(className, bytecode);
}
}
try (GeneratedBytecode generatedBytecode = bytecodeBuilder.apply(this, className)) {
Class generatedClass = (Class) generatedBytecode.generateClass(this);
if (bytecodeStorage != null) {
bytecodeStorage.saveBytecode(className, generatedBytecode.getBytecode());
}
return generatedClass;
}
}
}
/**
* Ensures that a class of a given name is present either by loading it from in memory cache or
* by creating the class using given bytecode factory.
*
* Defined classes a stored in a cache by a {@link ClassKey}, which is a combination of some superclass as well as
* an array of some arbitrary arguments.
*
* A bytecode factory will be called at most once. Classes ensured using this method
* are not persisted between application restarts.
*
* @param key a key of a class
* @param bytecodeBuilder factory that creates a {@link GeneratedBytecode} out of {@code this} {@link DefiningClassLoader}
* @param type parameter that represents ensured class
* @return an ensured class
*/
public Class ensureClass(ClassKey key, Function bytecodeBuilder) {
AtomicReference> reference = cachedClasses.computeIfAbsent(key, k -> new AtomicReference<>());
Class> generatedClass = reference.get();
if (generatedClass == null) {
synchronized (reference) {
generatedClass = reference.get();
if (generatedClass == null) {
try (GeneratedBytecode generatedBytecode = bytecodeBuilder.apply(this)) {
generatedClass = generatedBytecode.generateClass(this);
}
reference.set(generatedClass);
}
}
}
//noinspection unchecked
return (Class) generatedClass;
}
/**
* Ensures that a class of a given name is present and creates a new instance of such class
*
* @see #ensureClass(ClassKey, Function)
*/
public T ensureClassAndCreateInstance(
ClassKey key, Function bytecodeBuilder, Object... arguments
) {
Class aClass = ensureClass(key, bytecodeBuilder);
return createInstance(aClass, arguments);
}
static T createInstance(Class aClass, Object[] arguments) {
try {
return aClass
.getConstructor(Arrays.stream(arguments).map(Object::getClass).toArray(Class>[]::new))
.newInstance(arguments);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/**
* Returns a cached class by a class key
*
* @param key a class key
* @return a cached class
*/
public @Nullable Class> getCachedClass(ClassKey> key) {
return Optional.ofNullable(cachedClasses.get(key)).map(AtomicReference::get).orElse(null);
}
// region JMX
@Override
public int getDefinedClassesCount() {
return definedClasses.size();
}
@Override
public Map getDefinedClassesCountByType() {
return definedClasses.values().stream()
.map(aClass -> aClass.getSuperclass() == Object.class && aClass.getInterfaces().length != 0 ?
aClass.getInterfaces()[0] :
aClass.getSuperclass())
.map(Class::getName)
.collect(groupingBy(identity(), counting()));
}
@Override
public int getCachedClassesCount() {
return cachedClasses.size();
}
@Override
public Map getCachedClassesCountByType() {
return cachedClasses.keySet().stream()
.map(key -> key.getKeyClass().getName())
.collect(groupingBy(identity(), counting()));
}
// endregion
@Override
public String toString() {
return "{classes=" + cachedClasses.size() + ", byType=" + getCachedClassesCountByType() + '}';
}
}