raphw / byte-buddy

Runtime code generation for the Java virtual machine.
https://bytebuddy.net
Apache License 2.0
6.2k stars 796 forks source link

[maven plugin] transform-extended does not include dependency classes #1676

Closed LarsBodewig closed 3 days ago

LarsBodewig commented 1 month ago

I try to use the maven plugin goal transform-extended to transform classes from a dependency with a custom plugin. My POM looks like this:

        <dependency>
            <groupId>somedependency</groupId>
            <artifactId>somedependency</artifactId>
            <version>1.0.0</version>
        </dependency>
         ...
            <plugin>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy-maven-plugin</artifactId>
                <version>1.14.17</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>transform-extended</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <transformations>
                        <transformation>
                            <groupId>mygroup</groupId>
                            <artifactId>mypluginartifact</artifactId>
                            <version>myversion</version>
                            <plugin>myplugin</plugin>
                        </transformation>
                    </transformations>
                </configuration>
            </plugin>

(note: the custom plugin is in another maven module)

In my custom plugin I just print out the type names in the apply:

  public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription,
                                        ClassFileLocator classFileLocator) {
        System.out.println(typeDescription.getCanonicalName());
        return builder;
  }

However I only ever prints local classes without any dependency classes. What am I missing? transform-extended should include everything but test and import scope, right?

raphw commented 1 month ago

The documentation states: A Byte Buddy plugin that transforms a project's production class files where all scopes but the test scope are included. This means that the dependant classes will be resolved, and can be analysed during the build, for example as annotation properties. Any Maven module can however only transform the (test) classes that it defines. Dependencies cannot be instrumented during a build.

LarsBodewig commented 1 month ago

I found a related issue where you advise to use Plugin.Engine.Default to transform a library's jar file. How do I do that?

Does my custom plugin need to implement Plugin.Engine or do I to extend the ByteBuddy mojo to load the library jar and pass the classes as Source?

raphw commented 1 month ago

You can apply an engine to any folder or jar file, just as you can specify any folder or jar file as a destination. If you downloaded the jar during a build and transformed it, you might be able to substitute the dependency later. This is however related to how you run your app.

LarsBodewig commented 1 month ago

But to apply the engine to the jar file I need to use a custom mojo or can I configure the ByteBuddyMojo to do that? Downloading the jar during the build is no problem.

raphw commented 1 month ago

There is now, good point, this was missing.

Could you build Byte Buddy from master and see if that works for your real-life scenario? You would need to use the transform-location target and specify source and target. If you want to add other locations for class file resolution, you would need to specify locations as dependencies which will be resolved and added.

LarsBodewig commented 1 month ago

The goal fails with a PluginConfigurationException: Unable to parse configuration of mojo net.bytebuddy:byte-buddy-maven-plugin:1.14.19-SNAPSHOT:transform-location for parameter dependency: Cannot create instance of class net.bytebuddy.build.maven.MavenCoordinate

due to public List<MavenCoordinate> dependencies;.

I changed it to List<CoordinateConfiguration> and resolved it to an artifact in resolveClassPathElements().

                Map<Coordinate, String> coordinates = new HashMap<Coordinate, String>();
                if (project.getDependencyManagement() != null) {
                    for (Dependency dependency : project.getDependencyManagement().getDependencies()) {
                        coordinates.put(new Coordinate(dependency.getGroupId(), dependency.getArtifactId()), dependency.getVersion());
                    }
                }
                String managed = coordinates.get(new Coordinate(project.getGroupId(), project.getArtifactId()));
                for (CoordinateConfiguration dependency : dependencies) {
                    MavenCoordinate mavenCoordinate = dependency.asCoordinate(project.getGroupId(), project.getArtifactId(), managed == null ? project.getVersion() : managed, project.getPackaging());

I also had to change the apply method to pass the classpathElements as a compound source since the library jar was never transformed when I configured my source as ${project.build.sourceDirectory}

            List<Plugin.Engine.Source> resolved = new ArrayList<Plugin.Engine.Source>();
            List<String> combined = new ArrayList<String>(elements);
            combined.add(this.target);
            for (String element : combined) {
                File e = new File(element);
                if (e.isDirectory()) {
                    resolved.add(new Plugin.Engine.Source.ForFolder(e));
                } else if (e.exists()) {
                    resolved.add(new Plugin.Engine.Source.ForJarFile(e));
                } else {
                    throw new MojoFailureException("Source location does not exist: " + e);
                }
            }
            Plugin.Engine.Source compound = new Plugin.Engine.Source.Compound(resolved);

however I still have issues with missing classes from my compile classpath (located in a 3rd jar, not the one I pass in the configuration/that I try to transform). Since the transform goal works fine, I tried adding project.getCompileClasspathElements to the compound source as well but without any luck. Do you have any advise?

I also noticed, that you changed a line in ByteBuddyMojo.transform to .apply(source, target, factories); so I tried changing it back to .apply(source, new Plugin.Engine.Target.ForFolder(file), factories); (since you passed root before the change which became file) but could not see any difference.

Also the mvnw fails due to checksums, but since the README advises to use mvn to build, I guess that's fine.

raphw commented 1 month ago

Thanks for the first hint, I have fixed that on master.

As for the exception: You have to specify any dependencies as: <dependencies><dependency>...</dependency></dependencies> in the configuration block of the plugin. As you point to a location or jar file, the plugin does not know about any possible Maven coordinates that need to be included, so you would need to specify those manually. I just gave this a test myself and this seems to work.

LarsBodewig commented 1 month ago

Maybe I misunderstood. I figured the list of dependencies for the goal was a way to specify the libraries that should be additionally transformed, while I hoped other project dependencies would still be available on the classpath. I think I can work around it though, I will test it tomorrow.

raphw commented 1 month ago

I can add a flag to include the class path. You are right that it will make sense in many scenarios. As it is right now, you can transform any jar or folder, even outside the project, so it is a bit more generic. Also, a library will likely have less classes available than the project itself, so the scope can be reduced. But I will add a convenience option.

raphw commented 1 month ago

Can you try with the latest version? transform-location will now include the class path. transform-location-empty is now the previous behavior.

LarsBodewig commented 1 month ago

I tried transform-location and transform-location-extended but I probably still do something wrong.

                  <configuration>
                    <transformations>
                        <transformation>
                            <groupId>mygroup</groupId>
                            <artifactId>myplugin</artifactId>
                            <version>1.0.0</version>
                            <plugin>my.Plugin</plugin>
                        </transformation>
                    </transformations>
                    <source>${project.build.outputDirectory}</source>
                    <target>${project.build.outputDirectory}</target>
                    <dependencies>
                        <dependency>
                            <groupId>3rd.party</groupId>
                            <artifactId>library</artifactId>
                            <version>1.0.0</version>
                        </dependency>
                    </dependencies>
                </configuration>

If I set source to ${project.build.outputDirectory} my own classes are being transformed, but not the 3rd party library.

If I use the maven-dependency-plugin to download the 3rd party jar inbefore and set source to ${project.build.directory}/dependency it transforms neither the java classes nor the library (since that folder does not contain class files but jar files which are not detected).

I have to specifiy the full jar name to transform the 3rd party library, which will only ever work for one jar file (and likely skips the purpose of configuring the dependency separately).

<source>${project.build.directory}/dependency/library-1.0.0.jar</source>

Maybe it's not necessary to add 5 new mojos but instead just extend the existing mojos with List<CoordinateConfiguration> dependencies to specify additional libraries to be processed (additionally to the local classes, that the other mojos already process and without configuring a source)?

raphw commented 1 month ago

I am not sure what you are trying to accomplish, but I would copy your compiled classes and the dependencies to a custom location using: https://maven.apache.org/components/plugins/maven-dependency-plugin/examples/unpacking-project-dependencies.html

Then you can apply the Byte Buddy plugin on this exploded folder and pack the modified app in one app.

The reason I need to add that many Mojos is that Maven requires annotations to resolve a given dependency scope.

raphw commented 1 month ago

I added another target for this exact use case. Given the Maven API, I think this is the best approach. If you have a folder with all dependencies, using transform-dependencies should now do what you expect if that folder contains all relevant jar files.

Could you try this out?

LarsBodewig commented 1 month ago

I was able to achieve what I wanted by using two goals at once now:

I also tested out transform-dependencies to transform a folder of jar files, however with the maven-assembly-plugin it's easier to just unpack the dependencies immediately. This way I also skipped the need to specify the dependency in the plugin configuration so I can't say if that works as expected.

Overall, achieves what I'd like to use the plugin for :) I can also run my tests with gradle, if you plan on bringing this feature to the byte-buddy-gradle-plugin.

LarsBodewig commented 1 week ago

Thanks for releasing the new Maven Mojos in 1.14.19.

I briefly checked the changes to the gradle plugin in 1.15.0, I suppose they do not contain the equivalent of transform-location yet? If I can help out by setting up a test project, feel free to reach out.

raphw commented 1 week ago

I have a Jar plugin for Gradle. That one should work and since Gradle has a programmatic interface, it should be trivial to iterate over all files in a folder and apply it. Do you have a use case that is not covered? I'm happy to extend, if so.

LarsBodewig commented 3 days ago

Sorry, took me some time to get back into gradle.

The jar tasks works (after some trouble with gradle's buildscript classpath). Thanks again for putting in the work.