-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
jOOQ#100: Prototype for compiling multiple classes in one go.
- Loading branch information
Showing
4 changed files
with
476 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.joor; | ||
|
||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
|
||
public class CompilationUnit { | ||
|
||
private Map<String, String> files = new LinkedHashMap<>(); | ||
|
||
public static class Result { | ||
private final Map<String, Class<?>> classes = new LinkedHashMap<>(); | ||
|
||
public void addResult(String className, Class<?> clazz) { | ||
classes.put(className, clazz); | ||
} | ||
|
||
public Class<?> getClass(String className) { | ||
return classes.get(className); | ||
} | ||
|
||
public int size() { | ||
return classes.size(); | ||
} | ||
|
||
} | ||
|
||
public static CompilationUnit create() { | ||
return new CompilationUnit(); | ||
} | ||
|
||
public static CompilationUnit.Result result() { | ||
return new Result(); | ||
} | ||
|
||
public CompilationUnit unit(String className, String content) { | ||
files.put(className, content); | ||
return this; | ||
} | ||
|
||
public Map<String, String> getFiles() { | ||
return files; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,340 @@ | ||
/* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.joor; | ||
|
||
/* [java-8] */ | ||
|
||
import javax.tools.FileObject; | ||
import javax.tools.ForwardingJavaFileManager; | ||
import javax.tools.JavaCompiler; | ||
import javax.tools.JavaCompiler.CompilationTask; | ||
import javax.tools.SimpleJavaFileObject; | ||
import javax.tools.StandardJavaFileManager; | ||
import javax.tools.ToolProvider; | ||
import java.io.ByteArrayOutputStream; | ||
import java.io.File; | ||
import java.io.OutputStream; | ||
import java.io.StringWriter; | ||
import java.lang.invoke.MethodHandles; | ||
import java.lang.invoke.MethodHandles.Lookup; | ||
import java.net.URI; | ||
import java.net.URL; | ||
import java.net.URLClassLoader; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayDeque; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Deque; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Map.Entry; | ||
import java.util.stream.Stream; | ||
|
||
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; | ||
|
||
/** | ||
* A utility that simplifies in-memory compilation of multiple new classes in the same unit. | ||
* | ||
* @author Lukas Eder | ||
*/ | ||
public class CompileUnit { | ||
|
||
public static CompilationUnit.Result compileUnit(CompilationUnit unit, CompileOptions compileOptions) { | ||
CompilationUnit.Result result = CompilationUnit.result(); | ||
|
||
// some classes may already be compiled so try to load them first | ||
List<CharSequenceJavaFileObject> files = new ArrayList<>(); | ||
|
||
Lookup lookup = MethodHandles.lookup(); | ||
ClassLoader cl = lookup.lookupClass().getClassLoader(); | ||
unit.getFiles().forEach((cn, code) -> { | ||
try { | ||
Class<?> clazz = cl.loadClass(cn); | ||
result.addResult(cn, clazz); | ||
} | ||
catch (ClassNotFoundException ignore) { | ||
files.add(new CharSequenceJavaFileObject(cn, code)); | ||
} | ||
}); | ||
|
||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); | ||
|
||
try { | ||
ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null)); | ||
StringWriter out = new StringWriter(); | ||
|
||
List<String> options = new ArrayList<>(compileOptions.options); | ||
if (!options.contains("-classpath")) { | ||
StringBuilder classpath = new StringBuilder(); | ||
String separator = System.getProperty("path.separator"); | ||
String cp = System.getProperty("java.class.path"); | ||
String mp = System.getProperty("jdk.module.path"); | ||
|
||
if (cp != null && !"".equals(cp)) | ||
classpath.append(cp); | ||
if (mp != null && !"".equals(mp)) | ||
classpath.append(mp); | ||
|
||
if (cl instanceof URLClassLoader) { | ||
for (URL url : ((URLClassLoader) cl).getURLs()) { | ||
if (classpath.length() > 0) | ||
classpath.append(separator); | ||
|
||
if ("file".equals(url.getProtocol())) | ||
classpath.append(new File(url.toURI())); | ||
} | ||
} | ||
|
||
options.addAll(Arrays.asList("-classpath", classpath.toString())); | ||
} | ||
|
||
CompilationTask task = compiler.getTask(out, fileManager, null, options, null, files); | ||
|
||
if (!compileOptions.processors.isEmpty()) | ||
task.setProcessors(compileOptions.processors); | ||
|
||
task.call(); | ||
|
||
if (fileManager.isEmpty()) | ||
throw new ReflectException("Compilation error: " + out); | ||
|
||
// This works if we have private-access to the interfaces in the class hierarchy | ||
if (Reflect.CACHED_LOOKUP_CONSTRUCTOR != null) { | ||
for (CharSequenceJavaFileObject f : files) { | ||
String className = f.getClassName(); | ||
Class<?> clazz = fileManager.loadAndReturnMainClass(className, | ||
(name, bytes) -> Reflect.on(cl).call("defineClass", name, bytes, 0, bytes.length).get()); | ||
if (clazz != null) { | ||
result.addResult(className, clazz); | ||
} | ||
} | ||
/* [java-9] */ | ||
|
||
// Lookup.defineClass() has only been introduced in Java 9. It is | ||
// required to get private-access to interfaces in the class hierarchy | ||
} else { | ||
|
||
// This method is called by client code from two levels up the current stack frame | ||
// We need a private-access lookup from the class in that stack frame in order to get | ||
// private-access to any local interfaces at that location. | ||
// Class<?> caller = StackWalker | ||
// .getInstance(RETAIN_CLASS_REFERENCE) | ||
// .walk(s -> s | ||
// .skip(2) | ||
// .findFirst() | ||
// .get() | ||
// .getDeclaringClass()); | ||
|
||
int index = 2; | ||
for (CharSequenceJavaFileObject f : files) { | ||
String className = f.getClassName(); | ||
|
||
Class<?> caller = getClassFromIndex(index); | ||
index++; | ||
|
||
// If the compiled class is in the same package as the caller class, then | ||
// we can use the private-access Lookup of the caller class | ||
if (className.startsWith(caller.getPackageName() + ".") && | ||
|
||
// [#74] This heuristic is necessary to prevent classes in subpackages of the caller to be loaded | ||
// this way, as subpackages cannot access private content in super packages. | ||
// The heuristic will work only with classes that follow standard naming conventions. | ||
// A better implementation is difficult at this point. | ||
Character.isUpperCase(className.charAt(caller.getPackageName().length() + 1))) { | ||
Lookup privateLookup = MethodHandles.privateLookupIn(caller, lookup); | ||
Class<?> clazz = fileManager.loadAndReturnMainClass(className, | ||
(name, bytes) -> privateLookup.defineClass(bytes)); | ||
if (clazz != null) { | ||
result.addResult(className, clazz); | ||
} | ||
} | ||
|
||
// Otherwise, use an arbitrary class loader. This approach doesn't allow for | ||
// loading private-access interfaces in the compiled class's type hierarchy | ||
else { | ||
Compile.ByteArrayClassLoader c = new Compile.ByteArrayClassLoader(fileManager.classes()); | ||
Class<?> clazz = fileManager.loadAndReturnMainClass(className, | ||
(name, bytes) -> c.loadClass(name)); | ||
if (clazz != null) { | ||
result.addResult(className, clazz); | ||
} | ||
} | ||
} | ||
} | ||
/* [/java-9] */ | ||
|
||
return result; | ||
} catch (ReflectException e) { | ||
throw e; | ||
} catch (Exception e) { | ||
throw new ReflectException("Error while compiling unit " + unit, e); | ||
} | ||
} | ||
|
||
private static Class<?> getClassFromIndex(int index) { | ||
return StackWalker | ||
.getInstance(RETAIN_CLASS_REFERENCE) | ||
.walk(s -> s | ||
.skip(index) | ||
.findFirst() | ||
.get() | ||
.getDeclaringClass()); | ||
} | ||
|
||
/* [java-9] */ | ||
static final class ByteArrayClassLoader extends ClassLoader { | ||
private final Map<String, byte[]> classes; | ||
|
||
ByteArrayClassLoader(Map<String, byte[]> classes) { | ||
super(ByteArrayClassLoader.class.getClassLoader()); | ||
|
||
this.classes = classes; | ||
} | ||
|
||
@Override | ||
protected Class<?> findClass(String name) throws ClassNotFoundException { | ||
byte[] bytes = classes.get(name); | ||
|
||
if (bytes == null) | ||
return super.findClass(name); | ||
else | ||
return defineClass(name, bytes, 0, bytes.length); | ||
} | ||
} | ||
/* [/java-9] */ | ||
|
||
static final class JavaFileObject extends SimpleJavaFileObject { | ||
final ByteArrayOutputStream os = new ByteArrayOutputStream(); | ||
|
||
JavaFileObject(String name, Kind kind) { | ||
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind); | ||
} | ||
|
||
byte[] getBytes() { | ||
return os.toByteArray(); | ||
} | ||
|
||
@Override | ||
public OutputStream openOutputStream() { | ||
return os; | ||
} | ||
|
||
@Override | ||
public CharSequence getCharContent(boolean ignoreEncodingErrors) { | ||
return new String(os.toByteArray(), StandardCharsets.UTF_8); | ||
} | ||
} | ||
|
||
static final class ClassFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> { | ||
private final Map<String, JavaFileObject> fileObjectMap; | ||
private Map<String, byte[]> classes; | ||
|
||
ClassFileManager(StandardJavaFileManager standardManager) { | ||
super(standardManager); | ||
|
||
fileObjectMap = new LinkedHashMap<>(); | ||
} | ||
|
||
@Override | ||
public JavaFileObject getJavaFileForOutput( | ||
Location location, | ||
String className, | ||
JavaFileObject.Kind kind, | ||
FileObject sibling | ||
) { | ||
JavaFileObject result = new JavaFileObject(className, kind); | ||
fileObjectMap.put(className, result); | ||
return result; | ||
} | ||
|
||
boolean isEmpty() { | ||
return fileObjectMap.isEmpty(); | ||
} | ||
|
||
Map<String, byte[]> classes() { | ||
if (classes == null) { | ||
classes = new LinkedHashMap<>(); | ||
|
||
for (Entry<String, JavaFileObject> entry : fileObjectMap.entrySet()) | ||
classes.put(entry.getKey(), entry.getValue().getBytes()); | ||
} | ||
|
||
return classes; | ||
} | ||
|
||
Class<?> loadAndReturnMainClass(String mainClassName, ThrowingBiFunction<String, byte[], Class<?>> definer) throws Exception { | ||
Class<?> result = null; | ||
|
||
// [#117] We don't know the subclass hierarchy of the top level | ||
// classes in the compilation unit, and we can't find out | ||
// without either: | ||
// | ||
// - class loading them (which fails due to NoClassDefFoundError) | ||
// - using a library like ASM (which is a big and painful dependency) | ||
// | ||
// Simple workaround: try until it works, in O(n^2), where n | ||
// can be reasonably expected to be small. | ||
Deque<Entry<String, byte[]>> queue = new ArrayDeque<>(classes().entrySet()); | ||
int n1 = queue.size(); | ||
|
||
// Try at most n times | ||
for (int i1 = 0; i1 < n1 && !queue.isEmpty(); i1++) { | ||
int n2 = queue.size(); | ||
|
||
for (int i2 = 0; i2 < n2; i2++) { | ||
Entry<String, byte[]> entry = queue.pop(); | ||
|
||
try { | ||
Class<?> c = definer.apply(entry.getKey(), entry.getValue()); | ||
|
||
if (mainClassName.equals(entry.getKey())) | ||
result = c; | ||
} catch (ReflectException e) { | ||
queue.offer(entry); | ||
} | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} | ||
|
||
@FunctionalInterface | ||
interface ThrowingBiFunction<T, U, R> { | ||
R apply(T t, U u) throws Exception; | ||
} | ||
|
||
static final class CharSequenceJavaFileObject extends SimpleJavaFileObject { | ||
final CharSequence content; | ||
final String className; | ||
|
||
public CharSequenceJavaFileObject(String className, CharSequence content) { | ||
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); | ||
this.className = className; | ||
this.content = content; | ||
} | ||
|
||
public String getClassName() { | ||
return className; | ||
} | ||
|
||
@Override | ||
public CharSequence getCharContent(boolean ignoreEncodingErrors) { | ||
return content; | ||
} | ||
} | ||
} | ||
/* [/java-8] */ |
Oops, something went wrong.