Two months after the first commit in October 2022, Peter Verhas, senior architect at EPAM Systems, has released version 2.0.0 of SourceBuddy, a new utility that compiles dynamically created Java source code defined by a String
or a file to a class
file. SourceBuddy, requiring Java 17, is a simplified facade for the javac
compiler, which delivers the same functionality.
Version 2.0.0 supports a combination of hidden and non-hidden classes during compilation and run-time. Furthermore, the API has been simplified, containing breaking changes such as changing the loadHidden()
method to the hidden()
method, hence the new major release. A complete overview of the changes per version is available in the releases documentation 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 the following Gradle dependency may be used:
implementation 'com.javax0.sourcebuddy:SourceBuddy:2.0.0'
To demonstrate SourceBuddy, consider the following example interface which will be used by the dynamically created code:
package com.app;
public interface PrintInterface {
void print();
}
The simple API is able to compile one class at a time by using the static com.javax0.sourcebuddy.Compiler.compile()
method. For example, to compile a new class implementing the previously mentioned 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 features to solve more complex problems such as the compilation of multiple files with the Compiler.java()
static method:
Compiler.java().from(source).compile().load().newInstance(PrintInterface.class);
Optionally, the binary name of the class may be specified, although SourceBuddy will already detect the name whenever possible:
.from("com.app", source)
For multiple source files, the from()
method may be called multiple times, or all the source files in a specific directory may be loaded at once:
.from(Paths.get("src/main/java/sourcefiles"))
Optionally, the hidden()
method may be used to create a Hidden Class which can't be used directly by other classes, only through reflection by using the class object returned by SourceBuddy.
The compile()
method generates the byte codes for the Java source files, but doesn't load them into memory yet.
final var byteCodes = Compiler.java()
.from("com.app", source)
.compile();
Optionally, the byte codes may be saved to the local drive:
byteCodes.saveTo(Paths.get("./target/generated_classes"));
Alternatively, the stream()
method, which returns a stream of byte arrays, may be used to retrieve information such as the binary name:
byteCodes.stream().forEach(
bytecode -> System.out.println(Compiler.getBinaryName(bytecode)));
The byteCodes.load()
method loads the classes and converts the bytecode to Class
objects:
final var loadedClasses = compiled.load();
Accessing a class is possible by casting to a superclass or an interface the class implements or by using the reflection API. For example, to access the CustomClass
:
Class<?> customClass = loadedClasses.get("com.app.CustomClass");
Alternatively, the newInstance()
method may be used to create an instance of the class:
Object customClassInstance = loadedClasses.newInstance("com.app.CustomClass");
The stream of classes may be used to retrieve more information about the classes:
loadedClasses.stream().forEach(
clazz -> System.out.println(clazz.getSimpleName()));
More information about SourceBuddy can be found in the detailed explanations inside the README
file on GitHub.