8210839: Improve interaction between source launcher and classpath
Reviewed-by: alanb, mchung
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java Tue Sep 25 09:34:51 2018 -0700
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java Tue Sep 25 10:30:32 2018 -0700
@@ -50,8 +50,6 @@
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.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 @@
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 @@
}
/**
- * 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[]> sourceFileClasses, ClassLoader parent) {
+ super(parent);
+ 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;
+ }
+ }
+
- MemoryClassLoader(Map<String, byte[]> map, ClassLoader parent) {
- super(parent);
- this.map = map;
+ /**
+ * 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 @@
@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 @@
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())));
}
}
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties Tue Sep 25 09:34:51 2018 -0700
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties Tue Sep 25 10:30:32 2018 -0700
@@ -109,10 +109,6 @@
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}
--- a/test/langtools/tools/javac/launcher/SourceLauncherTest.java Tue Sep 25 09:34:51 2018 -0700
+++ b/test/langtools/tools/javac/launcher/SourceLauncherTest.java Tue Sep 25 10:30:32 2018 -0700
@@ -47,6 +47,7 @@
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 @@
}
@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 @@
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 @@
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()) {
--- a/test/langtools/tools/javac/launcher/src/CLTest.java Tue Sep 25 09:34:51 2018 -0700
+++ b/test/langtools/tools/javac/launcher/src/CLTest.java Tue Sep 25 10:30:32 2018 -0700
@@ -43,38 +43,38 @@
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 @@
}
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);