SourceBuddy dynamically compiles Java source code

Two months after the first commit in October 2022 Peter VerhasSenior Architect EPAM Systems, released version 2.0.0 sourcebuddya new utility that dynamically compiles Java source code, given in a string or file, into a class file.

SourceBuddy requires Java 17 and is a lightweight facade for the compiler javacwhich provides the same functionality.

Version 2.0.0 supports a combination of hidden and non-hidden classes at compile and run time. In addition, the API has been simplified, including breaking changes such as changing the method loadHidden() per method hidden(), hence the new major release. A complete overview of the changes for each version is available in the documentation for issues on GitHub.

SourceBuddy can be used after adding the following Maven dependency:

<dependency>
    <groupId>com.javax0.sourcebuddy</groupId>
    <artifactId>SourceBuddy</artifactId>
    <version>2.0.0</version>
</dependency>

Alternatively, you can use the following Gradle dependency:

implementation 'com.javax0.sourcebuddy:SourceBuddy:2.0.0'

To demonstrate SourceBuddy, consider the following interface example to be used by dynamically generated code:

package com.app;

public interface PrintInterface {
	void print();
}

A simple API is able to compile one class at a time using a static method com.javax0.sourcebuddy.Compiler.compile(). Here is an example to compile a new class that implements the previously mentioned interface PrintInterface:

String source = """
package com.app;

public class CustomClass implements PrintInterface {
    @Override
    public void print() {
        System.out.println("Hello world!");
    }
}""";
Class<?> clazz = Compiler.compile(source);
PrintInterface customClass = 
    (PrintInterface) clazz.getConstructor().newInstance();
customClass.print();

The Fluent API offers functions for more complex tasks such as compiling multiple files with a static method Compiler.java():

Compiler.java().from(source).compile().load().newInstance(PrintInterface.class);

You can optionally specify the binary name of the class, although SourceBuddy will already determine the name when possible:

.from("com.app", source)

For multiple source files, the method from() may be called multiple times, or all source files in a particular directory may be loaded at once:

.from(Paths.get("src/main/java/sourcefiles"))

If desired, the method hidden() can be used to create hidden classwhich cannot be used directly by other classes, only through reflection using an object ClassThe returned by SourceBuddy.

Method compile() generates bytecodes for Java source files, but doesn’t load them into memory yet.

final var byteCodes = Compiler.java()
    .from("com.app", source)
    .compile();

If desired, bytecodes can be stored on a local disk:

byteCodes.saveTo(Paths.get("./target/generated_classes"));

Alternatively, you can use the method stream()which returns a stream of byte arrays and can be used to get information such as the binary name:

byteCodes.stream().forEach(
    bytecode -> System.out.println(Compiler.getBinaryName(bytecode)));

Method byteCodes.load() loads classes and converts bytecode into objects of type Class:

final var loadedClasses = compiled.load();

A class can be accessed by casting to a superclass or an interface that implements the class, or by using the reflection API. Here is an example how to access the class CustomClass:

Class<?> customClass = loadedClasses.get("com.app.CustomClass");

Alternatively, you can use the method to instantiate a class newInstance():

Object customClassInstance = loadedClasses.newInstance("com.app.CustomClass");

The class stream can be used to get more information about classes:

loadedClasses.stream().forEach(
    clazz -> System.out.println(clazz.getSimpleName()));

More information about SourceBuddy can be found in the detailed explanations in the file README on GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *