Faster compilation of functions

This change includes caching and smarter analysis of classpaths
to limit the amount of jar walking necessary to find dependencies
when compiling. It also includes some new tests that verify
packaging of dependencies in boot style form (BOOT-INF/lib and
BOOT-INF/classes).
This commit is contained in:
Andy Clement
2017-11-05 11:26:57 -08:00
committed by Dave Syer
parent 8d5f09efa1
commit aaa6b6526c
5 changed files with 742 additions and 34 deletions

View File

@@ -20,6 +20,8 @@ import java.io.File;
import javax.tools.JavaFileObject;
import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache;
/**
* Common superclass for iterables that need to handle closing when finished
* with and that need to handle possible constraints on the values that
@@ -35,22 +37,26 @@ public abstract class CloseableFilterableJavaFileObjectIterable implements Itera
private final static String BOOT_PACKAGING_PREFIX_FOR_CLASSES = "BOOT-INF/classes/";
// If set specifies the package the iterator consumer is interested in. Only
// return results in this package.
private String packageNameFilter;
// return results in this package. Will have a trailing separator to speed
// matching. '/' on its own represents the default package
protected String packageNameFilter;
// Indicates whether the consumer of the iterator wants to see classes
// that are in subpackages of those matching the filter.
private boolean includeSubpackages;
protected boolean includeSubpackages;
protected CompilationInfoCache compilationInfoCache;
public CloseableFilterableJavaFileObjectIterable(String packageNameFilter, boolean includeSubpackages) {
public CloseableFilterableJavaFileObjectIterable(CompilationInfoCache compilationInfoCache, String packageNameFilter, boolean includeSubpackages) {
if (packageNameFilter!=null && packageNameFilter.contains(File.separator)) {
throw new IllegalArgumentException("Package name filters should use dots to separate components: "+packageNameFilter);
}
this.compilationInfoCache = compilationInfoCache;
// Normalize filter to forward slashes
this.packageNameFilter = packageNameFilter==null?null:packageNameFilter.replace('.', '/') + '/';
this.includeSubpackages = includeSubpackages;
}
/**
* Used by subclasses to check values against any specified constraints.
*
@@ -68,6 +74,16 @@ public abstract class CloseableFilterableJavaFileObjectIterable implements Itera
boolean accept;
// Normalize to forward slashes (some jars are producing paths with forward slashes, some with backward slashes)
name = name.replace('\\', '/');
if (packageNameFilter.length() == 1 && packageNameFilter.equals("/")) {
// This is the 'default package' filter representation
if (name.indexOf('/') == -1) {
accept = true;
} else if (BOOT_PACKAGING_AWARE) {
accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) &&
name.indexOf('/',BOOT_PACKAGING_PREFIX_FOR_CLASSES.length()) == -1;
}
return accept;
}
if (includeSubpackages == true) {
accept = name.startsWith(packageNameFilter);
if (!accept && BOOT_PACKAGING_AWARE) {

View File

@@ -33,6 +33,8 @@ import javax.tools.JavaFileObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache;
import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache.ArchiveInfo;
/**
* Iterable that will produce an iterator that returns classes found
@@ -46,24 +48,30 @@ public class IterableClasspath extends CloseableFilterableJavaFileObjectIterable
private static Logger logger = LoggerFactory.getLogger(IterableClasspath.class);
private static final String BOOT_PACKAGING_PREFIX_FOR_LIBRARIES = "BOOT-INF/lib/";
private List<File> classpathEntries = new ArrayList<>();
private List<ZipFile> openArchives = new ArrayList<>();
/**
* @param compilationInfoCache cache of info that may help accelerate compilation
* @param classpath a classpath of jars/directories
* @param packageNameFilter an optional package name if choosing to filter (e.g. com.example)
* @param includeSubpackages if true, include results in subpackages of the specified package filter
*/
IterableClasspath(String classpath, String packageNameFilter, boolean includeSubpackages) {
super(packageNameFilter, includeSubpackages);
IterableClasspath(CompilationInfoCache compilationInfoCache, String classpath, String packageNameFilter, boolean includeSubpackages) {
super(compilationInfoCache, packageNameFilter, includeSubpackages);
StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator);
while (tokenizer.hasMoreElements()) {
String nextEntry = tokenizer.nextToken();
File f = new File(nextEntry);
if (f.exists()) {
// Skip iterating over archives that cannot possibly match the filter
if (this.packageNameFilter != null && this.packageNameFilter.length() > 0) {
ArchiveInfo archiveInfo = compilationInfoCache.getArchiveInfoFor(f);
if (archiveInfo != null && !archiveInfo.containsPackage(this.packageNameFilter, this.includeSubpackages)) {
continue;
}
}
classpathEntries.add(f);
} else {
logger.debug("path element does not exist {}",f);
@@ -131,7 +139,7 @@ public class IterableClasspath extends CloseableFilterableJavaFileObjectIterable
nextEntry = new ZipEntryJavaFileObject(openFile, openArchive, entry);
}
return;
} else if (nestedZip == null && entryName.startsWith(BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) && entryName.endsWith(".jar")) {
} else if (nestedZip == null && entryName.startsWith(MemoryBasedJavaFileManager.BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) && entryName.endsWith(".jar")) {
// nested jar in uber jar
logger.debug("opening nested archive {}",entry.getName());
ZipInputStream zis = new ZipInputStream(openArchive.getInputStream(entry));
@@ -211,4 +219,8 @@ public class IterableClasspath extends CloseableFilterableJavaFileObjectIterable
}
}
public void reset() {
close();
}
}

View File

@@ -18,14 +18,24 @@ package org.springframework.cloud.function.compiler.java;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
@@ -37,6 +47,7 @@ import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.function.compiler.java.IterableClasspath.ZipEnumerator;
/**
* A file manager that serves source code from in memory and ensures output results are
@@ -48,17 +59,30 @@ import org.slf4j.LoggerFactory;
*/
public class MemoryBasedJavaFileManager implements JavaFileManager {
final static String BOOT_PACKAGING_PREFIX_FOR_CLASSES = "BOOT-INF/classes/";
final static String BOOT_PACKAGING_PREFIX_FOR_LIBRARIES = "BOOT-INF/lib/";
private static Logger logger = LoggerFactory
.getLogger(MemoryBasedJavaFileManager.class);
private CompilationOutputCollector outputCollector;
private List<CloseableFilterableJavaFileObjectIterable> toClose = new ArrayList<>();
// private List<CloseableFilterableJavaFileObjectIterable> toClose = new ArrayList<>();
private Map<String, File> resolvedAdditionalDependencies = new LinkedHashMap<>();
private String platformClasspath;
private String classpath;
private CompilationInfoCache compilationInfoCache;
private Map<Key, IterableClasspath> iterables = new HashMap<>();
public MemoryBasedJavaFileManager() {
outputCollector = new CompilationOutputCollector();
compilationInfoCache = new CompilationInfoCache();
}
@Override
@@ -74,7 +98,184 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
logger.debug("getClassLoader({})", location);
return null; // Do not currently need to load plugins
}
// Holds information that may help speed up compilation
static class CompilationInfoCache {
private Map<File, ArchiveInfo> archivePackageCache;
static class ArchiveInfo {
// The packages identified in a particular archive
private List<String> packageNames;
private boolean isBootJar = false;
public ArchiveInfo(List<String> packageNames, boolean isBootJar) {
this.packageNames = packageNames;
Collections.sort(this.packageNames);
this.isBootJar = isBootJar;
}
public List<String> getPackageNames() {
return packageNames;
}
public boolean isBootJar() {
return isBootJar;
}
public boolean containsPackage(String packageName, boolean subpackageMatchesAllowed) {
if (subpackageMatchesAllowed) {
for (String candidatePackageName: packageNames) {
if (candidatePackageName.startsWith(packageName)) {
return true;
}
}
return false;
} else {
// Must be an exact match, fast binary search:
int pos = Collections.binarySearch(packageNames, packageName);
return (pos >= 0);
}
}
}
ArchiveInfo getArchiveInfoFor(File archive) {
if (!archive.isFile() || !(archive.getName().endsWith(".zip") || archive.getName().endsWith(".jar"))) {
// it is not an archive
return null;
}
if (archivePackageCache == null) {
archivePackageCache = new HashMap<>();
}
try {
ArchiveInfo result = archivePackageCache.get(archive);
if (result == null) {
result = buildArchiveInfo(archive);
archivePackageCache.put(archive, result);
}
return result;
} catch (Exception e) {
throw new IllegalStateException("Unexpected problem caching entries from "+archive.getName(), e);
}
}
/**
* Walk the specified archive and collect up the package names of any .class files encountered. If
* the archive contains nested jars packaged in a BOOT style way (under a BOOT-INF/lib folder) then
* walk those too and include relevant packages.
*
* @param file archive file to discover packages from
* @return an ArchiveInfo encapsulating package info from the archive
*/
private ArchiveInfo buildArchiveInfo(File file) {
List<String> packageNames = new ArrayList<>();
boolean isBootJar = false;
try (ZipFile openArchive = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = openArchive.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(".class")) {
if (name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES)) {
isBootJar = true;
int idx = name.lastIndexOf('/') + 1;
if (idx != 0 ) {
if (idx == BOOT_PACKAGING_PREFIX_FOR_CLASSES.length()) {
// default package
packageNames.add("/");
} else {
// Normalize to forward slashes
name = name.substring(BOOT_PACKAGING_PREFIX_FOR_CLASSES.length(), idx);
name = name.replace('\\', '/');
packageNames.add(name);
}
}
} else {
int idx = name.lastIndexOf('/') + 1;
// if (name.contains("TestX")) {
// System.out.println("For TestX: "+name+" "+idx+" "+BOOT_PACKAGING_PREFIX_FOR_CLASSES.length());
// }
if (idx != 0 ) {
// Normalize to forward slashes
name = name.replace('\\', '/');
name = name.substring(0, idx);
packageNames.add(name);
} else if (idx == 0) {
// default package entries in here
packageNames.add("/");
}
}
} else if (name.startsWith(BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) && name.endsWith(".jar")) {
isBootJar = true;
try (ZipInputStream zis = new ZipInputStream(openArchive.getInputStream(entry))) {
Enumeration<? extends ZipEntry> nestedZipEnumerator = new ZipEnumerator(zis);
while (nestedZipEnumerator.hasMoreElements()) {
ZipEntry innerEntry = nestedZipEnumerator.nextElement();
String innerEntryName = innerEntry.getName();
if (innerEntryName.endsWith(".class")) {
int idx = innerEntryName.lastIndexOf('/') + 1;
if (idx != 0 ) {
// Normalize to forward slashes
innerEntryName = innerEntryName.replace('\\', '/');
innerEntryName = innerEntryName.substring(0, idx);
packageNames.add(innerEntryName);
} else if (idx == 0) {
// default package entries in here
packageNames.add("/");
}
}
}
}
}
}
} catch (IOException ioe) {
throw new IllegalStateException("Unexpected problem determining packages in "+file,ioe);
}
return new ArchiveInfo(packageNames, isBootJar);
}
}
static class Key {
private String classpath;
private String packageName;
private Set<Kind> kinds;
private boolean recurse;
public Key(String classpath, String packageName, Set<Kind> kinds, boolean recurse) {
this.classpath = classpath;
this.packageName = packageName;
this.kinds = kinds;
this.recurse = recurse;
}
@Override
public int hashCode() {
return ((classpath.hashCode()*37+(packageName==null?0:packageName.hashCode()))*37+kinds.hashCode())*37+(recurse?1:0);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Key)) {
return false;
}
Key that = (Key)obj;
return classpath.equals(that.classpath) &&
kinds.equals(that.kinds) &&
(recurse==that.recurse) &&
(packageName==null?(that.packageName==null):this.packageName.equals(that.packageName));
}
}
private String getPlatformClassPath() {
if (platformClasspath == null) {
platformClasspath = System.getProperty("sun.boot.class.path");
}
return platformClasspath;
}
@Override
public Iterable<JavaFileObject> list(Location location, String packageName,
Set<Kind> kinds, boolean recurse) throws IOException {
@@ -82,7 +283,7 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
String classpath = "";
if (location == StandardLocation.PLATFORM_CLASS_PATH
&& (kinds == null || kinds.contains(Kind.CLASS))) {
classpath = System.getProperty("sun.boot.class.path");
classpath = getPlatformClassPath();
logger.debug("Creating iterable for boot class path: {}", classpath);
}
else if (location == StandardLocation.CLASS_PATH
@@ -98,34 +299,69 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
classpath = javaClassPath;
logger.debug("Creating iterable for class path: {}", classpath);
}
CloseableFilterableJavaFileObjectIterable resultIterable = new IterableClasspath(
classpath, packageName, recurse);
toClose.add(resultIterable);
Key k = new Key(classpath, packageName, kinds, recurse);
IterableClasspath resultIterable = iterables.get(k);
if (resultIterable == null) {
resultIterable = new IterableClasspath(compilationInfoCache, classpath, packageName, recurse);
iterables.put(k, resultIterable);
}
resultIterable.reset();
return resultIterable;
}
private String getClassPath() {
ClassLoader loader = InMemoryJavaFileObject.class.getClassLoader();
if (loader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) loader).getURLs();
if (urls.length > 1) { // heuristic that catches Maven surefire tests
if (!urls[0].toString().startsWith("jar:file:")) { // heuristic for Spring Boot fat jar
StringBuilder builder = new StringBuilder();
for (URL url : urls) {
if (builder.length() > 0) {
builder.append(File.pathSeparator);
if (classpath == null) {
ClassLoader loader = InMemoryJavaFileObject.class.getClassLoader();
String cp = null;
if (loader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) loader).getURLs();
if (urls.length > 1) { // heuristic that catches Maven surefire tests
if (!urls[0].toString().startsWith("jar:file:")) { // heuristic for Spring Boot fat jar
StringBuilder builder = new StringBuilder();
for (URL url : urls) {
if (builder.length() > 0) {
builder.append(File.pathSeparator);
}
String path = url.toString();
if (path.startsWith("file:")) {
path = path.substring("file:".length());
}
builder.append(path);
}
String path = url.toString();
if (path.startsWith("file:")) {
path = path.substring("file:".length());
}
builder.append(path);
cp = builder.toString();
}
return builder.toString();
}
}
if (cp == null) {
cp = System.getProperty("java.class.path");
}
classpath = pathWithPlatformClassPathRemoved(cp);
}
return System.getProperty("java.class.path");
return classpath;
}
// remove the platform classpath entries, they will be search separately (and earlier)
private String pathWithPlatformClassPathRemoved(String classpath) {
Set<String> pcps = toList(getPlatformClassPath());
Set<String> cps = toList(classpath);
cps.removeAll(pcps);
StringBuilder builder = new StringBuilder();
for (String cpe: cps) {
if (builder.length() > 0) {
builder.append(File.pathSeparator);
}
builder.append(cpe);
}
return builder.toString();
}
private Set<String> toList(String path) {
Set<String> result = new LinkedHashSet<>();
StringTokenizer tokenizer = new StringTokenizer(path,File.pathSeparator);
while (tokenizer.hasMoreTokens()) {
result.add(tokenizer.nextToken());
}
return result;
}
@Override
@@ -201,8 +437,9 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
@Override
public void close() throws IOException {
for (CloseableFilterableJavaFileObjectIterable closeable : toClose) {
closeable.close();
Collection<IterableClasspath> toClose = iterables.values();
for (IterableClasspath icp: toClose) {
icp.close();
}
}
@@ -232,6 +469,8 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
CompilationMessage.Kind.ERROR, re.getMessage(), null, 0, 0);
resolutionMessages.add(compilationMessage);
}
} else if (dependency.startsWith("file:")) {
resolvedAdditionalDependencies.put(dependency, new File(URI.create(dependency)));
}
else {
resolutionMessages.add(new CompilationMessage(

View File

@@ -49,7 +49,7 @@ public class RuntimeJavaCompiler {
* classes.
* @param className the name of the class (dotted form, e.g. com.foo.bar.Goo)
* @param classSourceCode the full source code for the class
* @param dependencies optional maven coordinates for dependencies "maven://groupId:artifactId:version"
* @param dependencies optional coordinates for dependencies, maven 'maven://groupId:artifactId:version', or 'file:' URIs for local files
* @return a CompilationResult that encapsulates what happened during compilation (classes/messages produced)
*/
public CompilationResult compile(String className, String classSourceCode, String... dependencies) {