Memory Class Compiler (MCC)
The Memory Class Compiler (MCC) is a simple Java library used to compile Java classes at runtime. The project is also available here.
Purpose
The purpose of MCC is to provide a simple API to be used in applications that need to compile and execute Java source code on the fly, where the Java source code is represented as a String.
License
This software is available under MIT license.
Technical Overview
The software was written in Java and tested with the following versions of Java Development Toolkit (JDK): JDK 7, JDK 8 and JDK 9. There is only one requirement, the library needs the program to be running under a JDK because only JDK grants access to the compiler API.
The following features are available:
- A simple library that facilitates the access to JavaCompiler implementation of JDK.
- A simple library that facilitates the access to execute PMD validation in Java source code.
- A custom ClassLoader to load java source code compiled at runtime.
Getting Started
To start using MCC, first, embed the library into your Java application via the following snippet
<dependency>
<groupId>com.github.schmittjoaopedro</groupId>
<artifactId>mcc</artifactId>
<version>1.0.1</version>
</dependency>
Create a simple class
This is a simple example of compilation using MCC:
//Create a simple String with the Java source code
String sourceCode =
"package comp.test;" +
"public class Test {" +
" public String sayHello() {" +
" return \"Hello World!\";" +
" }" +
"}";
//Create a object to encapsulate the source code
SourceClass sourceClass = new SourceClass("comp.test", "Test", sourceCode);
//Compile the SourceClass object
MemoryClassCompiler compiler = new MemoryClassCompiler();
compiler.checkAndCompile(sourceClass);
//Create a class loader and define the compiled class
SourceClassLoader classLoader = new SourceClassLoader(getClass().getClassLoader());
Class loadedClass = classLoader.loadSourceClassLoader(sourceClass);
//Invoke the method with reflection
Object o = loadedClass.newInstance();
Method m = loadedClass.getDeclaredMethod("sayHello", null);
Assert.assertEquals(m.invoke(o, null), "Hello World!");
Executing batch compilation
This is an example compiling one or more Java classes with batch jobs:
//Create a source task to execute batch compilation
SourceTask sourceTask = new SourceTask();
//Creating some classes
for(int i = 0; i < 10; i++) {
String sourceCode =
"package comp.test;" +
"public class Test" + i + " {" +
" public String sayHello(Integer val) { " +
" return val + \" - Hello World!\";" +
" }" +
"}";
//Add the class to the source task
sourceTask.createSourceClass("comp.test", "Test" + i, sourceCode);
}
//Create a memory compiler and execute passing the source task
MemoryClassCompiler compiler = new MemoryClassCompiler();
compiler.checkAndCompile(sourceTask);
//Create a custom class loader
SourceClassLoader classLoader = new SourceClassLoader(getClass().getClassLoader());
for(int i = 0; i < 10; i++) {
//Invoker the classes using reflection
SourceClass sourceClass = sourceTask.getSourcesClass().get(i);
Class loadedClass = classLoader.loadSourceClassLoader(sourceClass);
Object o = loadedClass.newInstance();
Method m = loadedClass.getDeclaredMethod("sayHello", Integer.class);
Assert.assertEquals(m.invoke(o, i), i + " - Hello World!");
}
Managing errors
This is a simple example obtaining detailed information when errors are thrown by the compiler:
//Example of class with problem (returning int but declared as void)
SourceClass sourceClass = new SourceClass();
sourceClass.setPackageName("teste");
sourceClass.setClassName("Teste");
sourceClass.setSourceCode("package teste; public class Teste { public void t() { return 2; } }");
MemoryClassCompiler compiler = new MemoryClassCompiler();
try {
compiler.compile(sourceClass);
} catch (MemoryCompilerException ex) {
MessageCompiler message = ex.getMessageCompiler();
Assert.assertNull(sourceClass.getBytecode());
Assert.assertEquals(message.getMessage(), "Error in compilation of class");
Assert.assertEquals(message.getStatus(), MessageStatus.FAILED);
Assert.assertEquals(message.getDiagnostics().get(0).getCode(), "compiler.err.prob.found.req");
Assert.assertEquals(message.getDiagnostics().get(0).getColumnNumber(), 62);
Assert.assertEquals(message.getDiagnostics().get(0).getEndPosition(), 62);
Assert.assertEquals(message.getDiagnostics().get(0).getLineNumber(), 1);
Assert.assertEquals(message.getDiagnostics().get(0).getMessage(null), "incompatible types: unexpected return value");
Assert.assertEquals(message.getDiagnostics().get(0).getPosition(), 61);
Assert.assertEquals(message.getDiagnostics().get(0).getStartPosition(), 61);
Assert.assertEquals(message.getDiagnostics().get(0).getKind(), Diagnostic.Kind.ERROR);
}
Managing PMD validations
This is a simple example obtaining detailed information when PMD errors are thrown by the compiler:
SourceClass sourceClass = new SourceClass();
sourceClass.setPackageName("test");
sourceClass.setClassName("Test");
sourceClass.setSourceCode("package test; public class Test { private int t; }");
try {
new MemoryPMDValidator().check(sourceClass);
} catch(MemoryCompilerException ex) {
Assert.assertEquals(ex.getMessage(), "PMD_ERROR: PMD Validation failed\nTest : 1 : Avoid unused private fields such as 't'.\n");
}