Can I dynamically generate and reference a class in a source file in the same Maven project?
Asked Answered
I

4

14

In a Maven Build, I am dynamically generating some Java types using a byte code generation library (Byte Buddy). Naturally, these class files don't have corresponding source files. Only a few classes would be generated this way. The majority of the code for this project is going to be Java source. Ideally, the Java source would reference the generated types in a static way, rather than using reflection or runtime code generation, which means the classes need to be on the the compile class path for javac. Can I get the generated classes on the compile class path for the same Maven project i.e. without having a separate Maven project and artifact to hold the generated byte code referenced by the Maven project containing the source code?

UPDATE: I have already tried putting the generated classes directly into target/classes i.e. project.build.outputDirectory, early in the Maven Build Lifecycle, but it seems that is not on the class path. The generated types couldn't be resolved by the Maven Compiler Plugin or the IDE. I also tried using the Build Helper Maven Plugin to add an extra resources directory under target containing the generated classes, which were then automatically copied into target/classes. This configuration exhibited the same problem.

UPDATE: I have created a complete public repo on GitHub: https://github.com/mches/so-42376851

Here is the Maven POM for the project I want to have static classes that referenced byte code enhanced classes:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>demo</groupId>
        <artifactId>demo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>demo-enhanced</artifactId>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>unpack</goal>
                        </goals>
                        <configuration>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>demo</groupId>
                                    <artifactId>demo-original</artifactId>
                                    <version>${project.version}</version>
                                    <overWrite>true</overWrite>
                                    <outputDirectory>${project.build.outputDirectory}</outputDirectory>
                                    <includes>**/*.class</includes>
                                    <excludes>META-INF/</excludes>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>transform</goal>
                        </goals>
                        <configuration>
                            <initialization>
                                <entryPoint>net.bytebuddy.test.SimpleEntryPoint</entryPoint>
                                <groupId>demo</groupId>
                                <artifactId>demo-transformer</artifactId>
                            </initialization>
                            <transformations>
                                <transformation>
                                    <plugin>net.bytebuddy.test.SimplePlugin</plugin>
                                    <groupId>demo</groupId>
                                    <artifactId>demo-transformer</artifactId>
                                </transformation>
                            </transformations>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

and here is the Parent POM:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>demo</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>demo-original</module>
        <module>demo-transformer</module>
        <module>demo-enhanced</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <byte-buddy.version>1.6.9</byte-buddy.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy</artifactId>
                <version>${byte-buddy.version}</version>
            </dependency>
            <dependency>
                <groupId>demo</groupId>
                <artifactId>demo-original</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>demo</groupId>
                <artifactId>demo-transformer</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>demo</groupId>
                <artifactId>demo-enhanced</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>net.bytebuddy</groupId>
                    <artifactId>byte-buddy-maven-plugin</artifactId>
                    <version>${byte-buddy.version}</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

foo/Bar.java (original source):

package foo;

public class Bar {
}

net/bytebuddy/test/SimplePlugin.java (byte code enhancer):

package net.bytebuddy.test;

import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.modifier.Visibility;


import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;

public class SimplePlugin implements Plugin {
    @Override
    public boolean matches(TypeDescription target) {
        return target.getName().equals("foo.Bar");
    }

    @Override
    public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription) {
        if (typeDescription.getTypeName().equals("foo.Bar")) {
            builder = builder.defineField("qux", String.class, Visibility.PRIVATE)
                    .defineMethod("getQux", String.class, Visibility.PUBLIC)
                    .intercept(FieldAccessor.ofField("qux"))
                    .defineMethod("setQux", void.class, Visibility.PUBLIC)
                    .withParameter(String.class)
                    .intercept(FieldAccessor.ofField("qux"));
        }
        return builder;
    }
}

foo/Baz.java (static source file referencing dynamic type):

package foo;

public class Baz {
    private Bar bar = new Bar();

    public String getQuux() {
        return bar.getQux();
    }

    public void setQuux(String quux) {
        bar.setQux(quux);
    }
}

UPDATE: Maven seems to understand a structure involving a consolidated module with the enhanced byte code and static class source code, as well as separate modules for each, but the IDEs, IntelliJ and Eclipse, fail to understand the class path for either structure the way Maven does.

Immensurable answered 21/2, 2017 at 20:2 Comment(1)
Maybe you can take a look source code of jax-b:xjc it does similar thing on generate-sources phaseNickens
I
4

With Byte Buddy, you are generating class files and not source files. Unfortunately, Maven only knows about generated sources, but not about generated class files. The easiest is therefore to create a specific module.

If this is not possible, you can however copy them to any source folder of your Maven project such as resources. Some IDEs find those classes and treat them just if you had them in your java folder but without attempting to compile them as the files end with .class.

Improvise answered 22/2, 2017 at 21:48 Comment(1)
It seems that Maven understands this structure, but the IDEs, IntelliJ and Eclipse, fail to understand it. I found that creating an additional module, to place the static classes, which depends on the module with the enhanced classes, to my surprise, didn't improve the IDEs' ability to understand the class path the way Maven understands it.Immensurable
S
1

Yes you can. Generate your classes on a pre-compile phase and put it in a target/classes folder.

Stagnate answered 21/2, 2017 at 20:31 Comment(4)
I had tried putting the classes there, early in the Maven Build Lifecycle, but it seems that target/classes (project.build.outputDirectory) is not on the class path. The generated types couldn't be resolved by the Maven Compiler Plugin or the IDE.Immensurable
@Mark They are on the classpath. At what phase did you generate them into target/classes? Are you sure they are generated with the correct package structure? Did you see then being generated in the logs, or in that folder?Nanna
@Nanna I extract unenhanced classes to target/classes in the generate-resources phase. I enhance the classes in the process-resources phase. And you are right. Running Maven with --debug revealed the classpathElements includes target/classes. With my current configuration, I'm able to get Maven to compile the static class that references the generated type that exists in target/classes and I can see that the class is correct, but IDEs (IntelliJ and Eclipse) don't seem to recognize the generated class and report source-level errors (Cannot resolve symbol) on the static type.Immensurable
@Mark Ah, so it works with Maven, but the IDEs are getting confused. With Eclipse, it sounds like you should have the proper M2E connector for bytebuddy (not sure if it exists, make sure M2Eclipse is up to date also). For IntelliJ, no idea, sorry.Nanna
I
1

It's not my ideal solution, but an idea that seems to work is making two compromises:

  1. Use separate Maven modules for the enhanced byte code and the static classes.
  2. Don't build the enhanced byte code module in the same Maven reactor build as the the static classes module.

As to why this seems to work:

When the IDEs generate their project/module configuration, based on the Maven pom.xml files, they'll bring in the dependency on the enhanced byte code as a library/JAR rather than a project/module. The JAR ends up on the IDE's class path for the static classes module and the enhanced classes are resolved.

IntelliJ project file for enhanced byte code module:

<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
    <output url="file://$MODULE_DIR$/target/classes" />
    <output-test url="file://$MODULE_DIR$/target/test-classes" />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
      <excludeFolder url="file://$MODULE_DIR$/target" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
</module>

IntelliJ project file with Maven modules in separate builds (errors):

<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
    <output url="file://$MODULE_DIR$/target/classes" />
    <output-test url="file://$MODULE_DIR$/target/test-classes" />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
      <excludeFolder url="file://$MODULE_DIR$/target" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="module" module-name="demo-enhanced" />
  </component>
</module>

IntelliJ project file with Maven modules in the same build (no errors):

<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
    <output url="file://$MODULE_DIR$/target/classes" />
    <output-test url="file://$MODULE_DIR$/target/test-classes" />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
      <excludeFolder url="file://$MODULE_DIR$/target" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="library" name="Maven: demo:demo-enhanced:1.0-SNAPSHOT" level="project" />
  </component>
</module>
Immensurable answered 23/2, 2017 at 0:1 Comment(0)
C
0

This plugin will convert the .class files to the Java source code which you can add to the pom.xml file:

          <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>add-source</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>add-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>${project.basedir}/target/generated-sources/xjb/brink/ordering</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
Crossover answered 20/4, 2020 at 6:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.