package net.bytebuddy.build.maven;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.build.EntryPoint;
import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer;
import net.bytebuddy.implementation.LoadedTypeInitializer;
import net.bytebuddy.pool.TypePool;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.*;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A Maven plugin for applying Byte Buddy transformations during a build.
*/
public abstract class ByteBuddyMojo extends AbstractMojo {
/**
* The file extension of a Java class file.
*/
private static final String CLASS_FILE_EXTENSION = ".class";
/**
* The built project's group id.
*/
@Parameter(defaultValue = "${project.groupId}", required = true, readonly = true)
protected String groupId;
/**
* The built project's artifact id.
*/
@Parameter(defaultValue = "${project.artifactId}", required = true, readonly = true)
protected String artifactId;
/**
* The built project's version.
*/
@Parameter(defaultValue = "${project.version}", required = true, readonly = true)
protected String version;
/**
* <p>
* The list of transformations. A transformation <b>must</b> specify the {@code plugin} property, containing the name of a class to apply.
* Additionally, it is possible to optionally specify Maven coordinates for a project that contains this plugin class as {@code groupId},
* {@code artifactId} and {@code version}. If any of the latter properties is not set, this projects coordinate is used.
* </p>
* <p>
* For example, the following configuration applies the {@code foo.Bar} class which must implement {@link Plugin} from artifact
* {@code transform-artifact} with this project's group and version:
* </p>
* <blockquote><pre>{@code
* <transformations>
* <transformation>
* <plugin>foo.Bar< /plugin>
* <artifactId>transform-artifact< /artifactId>
* < /transformation>
* < /transformations>
* }</pre></blockquote>
* <p>
* If the list of {@code transformations} is empty or is not supplied at all, this plugin does not apply but prints a warning.
* </p>
*/
@Parameter
protected List<Transformation> transformations;
/**
* <p>
* The initializer used for creating a {@link ByteBuddy} instance and for applying a transformation. By default, a type is
* rebased. The initializer's {@code entryPoint} property can be set to any constant name of {@link EntryPoint.Default} or
* to a class name. If the latter applies, it is possible to set Maven coordinates for a Maven plugin which defines this
* class where any property defaults to this project's coordinates.
* </p>
* <p>
* For example, the following configuration applies the {@code foo.Qux} class which must implement {@link EntryPoint} from
* artifact {@code initialization-artifact} with this project's group and version:
* </p>
* <blockquote><pre>{@code
* <initialization>
* <entryPoint>foo.Qux< /entryPoint>
* <artifactId>initialization-artifact< /artifactId>
* < /initialization>
* }</pre></blockquote>
*/
@Parameter
protected Initialization initialization;
/**
* Specifies the method name suffix that is used when type's method need to be rebased. If this property is not
* set or is empty, a random suffix will be appended to any rebased method. If this property is set, the supplied
* value is appended to the original method name.
*/
@Parameter
protected String suffix;
/**
* When transforming classes during build time, it is not possible to apply any transformations which require a class
* in its loaded state. Such transformations might imply setting a type's static field to a user interceptor or similar
* transformations. If this property is set to {@code false}, this plugin does not throw an exception if such a live
* initializer is defined during a transformation process.
*/
@Parameter(defaultValue = "true", required = true)
protected boolean failOnLiveInitializer;
/**
* The currently used repository system.
*/
@Component
protected RepositorySystem repositorySystem;
/**
* The currently used system session for the repository system.
*/
@Parameter(defaultValue = "${repositorySystemSession}", required = true, readonly = true)
protected RepositorySystemSession repositorySystemSession;
/**
* A list of all remote repositories.
*/
@Parameter(defaultValue = "${project.remoteProjectRepositories}", required = true, readonly = true)
protected List<RemoteRepository> remoteRepositories;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (transformations == null || transformations.isEmpty()) {
getLog().warn("No transformations are specified. Skipping plugin application.");
return;
}
ByteBuddyLogHandler byteBuddyLogHandler = ByteBuddyLogHandler.initialize(getLog());
try {
processOutputDirectory(new File(getOutputDirectory()), getClassPathElements());
} catch (IOException exception) {
throw new MojoFailureException("Error during writing process", exception);
} finally {
byteBuddyLogHandler.reset();
}
}
/**
* Returns the output directory to search for class files.
*
* @return The output directory to search for class files.
*/
protected abstract String getOutputDirectory();
/**
* Returns the class path elements of the relevant output directory.
*
* @return The class path elements of the relevant output directory.
*/
protected abstract List<String> getClassPathElements();
/**
* Processes all class files within the given directory.
*
* @param root The root directory to process.
* @param classPath A list of class path elements expected by the processed classes.
* @throws MojoExecutionException If the user configuration results in an error.
* @throws MojoFailureException If the plugin application raises an error.
* @throws IOException If an I/O exception occurs.
*/
@SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "Applies Maven exception wrapper")
private void processOutputDirectory(File root, List<? extends String> classPath) throws MojoExecutionException, MojoFailureException, IOException {
if (!root.isDirectory()) {
throw new MojoExecutionException("Target location does not exist or is no directory: " + root);
}
ClassLoaderResolver classLoaderResolver = new ClassLoaderResolver(getLog(), repositorySystem, repositorySystemSession, remoteRepositories);
try {
List<Plugin> plugins = new ArrayList<Plugin>(transformations.size());
for (Transformation transformation : transformations) {
String plugin = transformation.getPlugin();
try {
plugins.add((Plugin) Class.forName(plugin, false, classLoaderResolver.resolve(transformation.asCoordinate(groupId, artifactId, version)))
.getDeclaredConstructor()
.newInstance());
getLog().info("Created plugin: " + plugin);
} catch (Exception exception) {
throw new MojoExecutionException("Cannot create plugin: " + transformation.getRawPlugin(), exception);
}
}
EntryPoint entryPoint = (initialization == null
? Initialization.makeDefault()
: initialization).getEntryPoint(classLoaderResolver, groupId, artifactId, version);
getLog().info("Resolved entry point: " + entryPoint);
transform(root, entryPoint, classPath, plugins);
} finally {
classLoaderResolver.close();
}
}
/**
* Applies all registered transformations.
*
* @param root The root directory to process.
* @param entryPoint The transformation's entry point.
* @param classPath A list of class path elements expected by the processed classes.
* @param plugins The plugins to apply.
* @throws MojoExecutionException If the user configuration results in an error.
* @throws MojoFailureException If the plugin application raises an error.
* @throws IOException If an I/O exception occurs.
*/
private void transform(File root,
EntryPoint entryPoint,
List<? extends String> classPath,
List<Plugin> plugins) throws MojoExecutionException, MojoFailureException, IOException {
List<ClassFileLocator> classFileLocators = new ArrayList<ClassFileLocator>(classPath.size() + 1);
classFileLocators.add(new ClassFileLocator.ForFolder(root));
for (String target : classPath) {
File artifact = new File(target);
classFileLocators.add(artifact.isFile()
? ClassFileLocator.ForJarFile.of(artifact)
: new ClassFileLocator.ForFolder(artifact));
}
ClassFileLocator classFileLocator = new ClassFileLocator.Compound(classFileLocators);
try {
TypePool typePool = new TypePool.Default.WithLazyResolution(new TypePool.CacheProvider.Simple(),
classFileLocator,
TypePool.Default.ReaderMode.FAST,
TypePool.ClassLoading.ofBootPath());
getLog().info("Processing class files located in in: " + root);
ByteBuddy byteBuddy;
try {
byteBuddy = entryPoint.getByteBuddy();
} catch (Throwable throwable) {
throw new MojoExecutionException("Cannot create Byte Buddy instance", throwable);
}
processDirectory(root,
root,
byteBuddy,
entryPoint,
suffix == null || suffix.isEmpty()
? MethodNameTransformer.Suffixing.withRandomSuffix()
: new MethodNameTransformer.Suffixing(suffix),
classFileLocator,
typePool,
plugins);
} finally {
classFileLocator.close();
}
}
/**
* Processes a directory.
*
* @param root The root directory to process.
* @param folder The currently processed folder.
* @param byteBuddy The Byte Buddy instance to use.
* @param entryPoint The transformation's entry point.
* @param methodNameTransformer The method name transformer to use.
* @param classFileLocator The class file locator to use.
* @param typePool The type pool to query for type descriptions.
* @param plugins The plugins to apply.
* @throws MojoExecutionException If the user configuration results in an error.
* @throws MojoFailureException If the plugin application raises an error.
*/
private void processDirectory(File root,
File folder,
ByteBuddy byteBuddy,
EntryPoint entryPoint,
MethodNameTransformer methodNameTransformer,
ClassFileLocator classFileLocator,
TypePool typePool,
List<Plugin> plugins) throws MojoExecutionException, MojoFailureException {
File[] file = folder.listFiles();
if (file != null) {
for (File aFile : file) {
if (aFile.isDirectory()) {
processDirectory(root, aFile, byteBuddy, entryPoint, methodNameTransformer, classFileLocator, typePool, plugins);
} else if (aFile.isFile() && aFile.getName().endsWith(CLASS_FILE_EXTENSION)) {
processClassFile(root,
root.toURI().relativize(aFile.toURI()).toString(),
byteBuddy,
entryPoint,
methodNameTransformer,
classFileLocator,
typePool,
plugins);
} else {
getLog().debug("Skipping ignored file: " + aFile);
}
}
}
}
/**
* Processes a class file.
*
* @param root The root directory to process.
* @param file The class file to process.
* @param byteBuddy The Byte Buddy instance to use.
* @param entryPoint The transformation's entry point.
* @param methodNameTransformer The method name transformer to use.
* @param classFileLocator The class file locator to use.
* @param typePool The type pool to query for type descriptions.
* @param plugins The plugins to apply.
* @throws MojoExecutionException If the user configuration results in an error.
* @throws MojoFailureException If the plugin application raises an error.
*/
private void processClassFile(File root,
String file,
ByteBuddy byteBuddy,
EntryPoint entryPoint,
MethodNameTransformer methodNameTransformer,
ClassFileLocator classFileLocator,
TypePool typePool,
List<Plugin> plugins) throws MojoExecutionException, MojoFailureException {
String typeName = file.replace('/', '.').substring(0, file.length() - CLASS_FILE_EXTENSION.length());
getLog().debug("Processing class file: " + typeName);
TypeDescription typeDescription = typePool.describe(typeName).resolve();
DynamicType.Builder<?> builder;
try {
builder = entryPoint.transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer);
} catch (Throwable throwable) {
throw new MojoExecutionException("Cannot transform type: " + typeName, throwable);
}
boolean transformed = false;
for (Plugin plugin : plugins) {
try {
if (plugin.matches(typeDescription)) {
builder = plugin.apply(builder, typeDescription);
transformed = true;
}
} catch (Throwable throwable) {
throw new MojoExecutionException("Cannot apply " + plugin + " on " + typeName, throwable);
}
}
if (transformed) {
getLog().info("Transformed type: " + typeName);
DynamicType dynamicType = builder.make();
for (Map.Entry<TypeDescription, LoadedTypeInitializer> entry : dynamicType.getLoadedTypeInitializers().entrySet()) {
if (failOnLiveInitializer && entry.getValue().isAlive()) {
throw new MojoExecutionException("Cannot apply live initializer for " + entry.getKey());
}
}
try {
dynamicType.saveIn(root);
} catch (IOException exception) {
throw new MojoFailureException("Cannot save " + typeName + " in " + root, exception);
}
} else {
getLog().debug("Skipping non-transformed type: " + typeName);
}
}
/**
* A Byte Buddy plugin that transforms a project's production class files.
*/
@Mojo(name = "transform",
defaultPhase = LifecyclePhase.PROCESS_CLASSES,
threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE)
public static class ForProductionTypes extends ByteBuddyMojo {
/**
* The current build's production output directory.
*/
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
protected String outputDirectory;
/**
* The production class path.
*/
@Parameter(defaultValue = "${project.compileClasspathElements}", required = true, readonly = true)
protected List<String> compileClasspathElements;
@Override
protected String getOutputDirectory() {
return outputDirectory;
}
@Override
protected List<String> getClassPathElements() {
return compileClasspathElements;
}
}
/**
* A Byte Buddy plugin that transforms a project's test class files.
*/
@Mojo(name = "transform-test",
defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES,
threadSafe = true,
requiresDependencyResolution = ResolutionScope.TEST)
public static class ForTestTypes extends ByteBuddyMojo {
/**
* The current build's test output directory.
*/
@Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true)
protected String testOutputDirectory;
/**
* The test class path.
*/
@Parameter(defaultValue = "${project.testClasspathElements}", required = true, readonly = true)
protected List<String> testClasspathElements;
@Override
protected String getOutputDirectory() {
return testOutputDirectory;
}
@Override
protected List<String> getClassPathElements() {
return testClasspathElements;
}
}
}