All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.bytebuddy.build.maven.ByteBuddyMojo Maven / Gradle / Ivy

/*
 * Copyright 2014 - 2020 Rafael Winterhalter
 *
 * 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 net.bytebuddy.build.maven;

import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.build.BuildLogger;
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.scaffold.inline.MethodNameTransformer;
import net.bytebuddy.utility.CompoundList;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.*;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
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 built project's group id.
     */
    @Parameter(defaultValue = "${project.groupId}", required = true, readonly = true)
    public String groupId;

    /**
     * The built project's artifact id.
     */
    @Parameter(defaultValue = "${project.artifactId}", required = true, readonly = true)
    public String artifactId;

    /**
     * The built project's version.
     */
    @Parameter(defaultValue = "${project.version}", required = true, readonly = true)
    public String version;

    /**
     * The built project's packaging.
     */
    @Parameter(defaultValue = "${project.packaging}", required = true, readonly = true)
    public String packaging;

    /**
     * The Maven project.
     */
    @Parameter(defaultValue = "${project}", readonly = true)
    public MavenProject project;

    /**
     * 

* The list of transformations. A transformation must 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. *

*

* 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: *

*
{@code
     * 
     *   
     *     foo.Bar< /plugin>
     *     transform-artifact< /artifactId>
     *   < /transformation>
     * < /transformations>
     * }
*

* If the list of {@code transformations} is empty or is not supplied at all, this plugin does not apply but prints a warning. *

*/ @Parameter public List transformations; /** *

* The initializer used for creating a {@link net.bytebuddy.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. *

*

* 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: *

*
{@code
     * 
     *   foo.Qux< /entryPoint>
     *   initialization-artifact< /artifactId>
     * < /initialization>
     * }
*/ @Parameter public 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 public 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) public boolean failOnLiveInitializer; /** * When set to {@code true}, this mojo is not applied to the current module. */ @Parameter(defaultValue = "false", required = true) public boolean skip; /** * When set to {@code true}, this mojo warns of an non-existent output directory. */ @Parameter(defaultValue = "true", required = true) public boolean warnOnMissingOutputDirectory; /** * When set to {@code true}, this mojo fails immediately if a plugin cannot be applied. */ @Parameter(defaultValue = "true", required = true) public boolean failFast; /** * When set to {@code true}, the debug information of class files should be parsed to extract parameter names. */ @Parameter(defaultValue = "false", required = true) public boolean extendedParsing; /** * Indicates the amount of threads used for parallel type processing or {@code 0} for serial processing. */ @Parameter(defaultValue = "0", required = true) public int threads; /** * The currently used repository system. */ @Component public RepositorySystem repositorySystem; /** * The currently used system session for the repository system. */ @Parameter(defaultValue = "${repositorySystemSession}", required = true, readonly = true) public RepositorySystemSession repositorySystemSession; /** * A list of all remote repositories. */ @Parameter(defaultValue = "${project.remoteProjectRepositories}", required = true, readonly = true) public List remoteRepositories; /** * {@inheritDoc} */ public void execute() throws MojoExecutionException, MojoFailureException { if (skip) { getLog().info("Not applying instrumentation as a result of plugin configuration."); return; } else if (transformations == null || transformations.isEmpty()) { getLog().warn("No transformations are specified. Skipping plugin application."); return; } try { apply(new File(getOutputDirectory()), getClassPathElements()); } catch (IOException exception) { throw new MojoFailureException("Error during writing process", exception); } } /** * 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 getClassPathElements(); /** * Applies the instrumentation. * * @param root The root folder that contains all class files. * @param classPath An iterable over all class path elements. * @throws MojoExecutionException If the plugin cannot be applied. * @throws IOException If an I/O exception occurs. */ @SuppressWarnings("unchecked") private void apply(File root, List classPath) throws MojoExecutionException, IOException { if (!root.exists()) { if (warnOnMissingOutputDirectory) { getLog().warn("Skipping instrumentation due to missing directory: " + root); } else { getLog().info("Skipping instrumentation due to missing directory: " + root); } return; } else if (!root.isDirectory()) { throw new MojoExecutionException("Not a directory: " + root); } ClassLoaderResolver classLoaderResolver = new ClassLoaderResolver(getLog(), repositorySystem, repositorySystemSession, remoteRepositories); try { List factories = new ArrayList(transformations.size()); for (Transformation transformation : transformations) { String plugin = transformation.getPlugin(); try { factories.add(new Plugin.Factory.UsingReflection((Class) Class.forName(plugin, false, classLoaderResolver.resolve(transformation.asCoordinate(groupId, artifactId, version, packaging)))) .with(transformation.makeArgumentResolvers()) .with(Plugin.Factory.UsingReflection.ArgumentResolver.ForType.of(File.class, root), Plugin.Factory.UsingReflection.ArgumentResolver.ForType.of(Log.class, getLog()), Plugin.Factory.UsingReflection.ArgumentResolver.ForType.of(BuildLogger.class, new MavenBuildLogger(getLog())))); getLog().info("Resolved plugin: " + transformation.getRawPlugin()); } catch (Throwable throwable) { throw new MojoExecutionException("Cannot resolve plugin: " + transformation.getRawPlugin(), throwable); } } EntryPoint entryPoint = (initialization == null ? Initialization.makeDefault() : initialization).getEntryPoint(classLoaderResolver, groupId, artifactId, version, packaging); getLog().info("Resolved entry point: " + entryPoint); List classFileLocators = new ArrayList(classPath.size()); 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); Plugin.Engine.Summary summary; try { getLog().info("Processing class files located in in: " + root); Plugin.Engine pluginEngine; try { String javaVersionString = findJavaVersionString(project); ClassFileVersion classFileVersion; if (javaVersionString == null) { classFileVersion = ClassFileVersion.ofThisVm(); getLog().warn("Could not locate Java target version, build is JDK dependant: " + classFileVersion.getMajorVersion()); } else { classFileVersion = ClassFileVersion.ofJavaVersionString(javaVersionString); getLog().debug("Java version detected: " + javaVersionString); } pluginEngine = Plugin.Engine.Default.of(entryPoint, classFileVersion, suffix == null || suffix.length() == 0 ? MethodNameTransformer.Suffixing.withRandomSuffix() : new MethodNameTransformer.Suffixing(suffix)); } catch (Throwable throwable) { throw new MojoExecutionException("Cannot create plugin engine", throwable); } try { summary = pluginEngine .with(extendedParsing ? Plugin.Engine.PoolStrategy.Default.EXTENDED : Plugin.Engine.PoolStrategy.Default.FAST) .with(classFileLocator) .with(new TransformationLogger(getLog())) .withErrorHandlers(Plugin.Engine.ErrorHandler.Enforcing.ALL_TYPES_RESOLVED, failOnLiveInitializer ? Plugin.Engine.ErrorHandler.Enforcing.NO_LIVE_INITIALIZERS : Plugin.Engine.Listener.NoOp.INSTANCE, failFast ? Plugin.Engine.ErrorHandler.Failing.FAIL_FAST : Plugin.Engine.Listener.NoOp.INSTANCE) .with(threads == 0 ? Plugin.Engine.Dispatcher.ForSerialTransformation.Factory.INSTANCE : new Plugin.Engine.Dispatcher.ForParallelTransformation.WithThrowawayExecutorService.Factory(threads)) .apply(new Plugin.Engine.Source.ForFolder(root), new Plugin.Engine.Target.ForFolder(root), factories); } catch (Throwable throwable) { throw new MojoExecutionException("Failed to transform class files in " + root, throwable); } } finally { classFileLocator.close(); } if (!summary.getFailed().isEmpty()) { throw new MojoExecutionException(summary.getFailed() + " type transformations have failed"); } else if (summary.getTransformed().isEmpty()) { getLog().warn("No types were transformed during plugin execution"); } else { getLog().info("Transformed " + summary.getTransformed().size() + " types"); } } finally { classLoaderResolver.close(); } } /** * Makes a best effort of locating the configured Java target version. * * @param project The relevant Maven project. * @return The Java version string of the configured build target version or {@code null} if no explicit configuration was detected. */ private static String findJavaVersionString(MavenProject project) { while (project != null) { String target = project.getProperties().getProperty("maven.compiler.target"); if (target != null) { return target; } for (org.apache.maven.model.Plugin plugin : CompoundList.of(project.getBuildPlugins(), project.getPluginManagement().getPlugins())) { if ("maven-compiler-plugin".equals(plugin.getArtifactId())) { if (plugin.getConfiguration() instanceof Xpp3Dom) { Xpp3Dom node = ((Xpp3Dom) plugin.getConfiguration()).getChild("target"); if (node != null) { return node.getValue(); } } } } project = project.getParent(); } return null; } /** * 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) public String outputDirectory; /** * The production class path. */ @Parameter(defaultValue = "${project.compileClasspathElements}", required = true, readonly = true) public List compileClasspathElements; @Override protected String getOutputDirectory() { return outputDirectory; } @Override protected List 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) public String testOutputDirectory; /** * The test class path. */ @Parameter(defaultValue = "${project.testClasspathElements}", required = true, readonly = true) public List testClasspathElements; @Override protected String getOutputDirectory() { return testOutputDirectory; } @Override protected List getClassPathElements() { return testClasspathElements; } } /** * A {@link BuildLogger} implementation for a Maven {@link Log}. */ protected static class MavenBuildLogger implements BuildLogger { /** * The logger to delegate to. */ private final Log log; /** * Creates a new Maven build logger. * * @param log The logger to delegate to. */ protected MavenBuildLogger(Log log) { this.log = log; } /** * {@inheritDoc} */ public boolean isDebugEnabled() { return log.isDebugEnabled(); } /** * {@inheritDoc} */ public void debug(String message) { log.debug(message); } /** * {@inheritDoc} */ public void debug(String message, Throwable throwable) { log.debug(message, throwable); } /** * {@inheritDoc} */ public boolean isInfoEnabled() { return log.isInfoEnabled(); } /** * {@inheritDoc} */ public void info(String message) { log.info(message); } /** * {@inheritDoc} */ public void info(String message, Throwable throwable) { log.info(message, throwable); } /** * {@inheritDoc} */ public boolean isWarnEnabled() { return log.isWarnEnabled(); } /** * {@inheritDoc} */ public void warn(String message) { log.warn(message); } /** * {@inheritDoc} */ public void warn(String message, Throwable throwable) { log.warn(message, throwable); } /** * {@inheritDoc} */ public boolean isErrorEnabled() { return log.isErrorEnabled(); } /** * {@inheritDoc} */ public void error(String message) { log.error(message); } /** * {@inheritDoc} */ public void error(String message, Throwable throwable) { log.error(message, throwable); } } /** * A {@link Plugin.Engine.Listener} that logs several relevant events during the build. */ protected static class TransformationLogger extends Plugin.Engine.Listener.Adapter { /** * The logger to delegate to. */ private final Log log; /** * Creates a new transformation logger. * * @param log The logger to delegate to. */ protected TransformationLogger(Log log) { this.log = log; } @Override public void onTransformation(TypeDescription typeDescription, List plugins) { log.debug("Transformed " + typeDescription + " using " + plugins); } @Override public void onError(TypeDescription typeDescription, Plugin plugin, Throwable throwable) { log.warn("Failed to transform " + typeDescription + " using " + plugin, throwable); } @Override public void onError(Map> throwables) { log.warn("Failed to transform " + throwables.size() + " types"); } @Override public void onError(Plugin plugin, Throwable throwable) { log.error("Failed to close " + plugin, throwable); } @Override public void onLiveInitializer(TypeDescription typeDescription, TypeDescription definingType) { log.debug("Discovered live initializer for " + definingType + " as a result of transforming " + typeDescription); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy