8210839: Improve interaction between source launcher and classpath

Reviewed-by: alanb, mchung
This commit is contained in:
Jonathan Gibbons 2018-09-25 10:30:32 -07:00
parent 64099fc1cc
commit 59fd35292e
4 changed files with 238 additions and 55 deletions

View File

@ -50,8 +50,6 @@ import java.nio.file.Files;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -63,6 +61,8 @@ import java.util.Map;
import java.util.MissingResourceException; import java.util.MissingResourceException;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.SourceVersion; import javax.lang.model.SourceVersion;
import javax.lang.model.element.NestingKind; import javax.lang.model.element.NestingKind;
@ -399,9 +399,6 @@ public class Main {
ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader()); ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader());
try { try {
Class<?> appClass = Class.forName(mainClassName, true, cl); Class<?> appClass = Class.forName(mainClassName, true, cl);
if (appClass.getClassLoader() != cl) {
throw new Fault(Errors.UnexpectedClass(mainClassName));
}
Method main = appClass.getDeclaredMethod("main", String[].class); Method main = appClass.getDeclaredMethod("main", String[].class);
int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC; int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC;
if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) { if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) {
@ -535,26 +532,103 @@ public class Main {
} }
/** /**
* An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}. * An in-memory classloader, that uses an in-memory cache of classes written by
* {@link MemoryFileManager}.
* *
* <p>The classloader uses the standard parent-delegation model, just providing * <p>The classloader inverts the standard parent-delegation model, giving preference
* {@code findClass} to find classes in the in-memory cache. * to classes defined in the source file before classes known to the parent (such
* as any like-named classes that might be found on the application class path.)
*/ */
private static class MemoryClassLoader extends ClassLoader { private static class MemoryClassLoader extends ClassLoader {
/** /**
* The map of classes known to this class loader, indexed by * The map of all classes found in the source file, indexed by
* {@link ClassLoader#name binary name}. * {@link ClassLoader#name binary name}.
*/ */
private final Map<String, byte[]> map; private final Map<String, byte[]> sourceFileClasses;
MemoryClassLoader(Map<String, byte[]> map, ClassLoader parent) { MemoryClassLoader(Map<String, byte[]> sourceFileClasses, ClassLoader parent) {
super(parent); super(parent);
this.map = map; this.sourceFileClasses = sourceFileClasses;
}
/**
* Override loadClass to check for classes defined in the source file
* before checking for classes in the parent class loader,
* including those on the classpath.
*
* {@code loadClass(String name)} calls this method, and so will have the same behavior.
*
* @param name the name of the class to load
* @param resolve whether or not to resolve the class
* @return the class
* @throws ClassNotFoundException if the class is not found
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
if (sourceFileClasses.containsKey(name)) {
c = findClass(name);
} else {
c = getParent().loadClass(name);
}
if (resolve) {
resolveClass(c);
}
}
return c;
}
}
/**
* Override getResource to check for resources (i.e. class files) defined in the
* source file before checking resources in the parent class loader,
* including those on the class path.
*
* {@code getResourceAsStream(String name)} calls this method,
* and so will have the same behavior.
*
* @param name the name of the resource
* @return a URL for the resource, or null if not found
*/
@Override
public URL getResource(String name) {
if (sourceFileClasses.containsKey(toBinaryName(name))) {
return findResource(name);
} else {
return getParent().getResource(name);
}
}
/**
* Override getResources to check for resources (i.e. class files) defined in the
* source file before checking resources in the parent class loader,
* including those on the class path.
*
* @param name the name of the resource
* @return an enumeration of the resources in this loader and in the application class loader
*/
@Override
public Enumeration<URL> getResources(String name) throws IOException {
URL u = findResource(name);
Enumeration<URL> e = getParent().getResources(name);
if (u == null) {
return e;
} else {
List<URL> list = new ArrayList<>();
list.add(u);
while (e.hasMoreElements()) {
list.add(e.nextElement());
}
return Collections.enumeration(list);
}
} }
@Override @Override
protected Class<?> findClass(String name) throws ClassNotFoundException { protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = map.get(name); byte[] bytes = sourceFileClasses.get(name);
if (bytes == null) { if (bytes == null) {
throw new ClassNotFoundException(name); throw new ClassNotFoundException(name);
} }
@ -564,7 +638,7 @@ public class Main {
@Override @Override
public URL findResource(String name) { public URL findResource(String name) {
String binaryName = toBinaryName(name); String binaryName = toBinaryName(name);
if (binaryName == null || map.get(binaryName) == null) { if (binaryName == null || sourceFileClasses.get(binaryName) == null) {
return null; return null;
} }
@ -628,7 +702,7 @@ public class Main {
if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) { if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) {
throw new IllegalArgumentException(u.toString()); throw new IllegalArgumentException(u.toString());
} }
return new MemoryURLConnection(u, map.get(toBinaryName(u.getPath()))); return new MemoryURLConnection(u, sourceFileClasses.get(toBinaryName(u.getPath())));
} }
} }

View File

@ -108,10 +108,6 @@ launcher.err.main.not.void=\
launcher.err.cant.find.class=\ launcher.err.cant.find.class=\
can''t find class: {0} can''t find class: {0}
# 0: string
launcher.err.unexpected.class=\
class found on application class path: {0}
# 0: string # 0: string
launcher.err.cant.find.main.method=\ launcher.err.cant.find.main.method=\
can''t find main(String[]) method in class: {0} can''t find main(String[]) method in class: {0}

View File

@ -47,6 +47,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.regex.Pattern;
import com.sun.tools.javac.launcher.Main; import com.sun.tools.javac.launcher.Main;
@ -233,22 +234,98 @@ public class SourceLauncherTest extends TestRunner {
} }
@Test @Test
public void testWrongClass(Path base) throws IOException { public void testLoadClass(Path base) throws IOException {
Path src1 = base.resolve("src1");
Path file1 = src1.resolve("LoadClass.java");
tb.writeJavaFiles(src1,
"class LoadClass {\n"
+ " public static void main(String... args) {\n"
+ " System.out.println(\"on classpath\");\n"
+ " };\n"
+ "}\n");
Path classes1 = Files.createDirectories(base.resolve("classes"));
new JavacTask(tb)
.outdir(classes1)
.files(file1)
.run();
String log1 = new JavaTask(tb)
.classpath(classes1.toString())
.className("LoadClass")
.run(Task.Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT);
checkEqual("stdout", log1.trim(),
"on classpath");
Path src2 = base.resolve("src2");
Path file2 = src2.resolve("LoadClass.java");
tb.writeJavaFiles(src2,
"class LoadClass {\n"
+ " public static void main(String... args) {\n"
+ " System.out.println(\"in source file\");\n"
+ " };\n"
+ "}\n");
String log2 = new JavaTask(tb)
.classpath(classes1.toString())
.className(file2.toString())
.run(Task.Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT);
checkEqual("stdout", log2.trim(),
"in source file");
}
@Test
public void testGetResource(Path base) throws IOException {
Path src = base.resolve("src"); Path src = base.resolve("src");
Path file = src.resolve("WrongClass.java"); Path file = src.resolve("GetResource.java");
tb.writeJavaFiles(src, "class WrongClass { }"); tb.writeJavaFiles(src,
"class GetResource {\n"
+ " public static void main(String... args) {\n"
+ " System.out.println(GetResource.class.getClassLoader().getResource(\"GetResource.class\"));\n"
+ " };\n"
+ "}\n");
Path classes = Files.createDirectories(base.resolve("classes")); Path classes = Files.createDirectories(base.resolve("classes"));
new JavacTask(tb) new JavacTask(tb)
.outdir(classes) .outdir(classes)
.files(file) .files(file)
.run(); .run();
String log = new JavaTask(tb) String log = new JavaTask(tb)
.classpath(classes.toString()) .classpath(classes.toString())
.className(file.toString()) .className(file.toString())
.run(Task.Expect.FAIL) .run(Task.Expect.SUCCESS)
.getOutput(Task.OutputKind.STDERR); .getOutput(Task.OutputKind.STDOUT);
checkEqual("stderr", log.trim(), checkMatch("stdout", log.trim(),
"error: class found on application class path: WrongClass"); Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResource.class"));
}
@Test
public void testGetResources(Path base) throws IOException {
Path src = base.resolve("src");
Path file = src.resolve("GetResources.java");
tb.writeJavaFiles(src,
"import java.io.*; import java.net.*; import java.util.*;\n"
+ "class GetResources {\n"
+ " public static void main(String... args) throws IOException {\n"
+ " Enumeration<URL> e =\n"
+ " GetResources.class.getClassLoader().getResources(\"GetResources.class\");\n"
+ " while (e.hasMoreElements()) System.out.println(e.nextElement());\n"
+ " };\n"
+ "}\n");
Path classes = Files.createDirectories(base.resolve("classes"));
new JavacTask(tb)
.outdir(classes)
.files(file)
.run();
List<String> log = new JavaTask(tb)
.classpath(classes.toString())
.className(file.toString())
.run(Task.Expect.SUCCESS)
.getOutputLines(Task.OutputKind.STDOUT);
checkMatch("stdout:0", log.get(0).trim(),
Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResources.class"));
checkMatch("stdout:1", log.get(1).trim(),
Pattern.compile("file:/.*/testGetResources/classes/GetResources.class"));
} }
@Test @Test
@ -294,7 +371,37 @@ public class SourceLauncherTest extends TestRunner {
checkEmpty("stdout", r.stdOut); checkEmpty("stdout", r.stdOut);
checkEqual("stderr", r.stdErr, expectStdErr); checkEqual("stderr", r.stdErr, expectStdErr);
checkFault("exception", r.exception, "error: compilation failed"); checkFault("exception", r.exception, "error: compilation failed");
}
@Test
public void testClassNotFound(Path base) throws IOException {
Path src = base.resolve("src");
Path file = src.resolve("ClassNotFound.java");
tb.writeJavaFiles(src,
"class ClassNotFound {\n"
+ " public static void main(String... args) {\n"
+ " try {\n"
+ " Class.forName(\"NoSuchClass\");\n"
+ " System.out.println(\"no exception\");\n"
+ " System.exit(1);\n"
+ " } catch (ClassNotFoundException e) {\n"
+ " System.out.println(\"Expected exception thrown: \" + e);\n"
+ " }\n"
+ " };\n"
+ "}\n");
Path classes = Files.createDirectories(base.resolve("classes"));
new JavacTask(tb)
.outdir(classes)
.files(file)
.run();
String log = new JavaTask(tb)
.classpath(classes.toString())
.className(file.toString())
.run(Task.Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT);
checkEqual("stdout", log.trim(),
"Expected exception thrown: java.lang.ClassNotFoundException: NoSuchClass");
} }
// For any source file that is invoked through the OS shebang mechanism, invalid shebang // For any source file that is invoked through the OS shebang mechanism, invalid shebang
@ -471,12 +578,18 @@ public class SourceLauncherTest extends TestRunner {
void checkEqual(String name, String found, String expect) { void checkEqual(String name, String found, String expect) {
expect = expect.replace("\n", tb.lineSeparator); expect = expect.replace("\n", tb.lineSeparator);
out.println(name + ": " + found); out.println(name + ": " + found);
out.println(name + ": " + found);
if (!expect.equals(found)) { if (!expect.equals(found)) {
error("Unexpected output; expected: " + expect); error("Unexpected output; expected: " + expect);
} }
} }
void checkMatch(String name, String found, Pattern expect) {
out.println(name + ": " + found);
if (!expect.matcher(found).matches()) {
error("Unexpected output; expected match for: " + expect);
}
}
void checkEmpty(String name, String found) { void checkEmpty(String name, String found) {
out.println(name + ": " + found); out.println(name + ": " + found);
if (!found.isEmpty()) { if (!found.isEmpty()) {

View File

@ -43,38 +43,38 @@ import java.util.*;
import com.sun.tools.classfile.ClassFile; import com.sun.tools.classfile.ClassFile;
public class CLTest { public class CLTest {
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
try { try {
new CLTest().run(); new CLTest().run();
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
System.exit(1); System.exit(1);
} }
}
void run() throws Exception {
String[] names = {
"p/q/CLTest.class",
"p/q/CLTest$Inner.class",
"p/q/CLTest2.class",
"java/lang/Object.class",
"UNKNOWN.class",
"UNKNOWN"
};
for (String name : names) {
testGetResource(name);
testGetResources(name);
testGetResourceAsStream(name);
} }
void run() throws Exception { if (errors > 0) {
String[] names = { throw new Exception(errors + " errors found");
"p/q/CLTest.class",
"p/q/CLTest$Inner.class",
"p/q/CLTest2.class",
"java/lang/Object.class",
"UNKNOWN.class",
"UNKNOWN"
};
for (String name : names) {
testGetResource(name);
testGetResources(name);
testGetResourceAsStream(name);
}
if (errors > 0) {
throw new Exception(errors + " errors found");
}
} }
}
void testGetResource(String name) { void testGetResource(String name) {
System.err.println("testGetResource: " + name); System.err.println("testGetResource: " + name);
try { try {
ClassLoader cl = getClass().getClassLoader(); ClassLoader cl = getClass().getClassLoader();
URL u = cl.getResource(name); URL u = cl.getResource(name);
@ -95,7 +95,7 @@ public class CLTest {
} }
void testGetResources(String name) { void testGetResources(String name) {
System.err.println("testGetResources: " + name); System.err.println("testGetResources: " + name);
try { try {
ClassLoader cl = getClass().getClassLoader(); ClassLoader cl = getClass().getClassLoader();
Enumeration<URL> e = cl.getResources(name); Enumeration<URL> e = cl.getResources(name);