-
Notifications
You must be signed in to change notification settings - Fork 147
Expand file tree
/
Copy pathCachedCompiler.java
More file actions
338 lines (310 loc) · 14 KB
/
CachedCompiler.java
File metadata and controls
338 lines (310 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
/*
* Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.compiler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import static net.openhft.compiler.CompilerUtils.*;
/**
* Manages in-memory compilation with an optional cache. When directories are
* supplied to the constructors, source and class files are also written to
* disk to aid debugging. Call {@link #close()} once finished and use
* {@link #updateFileManagerForClassLoader(ClassLoader, java.util.function.Consumer)}
* to tune a specific loader.
*/
public class CachedCompiler implements Closeable {
/**
* Logger for compilation activity.
*/
private static final Logger LOG = LoggerFactory.getLogger(CachedCompiler.class);
/**
* Writer used when no alternative is supplied.
*/
private static final PrintWriter DEFAULT_WRITER = createDefaultWriter();
/**
* Default compiler flags including debug symbols.
*/
private static final List<String> DEFAULT_OPTIONS = Arrays.asList("-g", "-nowarn");
private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("[\\p{Alnum}_$.\\-]+");
private static final Pattern CLASS_NAME_SEGMENT_PATTERN = Pattern.compile("[\\p{Alnum}_$]+(?:-[\\p{Alnum}_$]+)*");
private final Map<ClassLoader, Map<String, Class<?>>> loadedClassesMap = Collections.synchronizedMap(new WeakHashMap<>());
private final Map<ClassLoader, MyJavaFileManager> fileManagerMap = Collections.synchronizedMap(new WeakHashMap<>());
/**
* Optional testing hook to replace the file manager implementation.
* <p>
* This field remains {@code public} to preserve binary compatibility with callers that
* accessed it directly in previous releases. Prefer {@link #setFileManagerOverride(Function)}
* for source-compatible code.
*/
@SuppressWarnings("WeakerAccess")
public volatile Function<StandardJavaFileManager, MyJavaFileManager> fileManagerOverride;
@Nullable
private final File sourceDir;
@Nullable
private final File classDir;
@NotNull
private final List<String> options;
private final ConcurrentMap<String, JavaFileObject> javaFileObjects = new ConcurrentHashMap<>();
/**
* Create a compiler that optionally writes sources and classes to the given
* directories. When {@code sourceDir} or {@code classDir} is not null, the
* corresponding files are written for debugging purposes.
*/
public CachedCompiler(@Nullable File sourceDir, @Nullable File classDir) {
this(sourceDir, classDir, DEFAULT_OPTIONS);
}
/**
* Create a compiler with explicit compiler options. Directories behave as in
* {@link #CachedCompiler(File, File)} and allow inspection of generated
* output.
*
* @param sourceDir where sources are dumped when not null
* @param classDir where class files are dumped when not null
* @param options additional flags passed to the Java compiler
*/
public CachedCompiler(@Nullable File sourceDir,
@Nullable File classDir,
@NotNull List<String> options) {
this.sourceDir = sourceDir;
this.classDir = classDir;
this.options = Collections.unmodifiableList(new ArrayList<>(options));
}
/**
* Close any file managers created by this compiler.
* Normally called when the instance is discarded.
*/
public void close() {
try {
for (MyJavaFileManager fileManager : fileManagerMap.values()) {
fileManager.close();
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Compile the supplied source and load the class using this instance's
* class loader. Successfully compiled classes are cached for reuse.
*
* @param className expected binary name of the class
* @param javaCode source code to compile
* @return the loaded class instance
* @throws ClassNotFoundException if the compiled class cannot be defined
*/
public Class<?> loadFromJava(@NotNull String className, @NotNull String javaCode) throws ClassNotFoundException {
validateClassName(className);
return loadFromJava(getClass().getClassLoader(), className, javaCode, DEFAULT_WRITER);
}
/**
* Compile the source using the supplied class loader. Cached classes are
* stored per loader key.
*
* @param classLoader loader to define the class with
* @param className expected binary name
* @param javaCode source code to compile
* @return the loaded class instance
* @throws ClassNotFoundException if definition fails
*/
public Class<?> loadFromJava(@NotNull ClassLoader classLoader,
@NotNull String className,
@NotNull String javaCode) throws ClassNotFoundException {
validateClassName(className);
return loadFromJava(classLoader, className, javaCode, DEFAULT_WRITER);
}
/**
* Compile source code into byte arrays using the provided file manager.
* Results are cached and reused on subsequent calls when compilation
* succeeds.
*
* @param className name of the primary class
* @param javaCode source to compile
* @param fileManager manager responsible for storing the compiled output
* @return map of class names to compiled bytecode
*/
@NotNull
Map<String, byte[]> compileFromJava(@NotNull String className,
@NotNull String javaCode,
MyJavaFileManager fileManager) {
validateClassName(className);
return compileFromJava(className, javaCode, DEFAULT_WRITER, fileManager);
}
/**
* Compile source using the given writer and file manager. The resulting
* byte arrays are cached for the life of this compiler instance.
*
* @param className name of the primary class
* @param javaCode source to compile
* @param writer destination for diagnostic output
* @param fileManager file manager used to collect compiled classes
* @return map of class names to compiled bytecode
*/
@NotNull
Map<String, byte[]> compileFromJava(@NotNull String className,
@NotNull String javaCode,
final @NotNull PrintWriter writer,
MyJavaFileManager fileManager) {
validateClassName(className);
Iterable<? extends JavaFileObject> compilationUnits;
if (sourceDir != null) {
String filename = className.replaceAll("\\.", '\\' + File.separator) + ".java";
File file = safeResolve(sourceDir, filename);
writeText(file, javaCode);
if (s_standardJavaFileManager == null)
s_standardJavaFileManager = s_compiler.getStandardFileManager(null, null, null);
compilationUnits = s_standardJavaFileManager.getJavaFileObjects(file);
} else {
javaFileObjects.put(className, new JavaSourceFromString(className, javaCode));
compilationUnits = new ArrayList<>(javaFileObjects.values()); // To prevent CME from compiler code
}
// reuse the same file manager to allow caching of jar files
boolean ok = s_compiler.getTask(writer, fileManager, new DiagnosticListener<JavaFileObject>() {
@Override
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
writer.println(diagnostic);
}
}
}, options, null, compilationUnits).call();
if (!ok) {
// compilation error, so we want to exclude this file from future compilation passes
if (sourceDir == null)
javaFileObjects.remove(className);
// nothing to return due to compiler error
return Collections.emptyMap();
} else {
Map<String, byte[]> result = fileManager.getAllBuffers();
return result;
}
}
/**
* Compile and load using a specific class loader and writer. The
* compilation result is cached against the loader for future calls.
*
* @param classLoader loader to define the class with
* @param className expected binary name
* @param javaCode source code to compile
* @param writer destination for diagnostic messages, may be null
* @return the loaded class instance
* @throws ClassNotFoundException if definition fails
*/
public Class<?> loadFromJava(@NotNull ClassLoader classLoader,
@NotNull String className,
@NotNull String javaCode,
@Nullable PrintWriter writer) throws ClassNotFoundException {
Class<?> clazz = null;
Map<String, Class<?>> loadedClasses;
synchronized (loadedClassesMap) {
loadedClasses = loadedClassesMap.get(classLoader);
if (loadedClasses == null)
loadedClassesMap.put(classLoader, loadedClasses = new LinkedHashMap<>());
else
clazz = loadedClasses.get(className);
}
PrintWriter printWriter = writer == null ? DEFAULT_WRITER : writer;
if (clazz != null)
return clazz;
MyJavaFileManager fileManager = fileManagerMap.get(classLoader);
if (fileManager == null) {
StandardJavaFileManager standardJavaFileManager = s_compiler.getStandardFileManager(null, null, null);
fileManager = getFileManager(standardJavaFileManager);
fileManagerMap.put(classLoader, fileManager);
}
final Map<String, byte[]> compiled = compileFromJava(className, javaCode, printWriter, fileManager);
for (Map.Entry<String, byte[]> entry : compiled.entrySet()) {
String className2 = entry.getKey();
validateClassName(className2);
synchronized (loadedClassesMap) {
if (loadedClasses.containsKey(className2))
continue;
}
byte[] bytes = entry.getValue();
if (classDir != null) {
String filename = className2.replaceAll("\\.", '\\' + File.separator) + ".class";
boolean changed = writeBytes(safeResolve(classDir, filename), bytes);
if (changed) {
LOG.info("Updated {} in {}", className2, classDir);
}
}
synchronized (className2.intern()) { // To prevent duplicate class definition error
synchronized (loadedClassesMap) {
if (loadedClasses.containsKey(className2))
continue;
}
Class<?> clazz2 = CompilerUtils.defineClass(classLoader, className2, bytes);
synchronized (loadedClassesMap) {
loadedClasses.put(className2, clazz2);
}
}
}
synchronized (loadedClassesMap) {
loadedClasses.put(className, clazz = classLoader.loadClass(className));
}
return clazz;
}
/**
* Update the file manager for a specific class loader. This is mainly a
* testing utility and is ignored when no manager exists for the loader.
*
* @param classLoader the class loader to update
* @param updateFileManager function applying the update
*/
public void updateFileManagerForClassLoader(ClassLoader classLoader, Consumer<MyJavaFileManager> updateFileManager) {
MyJavaFileManager fileManager = fileManagerMap.get(classLoader);
if (fileManager != null) {
updateFileManager.accept(fileManager);
}
}
public void setFileManagerOverride(Function<StandardJavaFileManager, MyJavaFileManager> fileManagerOverride) {
this.fileManagerOverride = fileManagerOverride;
}
private @NotNull MyJavaFileManager getFileManager(StandardJavaFileManager fm) {
return fileManagerOverride != null
? fileManagerOverride.apply(fm)
: new MyJavaFileManager(fm);
}
private static void validateClassName(String className) {
Objects.requireNonNull(className, "className");
if (!CLASS_NAME_PATTERN.matcher(className).matches()) {
throw new IllegalArgumentException("Invalid class name: " + className);
}
for (String segment : className.split("\\.", -1)) {
if (!CLASS_NAME_SEGMENT_PATTERN.matcher(segment).matches()) {
throw new IllegalArgumentException("Invalid class name: " + className);
}
}
}
static File safeResolve(File root, String relativePath) {
Objects.requireNonNull(root, "root");
Objects.requireNonNull(relativePath, "relativePath");
Path base = root.toPath().toAbsolutePath().normalize();
Path candidate = base.resolve(relativePath).normalize();
if (!candidate.startsWith(base)) {
throw new IllegalArgumentException("Attempted path traversal for " + relativePath);
}
return candidate.toFile();
}
private static PrintWriter createDefaultWriter() {
OutputStreamWriter writer = new OutputStreamWriter(System.err, StandardCharsets.UTF_8);
return new PrintWriter(writer, true) {
@Override
public void close() {
flush(); // never close System.err
}
};
}
}