Core Java

Java compile in Java

In a previous post I wrote about how to generate a proxy during run-time and we got as far as having Java source code generated. However to use the class it has to be compiled and the generated byte code to be loaded into memory. That is “compile” time. Luckily since Java 1.6 we have access the Java compiler during run time and we can, thus mix up compile time into run time. Though that may lead a plethora of awful things generally resulting unmaintainable self modifying code in this very special case it may be useful: we can compile our run-time generated proxy.

Java compiler API

The Java compiler reads source files and generates class files. (Assembling them to JAR, WAR, EAR and other packages is the responsibility of a different tool.) The source files and class files do not necessarily need to be real operating system files residing in a magnetic disk, SSD or memory drive. After all Java is usually good about abstraction when it comes to the run-time API and this is the case now. These files are some “abstract” files you have to provide access to via an API that can be disk files but the same time they can be almost anything else. It would generally be a waste of resources to save the source code to disk just to let the compiler running in the same process to read it back and to do the same with the class files when they are ready.

The Java compiler as an API available in the run-time requires that you provide some simple API (or SPI of you like the term) to access the source code and also to send the generated byte code. In case we have the code in memory we can have the following code (from this file):

public Class<?> compile(String sourceCode, String canonicalClassName)
			throws Exception {
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		List<JavaSourceFromString> sources = new LinkedList<>();
		String className = calculateSimpleClassName(canonicalClassName);
		sources.add(new JavaSourceFromString(className, sourceCode));

		StringWriter sw = new StringWriter();
		MemoryJavaFileManager fm = new MemoryJavaFileManager(
				compiler.getStandardFileManager(null, null, null));
		JavaCompiler.CompilationTask task = compiler.getTask(sw, fm, null,
				null, null, sources);

		Boolean compilationWasSuccessful = task.call();
		if (compilationWasSuccessful) {
			ByteClassLoader byteClassLoader = new ByteClassLoader(new URL[0],
					classLoader, classesByteArraysMap(fm));

			Class<?> klass = byteClassLoader.loadClass(canonicalClassName);
			byteClassLoader.close();
			return klass;
		} else {
			compilerErrorOutput = sw.toString();
			return null;
		}
	}

The compiler instance is available through the ToolProvider and to create a compilation task we have to invoke getTask(). The code write the errors into a string via a string writer. The file manager (fm) is implemented in the same package and it simply stored the files as byte arrays in a map, where the keys are the “file names”. This is where the class loader will get the bytes later when the class(es) are loaded. The code does not provide any diagnistic listener (see the documentation of the java compiler in the RT), compiler options or classes to be processed by annotation processors. These are all nulls. The last argument is the list of source codes to compile. We compile only one single class in this tool, but since the compiler API is general and expects an iterable source we provide a list. Since there is another level of abstraction this list contains JavaSourceFromStrings.

To start the compilation the created task has to be “call”ed and if the compilation was successful the class is loaded from the generated byte array or arrays. Note that in case there is a nested or inner class inside the top level class we compile then the compiler will create several classes. This is the reason we have to maintain a whole map for the classes and not a single byte array even though we compile only one source class. If the compilation was not successful then the error output is stored in a field and can be queried.

The use of the class is very simple and you can find samples in the unit tests:

private String loadJavaSource(String name) throws IOException {
		InputStream is = this.getClass().getResourceAsStream(name);
		byte[] buf = new byte[3000];
		int len = is.read(buf);
		is.close();
		return new String(buf, 0, len, "utf-8");
	}
...
	@Test
	public void given_PerfectSourceCodeWithSubClasses_when_CallingCompiler_then_ProperClassIsReturned()
			throws Exception {
		final String source = loadJavaSource("Test3.java");
		Compiler compiler = new Compiler();
		Class<?> newClass = compiler.compile(source, "com.javax0.jscc.Test3");
		Object object = newClass.newInstance();
		Method f = newClass.getMethod("method");
		int i = (int) f.invoke(object, null);
		Assert.assertEquals(1, i);
	}

Note that the classes you create this way are only available to your code during run-time. You can create immutable versions of your objects for example. If you want to have classes that are available during compile time you should use annotation processor like scriapt.

Reference: Java compile in Java from our JCG partner Peter Verhas at the Java Deep blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
DaFab
DaFab
8 years ago

I can understand the need to generate on-the-fly code. But why doing it in such a complicated why when you have out-of-the-box solutions such as Groovy.

Peter Verhas
8 years ago
Reply to  DaFab

There can be several reasons why you would not like to have such a full blown language incorporated into your application. You may deliver a library and in that case the less dependency your library has the better. You may not want to increase the packaged application (be it war, ear or whatever). You may deliver an application in an enterprise environment where use of Groovy is not allowed by the policies in place (for just any reason).

Back to top button