DEV Community

Cover image for How to generate and compile sources at runtime in Java
Roberto Gentili for Burningwave

Posted on • Updated on

How to generate and compile sources at runtime in Java

With ClassFactory it is possible to generate classes at runtime, but how we can do if we only need to compile sources without loading them or just generate sources?

Here they come to our aid the source code generator components and the JavaMemoryCompiler of Burningwave Core. With source code generators we can generate source code and store it on the drive or compile it via the JavaMemoryCompiler.

To use all these components you should simply add the following to your projects dependencies:

Now we are going to generate and store the following class:

package source.generation.test;

import java.util.Arrays;
import java.util.List;

class GeneratedClass { 

    List<String> words; 

    GeneratedClass(String... words) {
        this.words = Arrays.asList(words); 
    } 

    public void print() {
        System.out.println("\n\t" + String.join(" ", words) + "\n"); 
    }

    public static void main(String[] args) {
        new GeneratedClass(args).print(); 
    } 

}
Enter fullscreen mode Exit fullscreen mode

... With the following source code generators:

package source.generation.test;

import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;

import org.burningwave.core.classes.ClassSourceGenerator;
import org.burningwave.core.classes.FunctionSourceGenerator;
import org.burningwave.core.classes.GenericSourceGenerator;
import org.burningwave.core.classes.TypeDeclarationSourceGenerator;
import org.burningwave.core.classes.UnitSourceGenerator;
import org.burningwave.core.classes.VariableSourceGenerator;

public class SourceGenerationTester {


    public static UnitSourceGenerator generate() {
        return UnitSourceGenerator.create(SourceGenerationTester.class.getPackage().getName())
        .addClass(
            ClassSourceGenerator.create(TypeDeclarationSourceGenerator.create("MyClass"))
            .addField(
                VariableSourceGenerator.create(
                    TypeDeclarationSourceGenerator.create(List.class)
                    .addGeneric(GenericSourceGenerator.create(String.class)),
                    "words"
                )
            )
            .addConstructor(
                FunctionSourceGenerator.create().addParameter(
                    VariableSourceGenerator.create(
                        TypeDeclarationSourceGenerator.create(String.class)
                        .setAsVarArgs(true),
                        "words"
                    )
                ).addBodyCodeLine("this.words = Arrays.asList(words);").useType(Arrays.class)
            )
            .addMethod(
                FunctionSourceGenerator.create("print")
                .addModifier(Modifier.PUBLIC).setReturnType(void.class)
                .addBodyCodeLine(
                  "System.out.println(\"\\n\\t\" + String.join(\" \", words) + \"\\n\");"
                )
            )
            .addMethod(
                FunctionSourceGenerator.create("main")
                .addModifier(Modifier.PUBLIC | Modifier.STATIC)
                .setReturnType(void.class)
                .addParameter(VariableSourceGenerator.create(String[].class, "args"))
                .addBodyCodeLine("new MyClass(args).print();")
            )
        );
    }


    public static void main(String[] args) {
        UnitSourceGenerator unitSG = SourceGenerationTester.generate();
        unitSG.storeToClassPath(System.getProperty("user.home") + "/Desktop/sources");
        System.out.println("\nGenerated code:\n" + unitSG);

    }
}
Enter fullscreen mode Exit fullscreen mode

And now let's try to compile the sources and store the compiled files with the JavaMemoryCompiler:

package source.compilation.test;


import org.burningwave.core.assembler.ComponentContainer;
import org.burningwave.core.classes.JavaMemoryCompiler;
import org.burningwave.core.classes.JavaMemoryCompiler.Compilation;
import org.burningwave.core.concurrent.QueuedTasksExecutor.ProducerTask;
import org.burningwave.core.io.FileSystemItem;

import source.generation.test.SourceGenerationTester;

public class SourceCompilationTester {


    public static void main(String[] args) throws ClassNotFoundException {
        ComponentContainer componentContainer = ComponentContainer.getInstance();
        JavaMemoryCompiler javaMemoryCompiler = componentContainer.getJavaMemoryCompiler();
        ProducerTask<Compilation.Result> compilationTask = javaMemoryCompiler.compile(
            Compilation.Config.forUnitSourceGenerator(
                SourceGenerationTester.generate()
            )
            .storeCompiledClassesTo(
                System.getProperty("user.home") + "/Desktop/classes"
            )
        );

        Compilation.Result compilationResult = compilationTask.join();

        System.out.println("\n\tAbsolute path of compiled file: " + 
            compilationResult.getClassPath()
            .findFirstInAllChildren(
                FileSystemItem.Criteria.forAllFileThat(FileSystemItem::isFile)
            ).getAbsolutePath() + "\n"
        );
    }

}
Enter fullscreen mode Exit fullscreen mode

And now let's try to load the compiled file:

package source.compilation.test;

import static org.burningwave.core.assembler.StaticComponentContainer.ClassLoaders;
import static org.burningwave.core.assembler.StaticComponentContainer.Methods;

import org.burningwave.core.assembler.ComponentContainer;
import org.burningwave.core.classes.JavaMemoryCompiler;
import org.burningwave.core.classes.JavaMemoryCompiler.Compilation;
import org.burningwave.core.concurrent.QueuedTasksExecutor.ProducerTask;

import source.generation.test.SourceGenerationTester;

public class SourceCompilationTester {


    public static void main(String[] args) throws ClassNotFoundException {
        ComponentContainer componentContainer = ComponentContainer.getInstance();
        JavaMemoryCompiler javaMemoryCompiler = componentContainer.getJavaMemoryCompiler();
        ProducerTask<Compilation.Result> compilationTask = javaMemoryCompiler.compile(
            Compilation.Config.forUnitSourceGenerator(
                SourceGenerationTester.generate()
            )
            .storeCompiledClassesTo(
                System.getProperty("user.home") + "/Desktop/classes"
            )
        );

        Compilation.Result compilattionResult = compilationTask.join();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ClassLoaders.addClassPaths(classLoader, compilattionResult.getDependencies());
        ClassLoaders.addClassPath(classLoader, compilattionResult.getClassPath().getAbsolutePath());
        Class<?> cls = classLoader.loadClass("source.generation.test.GeneratedClass");
        Methods.invokeStaticDirect(cls, "main", new Object[] {new String[] {"Hello", "world!"}});
    }

}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial we learned how to generate and compile sources and how to load the generated class files, you can do a lot of things which are not mentioned here like, for example, compile sources that references classes located outside the runtime class paths and if you are curious or need further help you can open a discussion on GitHub.

Top comments (0)