diff --git a/src/java.base/share/classes/sun/launcher/LauncherHelper.java b/src/java.base/share/classes/sun/launcher/LauncherHelper.java index 3e65949e59f..60115526027 100644 --- a/src/java.base/share/classes/sun/launcher/LauncherHelper.java +++ b/src/java.base/share/classes/sun/launcher/LauncherHelper.java @@ -904,6 +904,9 @@ public final class LauncherHelper { return false; } + private static boolean isStaticMain = false; + private static boolean noArgMain = false; + // Check the existence and signature of main and abort if incorrect. private static void validateMainMethod(Class mainClass) { Method mainMethod = null; @@ -927,19 +930,19 @@ public final class LauncherHelper { } int mods = mainMethod.getModifiers(); - boolean isStatic = Modifier.isStatic(mods); + isStaticMain = Modifier.isStatic(mods); boolean isPublic = Modifier.isPublic(mods); - boolean noArgs = mainMethod.getParameterCount() == 0; + noArgMain = mainMethod.getParameterCount() == 0; if (!PreviewFeatures.isEnabled()) { - if (!isStatic || !isPublic || noArgs) { + if (!isStaticMain || !isPublic || noArgMain) { abort(null, "java.launcher.cls.error2", mainClass.getName(), JAVAFX_APPLICATION_CLASS_NAME); } return; } - if (!isStatic) { + if (!isStaticMain) { String className = mainMethod.getDeclaringClass().getName(); if (mainClass.isMemberClass() && !Modifier.isStatic(mainClass.getModifiers())) { abort(null, "java.launcher.cls.error7", className); diff --git a/src/java.base/share/native/libjli/java.c b/src/java.base/share/native/libjli/java.c index 59cdeb770d3..1d506c3a6df 100644 --- a/src/java.base/share/native/libjli/java.c +++ b/src/java.base/share/native/libjli/java.c @@ -387,21 +387,11 @@ JLI_Launch(int argc, char ** argv, /* main argc, argv */ } \ } while (JNI_FALSE) -#define CHECK_EXCEPTION_FAIL() \ +#define CHECK_EXCEPTION_NULL_FAIL(obj) \ do { \ if ((*env)->ExceptionOccurred(env)) { \ - (*env)->ExceptionClear(env); \ return 0; \ - } \ - } while (JNI_FALSE) - - -#define CHECK_EXCEPTION_NULL_FAIL(mainObject) \ - do { \ - if ((*env)->ExceptionOccurred(env)) { \ - (*env)->ExceptionClear(env); \ - return 0; \ - } else if (mainObject == NULL) { \ + } else if (obj == NULL) { \ return 0; \ } \ } while (JNI_FALSE) @@ -414,7 +404,7 @@ int invokeStaticMainWithArgs(JNIEnv *env, jclass mainClass, jobjectArray mainArgs) { jmethodID mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); - CHECK_EXCEPTION_FAIL(); + CHECK_EXCEPTION_NULL_FAIL(mainID); (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); return 1; } @@ -426,15 +416,15 @@ invokeStaticMainWithArgs(JNIEnv *env, jclass mainClass, jobjectArray mainArgs) { int invokeInstanceMainWithArgs(JNIEnv *env, jclass mainClass, jobjectArray mainArgs) { jmethodID constructor = (*env)->GetMethodID(env, mainClass, "", "()V"); - CHECK_EXCEPTION_FAIL(); + CHECK_EXCEPTION_NULL_FAIL(constructor); jobject mainObject = (*env)->NewObject(env, mainClass, constructor); CHECK_EXCEPTION_NULL_FAIL(mainObject); - jmethodID mainID = (*env)->GetMethodID(env, mainClass, "main", - "([Ljava/lang/String;)V"); - CHECK_EXCEPTION_FAIL(); + jmethodID mainID = + (*env)->GetMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); + CHECK_EXCEPTION_NULL_FAIL(mainID); (*env)->CallVoidMethod(env, mainObject, mainID, mainArgs); return 1; - } +} /* * Invoke a static main without arguments. Returns 1 (true) if successful otherwise @@ -444,7 +434,7 @@ int invokeStaticMainWithoutArgs(JNIEnv *env, jclass mainClass) { jmethodID mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "()V"); - CHECK_EXCEPTION_FAIL(); + CHECK_EXCEPTION_NULL_FAIL(mainID); (*env)->CallStaticVoidMethod(env, mainClass, mainID); return 1; } @@ -456,12 +446,12 @@ invokeStaticMainWithoutArgs(JNIEnv *env, jclass mainClass) { int invokeInstanceMainWithoutArgs(JNIEnv *env, jclass mainClass) { jmethodID constructor = (*env)->GetMethodID(env, mainClass, "", "()V"); - CHECK_EXCEPTION_FAIL(); + CHECK_EXCEPTION_NULL_FAIL(constructor); jobject mainObject = (*env)->NewObject(env, mainClass, constructor); CHECK_EXCEPTION_NULL_FAIL(mainObject); jmethodID mainID = (*env)->GetMethodID(env, mainClass, "main", "()V"); - CHECK_EXCEPTION_FAIL(); + CHECK_EXCEPTION_NULL_FAIL(mainID); (*env)->CallVoidMethod(env, mainObject, mainID); return 1; } @@ -483,6 +473,11 @@ JavaMain(void* _args) jobjectArray mainArgs; int ret = 0; jlong start = 0, end = 0; + jclass helperClass; + jfieldID isStaticMainField; + jboolean isStaticMain; + jfieldID noArgMainField; + jboolean noArgMain; RegisterThread(); @@ -620,12 +615,31 @@ JavaMain(void* _args) * The main method is invoked here so that extraneous java stacks are not in * the application stack trace. */ - if (!invokeStaticMainWithArgs(env, mainClass, mainArgs) && - !invokeInstanceMainWithArgs(env, mainClass, mainArgs) && - !invokeStaticMainWithoutArgs(env, mainClass) && - !invokeInstanceMainWithoutArgs(env, mainClass)) { - ret = 1; - LEAVE(); + + helperClass = GetLauncherHelperClass(env); + isStaticMainField = (*env)->GetStaticFieldID(env, helperClass, "isStaticMain", "Z"); + CHECK_EXCEPTION_NULL_LEAVE(isStaticMainField); + isStaticMain = (*env)->GetStaticBooleanField(env, helperClass, isStaticMainField); + + noArgMainField = (*env)->GetStaticFieldID(env, helperClass, "noArgMain", "Z"); + CHECK_EXCEPTION_NULL_LEAVE(noArgMainField); + noArgMain = (*env)->GetStaticBooleanField(env, helperClass, noArgMainField); + + if (isStaticMain) { + if (noArgMain) { + ret = invokeStaticMainWithoutArgs(env, mainClass); + } else { + ret = invokeStaticMainWithArgs(env, mainClass, mainArgs); + } + } else { + if (noArgMain) { + ret = invokeInstanceMainWithoutArgs(env, mainClass); + } else { + ret = invokeInstanceMainWithArgs(env, mainClass, mainArgs); + } + } + if (!ret) { + CHECK_EXCEPTION_LEAVE(1); } /* diff --git a/test/jdk/tools/launcher/InstanceMainTest.java b/test/jdk/tools/launcher/InstanceMainTest.java index 08b5cb48dbb..699db27d7bb 100644 --- a/test/jdk/tools/launcher/InstanceMainTest.java +++ b/test/jdk/tools/launcher/InstanceMainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,10 +22,14 @@ */ import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; /** * @test - * @summary test execution priority of main methods + * @bug 8329420 + * @summary test execution priority and behavior of main methods * @run main InstanceMainTest */ public class InstanceMainTest extends TestHelper { @@ -175,16 +179,191 @@ public class InstanceMainTest extends TestHelper { """ }; - public static void main(String... args) throws Exception { + private static void testMethodOrder() throws Exception { for (String source : SOURCES) { - Files.writeString(Path.of("MainClass.java"), source); - var version = System.getProperty("java.specification.version"); - var tr = doExec(javaCmd, "--enable-preview", "--source", version, "MainClass.java"); - if (!tr.isOK()) { - System.err.println(source); - System.err.println(tr); - throw new AssertionError(); - } + performTest(source, true, tr -> { + if (!tr.isOK()) { + System.err.println(source); + System.err.println(tr); + throw new AssertionError(); + } + }); } } + + record TestCase(String sourceCode, boolean enablePreview, List expectedOutput) { + + public TestCase(String sourceCode, List expectedOutput) { + this(sourceCode, true, expectedOutput); + } + + } + + private static final TestCase[] EXECUTION_ORDER = new TestCase[] { + new TestCase(""" + public class MainClass { + public MainClass() { + System.out.println("Constructor called!"); + } + public static void main() { + System.out.println("main called!"); + } + } + """, + List.of("main called!")), + new TestCase(""" + public class MainClass { + public MainClass() { + System.out.println("Constructor called!"); + } + public void main() { + System.out.println("main called!"); + } + } + """, + List.of("Constructor called!", "main called!")) + }; + + private static void testExecutionOrder() throws Exception { + for (TestCase testCase : EXECUTION_ORDER) { + performTest(testCase.sourceCode, testCase.enablePreview(), tr -> { + if (!Objects.equals(testCase.expectedOutput, tr.testOutput)) { + throw new AssertionError("Unexpected output, " + + "expected: " + testCase.expectedOutput + + ", actual: " + tr.testOutput); + } + }); + } + } + + private static final TestCase[] EXECUTION_ERRORS = new TestCase[] { + new TestCase(""" + public class MainClass { + public MainClass() { + System.out.println("Constructor called!"); + if (true) throw new Error(); + } + public void main(String... args) { + System.out.println("main called!"); + } + } + """, + List.of("Constructor called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:4)")), + new TestCase(""" + public class MainClass { + public MainClass() { + System.out.println("Constructor called!"); + if (true) throw new Error(); + } + public void main() { + System.out.println("main called!"); + } + } + """, + List.of("Constructor called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:4)")), + new TestCase(""" + public class MainClass { + static int idx; + public MainClass() { + System.out.println("Constructor called!"); + if (idx++ == 0) throw new Error(); + } + public void main(String... args) { + System.out.println("main called!"); + } + public void main() { + System.out.println("main called!"); + } + } + """, + List.of("Constructor called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:5)")), + new TestCase(""" + public class MainClass { + static { + System.out.println("static init called!"); + if (true) throw new Error(); + } + public static void main(String... args) { + System.out.println("main called!"); + } + } + """, + false, + List.of("static init called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:4)")), + new TestCase(""" + public class MainClass { + static { + System.out.println("static init called!"); + if (true) throw new Error(); + } + public static void main(String... args) { + System.out.println("main called!"); + } + } + """, + true, + List.of("static init called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:4)")), + new TestCase(""" + public class MainClass { + static { + System.out.println("static init called!"); + if (true) throw new Error(); + } + public void main(String... args) { + System.out.println("main called!"); + } + } + """, + true, + List.of("static init called!", + "Exception in thread \"main\" java.lang.Error", + "\tat MainClass.(MainClass.java:4)")), + }; + + private static void testExecutionErrors() throws Exception { + for (TestCase testCase : EXECUTION_ERRORS) { + performTest(testCase.sourceCode, testCase.enablePreview(), tr -> { + for (int i = 0; i < testCase.expectedOutput.size(); i++) { + if (i >= tr.testOutput.size() || + !Objects.equals(testCase.expectedOutput.get(i), + tr.testOutput.get(i))) { + throw new AssertionError("Unexpected output, " + + "expected: " + testCase.expectedOutput + + ", actual: " + tr.testOutput + + ", failed comparison at index: " + i); + } + } + }); + } + } + + private static void performTest(String source, boolean enablePreview, Consumer validator) throws Exception { + Path mainClass = Path.of("MainClass.java"); + Files.writeString(mainClass, source); + var version = System.getProperty("java.specification.version"); + var previewRuntime = enablePreview ? "--enable-preview" : "-DtestNoPreview"; + var previewCompile = enablePreview ? "--enable-preview" : "-XDtestNoPreview"; + var trSource = doExec(javaCmd, previewRuntime, "--source", version, "MainClass.java"); + validator.accept(trSource); + compile(previewCompile, "--source", version, "MainClass.java"); + String cp = mainClass.toAbsolutePath().getParent().toString(); + var trCompile = doExec(javaCmd, previewRuntime, "--class-path", cp, "MainClass"); + validator.accept(trCompile); + } + + public static void main(String... args) throws Exception { + testMethodOrder(); + testExecutionOrder(); + testExecutionErrors(); + } } diff --git a/test/jdk/tools/launcher/LauncherExceptionTest.java b/test/jdk/tools/launcher/LauncherExceptionTest.java new file mode 100644 index 00000000000..7981c834352 --- /dev/null +++ b/test/jdk/tools/launcher/LauncherExceptionTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, Red Hat Inc. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import java.io.File; +import java.util.ArrayList; + +/** + * @test + * @bug 8329581 + * @summary verifies launcher prints stack trace when main class can't be loaded + * @compile LauncherExceptionTest.java + * @run main LauncherExceptionTest + */ + +public class LauncherExceptionTest extends TestHelper { + + @Test + void testLauncherReportsException() throws Exception { + if (!isEnglishLocale()) { + return; + } + + File cwd = new File("."); + File srcDir = new File(cwd, "src"); + if (srcDir.exists()) { + recursiveDelete(srcDir); + } + srcDir.mkdirs(); + + // Generate class Test.java + ArrayList scratchPad = new ArrayList<>(); + scratchPad.add("public class Test {"); + scratchPad.add(" static class SomeDependency {}"); + scratchPad.add(" private static final SomeDependency X = new SomeDependency();"); + scratchPad.add(" public static void main(String... args) {"); + scratchPad.add(" System.out.println(\"X=\" + X);"); + scratchPad.add(" }"); + scratchPad.add("}"); + createFile(new File(srcDir, "Test.java"), scratchPad); + + + // Compile and execute Test should succeed + TestResult trCompilation = doExec(javacCmd, + "-d", "classes", + new File(srcDir, "Test.java").toString()); + if (!trCompilation.isOK()) { + System.err.println(trCompilation); + throw new RuntimeException("Error: compiling"); + } + + TestResult trExecution = doExec(javaCmd, "-cp", "classes", "Test"); + if (!trExecution.isOK()) { + System.err.println(trExecution); + throw new RuntimeException("Error: executing"); + } + + // Delete dependency + File dependency = new File("classes/Test$SomeDependency.class"); + recursiveDelete(dependency); + + // Executing Test should report exception description + trExecution = doExec(javaCmd, "-cp", "classes", "Test"); + trExecution.contains("Exception in thread \"main\" java.lang.NoClassDefFoundError: " + + "Test$SomeDependency"); + trExecution.contains("Caused by: java.lang.ClassNotFoundException: " + + "Test$SomeDependency"); + if (!trExecution.testStatus) + System.err.println(trExecution); + } + + public static void main(String[] args) throws Exception { + LauncherExceptionTest a = new LauncherExceptionTest(); + a.run(args); + if (testExitValue > 0) { + System.out.println("Total of " + testExitValue + " failed"); + throw new RuntimeException("Test failed"); + } else { + System.out.println("Test passed"); + } + } +}