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.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
@ -63,6 +61,8 @@ import java.util.Map;
import java.util.MissingResourceException;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.NestingKind;
@ -399,9 +399,6 @@ public class Main {
ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader());
try {
Class<?> appClass = Class.forName(mainClassName, true, cl);
if (appClass.getClassLoader() != cl) {
throw new Fault(Errors.UnexpectedClass(mainClassName));
}
Method main = appClass.getDeclaredMethod("main", String[].class);
int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.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
* {@code findClass} to find classes in the in-memory cache.
* <p>The classloader inverts the standard parent-delegation model, giving preference
* 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 {
/**
* 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}.
*/
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);
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
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = map.get(name);
byte[] bytes = sourceFileClasses.get(name);
if (bytes == null) {
throw new ClassNotFoundException(name);
}
@ -564,7 +638,7 @@ public class Main {
@Override
public URL findResource(String name) {
String binaryName = toBinaryName(name);
if (binaryName == null || map.get(binaryName) == null) {
if (binaryName == null || sourceFileClasses.get(binaryName) == null) {
return null;
}
@ -628,7 +702,7 @@ public class Main {
if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) {
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=\
can''t find class: {0}
# 0: string
launcher.err.unexpected.class=\
class found on application class path: {0}
# 0: string
launcher.err.cant.find.main.method=\
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.List;
import java.util.Properties;
import java.util.regex.Pattern;
import com.sun.tools.javac.launcher.Main;
@ -233,22 +234,98 @@ public class SourceLauncherTest extends TestRunner {
}
@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 file = src.resolve("WrongClass.java");
tb.writeJavaFiles(src, "class WrongClass { }");
Path file = src.resolve("GetResource.java");
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"));
new JavacTask(tb)
.outdir(classes)
.files(file)
.run();
String log = new JavaTask(tb)
.classpath(classes.toString())
.className(file.toString())
.run(Task.Expect.FAIL)
.getOutput(Task.OutputKind.STDERR);
checkEqual("stderr", log.trim(),
"error: class found on application class path: WrongClass");
.run(Task.Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT);
checkMatch("stdout", log.trim(),
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
@ -294,7 +371,37 @@ public class SourceLauncherTest extends TestRunner {
checkEmpty("stdout", r.stdOut);
checkEqual("stderr", r.stdErr, expectStdErr);
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
@ -471,12 +578,18 @@ public class SourceLauncherTest extends TestRunner {
void checkEqual(String name, String found, String expect) {
expect = expect.replace("\n", tb.lineSeparator);
out.println(name + ": " + found);
out.println(name + ": " + found);
if (!expect.equals(found)) {
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) {
out.println(name + ": " + found);
if (!found.isEmpty()) {

View File

@ -43,38 +43,38 @@ import java.util.*;
import com.sun.tools.classfile.ClassFile;
public class CLTest {
public static void main(String... args) throws Exception {
try {
new CLTest().run();
} catch (Throwable t) {
t.printStackTrace();
System.exit(1);
}
public static void main(String... args) throws Exception {
try {
new CLTest().run();
} catch (Throwable t) {
t.printStackTrace();
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 {
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);
}
if (errors > 0) {
throw new Exception(errors + " errors found");
}
if (errors > 0) {
throw new Exception(errors + " errors found");
}
}
void testGetResource(String name) {
System.err.println("testGetResource: " + name);
System.err.println("testGetResource: " + name);
try {
ClassLoader cl = getClass().getClassLoader();
URL u = cl.getResource(name);
@ -95,7 +95,7 @@ public class CLTest {
}
void testGetResources(String name) {
System.err.println("testGetResources: " + name);
System.err.println("testGetResources: " + name);
try {
ClassLoader cl = getClass().getClassLoader();
Enumeration<URL> e = cl.getResources(name);