Mark Chesney
Mark Chesney

Reputation: 1152

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

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.

Upvotes: 14

Views: 4496

Answers (4)

Arefe
Arefe

Reputation: 12431

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>

Upvotes: 0

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

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.

Upvotes: 4

Mark Chesney
Mark Chesney

Reputation: 1152

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>

Upvotes: 1

Egor Zhuk
Egor Zhuk

Reputation: 276

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

Upvotes: 1

Related Questions