Sourcebuddy
Java source code compiler facade for the Java JDK built-in Java compiler
Install / Use
/learn @sourcebuddy/SourcebuddyREADME
= SourceBuddy
== Introduction .This documentation is about the version 2.5.4-SNAPSHOT of the software.
SourceBuddy is a Java library you can use to compile and load dynamically generated Java source code in your program.
You can compile Java source code you created dynamically in your Java application. Your program can create the source code of one or more Java classes, pass the strings to SourceBuddy and use the classes. An example code is the following:
[source,java]
String source = """ package com.sb.demo;
public class MyClass implements Talker {
@Override // comment
public void say() {
System.out.println("Hello, Buddy!");
}
}""";
Class<?> myClassClass = Compiler.compile(source); Talker myClass = (Talker) myClassClass.getConstructor().newInstance(); myClass.say();
SourceBuddy is a simple Java source compiler facade in front of the JDK-provided javac compiler.
You need to follow the steps as depicted here:
image::images/compile_flow.svg[]
. get a Compiler object calling Compiler.java(), and from there, all you need is
. specify the source code either as Java strings or files,
. call compile(),
. fetch the compiled .class files as byte[] byte array, save to file, or the load the classes, and
. use the class and instances (not on the picture, it is already up to you).
There are two apis.
- A simple API with one call for simple cases compiling one class only, and
[source,java]
Compiler.compile(source).getConstructor().newInstance();
- a more versatile fluent API for cases being a bit more complex.
[source,java]
Compiler.java().from(source).compile().load().newInstance(Talker.class);
In this document, we will explain the details of how to use the library, including
-
Maven coordinates,
-
Simple API,
-
Fluent API,
-
Handling Hidden Classes,
-
Creating and Loading Inner Classes, and
-
Support.
== Maven Coordinates
The library is available from Maven central. The Maven central coordinates are:
[source,xml]
<dependency> <groupId>com.javax0.sourcebuddy</groupId> <artifactId>SourceBuddy</artifactId> <version>2.5.4-SNAPSHOT</version> </dependency> ----SourceBuddy requires Java 17.
The library is modularized starting with the release 2.5.2.
It means that you have to add a requires statement to your module-info.java file.
Documentation on the different link:RELEASES.adoc[releases].
== Simple API
You can use the simple API in simple projects compiling and loading one class at a time.
To do that, the class com.javax0.sourcebuddy.Compiler defines a static method compile().
Here is the line from the example displayed in the introduction using this method:
[soure,java]
Class<?> myClassClass = Compiler.compile(source);
The method's parameter is the string source code of the class.
The method's return value is the class compiled and loaded into the JVM.
If there is an error during the compilation, the call will throw a CompileException with the error message in the exception message.
There are four overloaded versions of the static compile() method.
Using the previous source code, it can be compiled in four different ways:
[source,java]
- Class<?> objectClassImplicitName = Compiler.compile(source);
- Class<?> objectClass = Compiler.compile(name, source);
- Class<Talker> classImplicitName = Compiler.compile(source, Talker.class);
- Class<Talker> klass = Compiler.compile(name, source, Talker.class);
. Providing only the source code as we have already seen before. . Providing the name of the class and the source code. This version should be used, when the library cannot figure out the name of the class from the source code. The library uses simple pattern matching to find the package and class names in the Java source. . The same as the first version, but you can also provide a class for the loaded type. This can be used when the class in the source code implements and interface or extends a class which is available during the compile time. The returned class object can be cast to that type and the library will do that casting for you. . The same as the second version, but again you can provide a class for the casting.
== Fluent API
The fluent API is available when there are more files to be compiled.
image::images/fluent-rail.svg[]
To demonstrate the use of the API, we will use the sample code:
[source,java]
- String sourceFirstClass = """
-
package com.sb.demo; -
public class FirstClass { -
public String a() { -
return "x"; -
} -
}"""; - final var compiled = Compiler.java()
-
.options("-g:none") -
.from("com.sb.demo.FirstClass", sourceFirstClass) -
.from(Paths.get("src/test/resources/src")) -
.compile(); - compiled.saveTo(Paths.get("./target/generated_classes"));
- compiled.stream().forEach(bc -> System.out.println(Compiler.getBinaryName(bc)));
- final var loaded = compiled.load();
- Class<?> firstClassClass = loaded.get("com.sb.demo.FirstClass");
- Object firstClassInstance = loaded.newInstance("com.sb.demo.FirstClass");
- loaded.stream().forEach(klass -> System.out.println(klass.getSimpleName()));
- final var compiler = loaded.reset();
- final var sameCompiler = compiled.reset();
In the following sections we wil go through the lines of the code explaining their meaning.
=== 1. Get the compiler object
To start the compilation, you must have a Compiler object.
To get that, you have to call the
.line 9. [source,java]
final var compiled = Compiler.java()
=== 2. Compiler Options
You can set compiler options calling the method options().
.line 10. [source,java]
.options("-g:none")
In the example we are setting the option -g:none.
You can use the same options as you would use when calling the javac compiler from the command line.
Use the strings as you would use them in the command line including the leading - for the option keywords and using separate arguments for the values separated by spaces on the command line.
In addition to the method options() there are convenience methods defined in the fluent API to set the most common options in a readable way.
These methods are
release(int)sets the release version of the Java compiler.source(int)sets the source version of the Java compiler.target(int)sets the target version of the Java compiler.encoding(Charset)sets the encoding of the source files.verbose()sets the compiler to be verbose.debugInfo(DebugInfo)sets the debug information level of the compiler. The possible values areNONE,LINES,SOURCE,VARS, andALLas listed in the enumeration.noDebugInfo()sets the compiler to suppress debug information.nowarn()sets the compiler to suppress warnings.showDeprecation()sets the compiler to show deprecation warnings.parameters()sets the compiler to store formal parameter names of constructors and methods in the generated class files.addExports(Export...)adds export directives to the module declaration. To create anExportobject, use the methods of the classExport. A typical usage is
[source,java]
addExports(Export.from("module").thePackage("package").to("otherModule"))
You can make a static import for the method from to make the code more readable.
addModules(String...)adds required modules to the module declaration.limitModules(String...)limits the modules that are visible during compilation.module(String)sets the module name of the compiled classes.
The line in the example calls the method options() directly.
Using the complimentary methods, we could have written the line as
[source,java]
.debugInfo(NONE);
or even
[source,java]
.noDebugInfo();
Adding options is not mandatory.
=== 3. Add sources
The next step is to add the source files to the compiler object.
To do that, you can specify the sources one by one as strings, or you can add directories where the source files are.
The overloaded method from() is used for both operations.
To add sources individually, you can call
.line 11. [source,java]
.from("com.sb.demo.FirstClass", sourceFirstClass)
The first argument is the binary name of the class. The second is the actual source code.
You can omit the class name. This information is already in the source code after all. The class name is required by the JDK compiler. SourceBuddy has to provide it. To do that, it either gets it as an argument or tries to figure out even before compiling the code. Use the one without the name, and specify the name only in special cases when SourceBuddy cannot identify it.
To add multiple sources, you can call this method multiple times.
If the sources are in the file system in a directory, you can also call
.line 12. [source,java]
.from(Paths.get("src/test/resources/src"))
In this call, you specify only one parameter. A path pointing to the source root. It is the directory where the directory structure matching the Java package structure starts. You can have many calls to this method if you have multiple source trees on the disk. You can also add some sources as strings, individually and others scanned from the file system.
.Class name calculation [NOTE]
The class names are calculated from the directory structure and the name of the file. The class name of a single class is calculated the same way as before when the path points to a single file. You can also provide the class name as string and a path to a single source file.
=== 4. Hide the class
You can call the method hidden() when you want to load a class hidden.
Hidden and non-hidden classes can be mixed in one SourceBuddy compiler object
