关于java:启动一个没有-main-函数的-java-程序

9次阅读

共计 6586 个字符,预计需要花费 17 分钟才能阅读完成。

天气不错,闲来无事,所以搞点事件吧 …

作为一名 JAVA 开发者,不晓得大家有没有去想过,JAVA 程序为什么肯定要从 main 函数执行开始,其实对于这个话题,我大略从网上搜了下,其实不乏有 main 办法是咱们学习 Java 语言学习的第一个办法,也是每个 java 使用者最相熟的办法, 每个 Java 应用程序都必须有且仅有一个 main 办法 这种说法。那么真的是这样吗?明天就来聊聊这个事件。

为什么 main 函数是 java 执行入口

咱们在通过个别的 IDE 去 debug 时,main 函数的确是在堆栈的最开始中央 …

然而如果你相熟 SpringBoot 的启动过程,你会晓得,你看到的 main 函数并不是真正开始执行启动的 main 函数,对于这点,我在之前 SpringBoot 系列 -FatJar 启动原理 这篇文章中有过阐明;即便是通过 JarLaunch 启动,然而入口还是 main,只不过套了一层,而后反射去调用你利用的 main 办法。

public class JarLauncher extends ExecutableArchiveLauncher {
    // BOOT-INF/classes/
    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    // BOOT-INF/lib/
    static final String BOOT_INF_LIB = "BOOT-INF/lib/";
    // 空构造函数
    public JarLauncher() {}
    // 省略无关代码...
    // main 函数
    public static void main(String[] args) throws Exception {
        // 疏导启动入口
        new JarLauncher().launch(args);
    }
}
复制代码

这里抛开 JarLaunch 这种疏导启动的形式,单从最一般 java 程序来看,咱们来看下 main 函数作为入口的起因。

找的最开始、最边远的中央

JDK 外面的代码太多了,如果在不分明的状况上来找,那和海底捞针差不多;那咱们想一下,既然 java 要去执行 main,首先它要找到这个 main,那 main 办法是写在咱们代码外面的,所以对于 java 来说,它就不得不去先把咱们蕴含 main 办法的类加载起来。所以:

咱们找到了 LauncherHelper#checkAndLoadMain 这个上一层入口;通过这个办法的代码正文,咱们就晓得了,网上对于介绍 main 作为启动办法的一系列验证是缘起何处了:

  • 能够从 fatjar manifest 中找到启动类的 classname
  • 应用 System ClassLoader 加载这个类
  • 验证这个启动类的合法性

    • 这个类是否存在
    • 有没有 main 函数
    • 是不是 static 的
    • 是不是 public 的
    • 有没有 string 数组作为参数
  • 如果没有 main 函数,那以后的这个类是不是继承了 FX Application(要害

PS: 这里摘取一篇对于为什么是 public 的形容:JAVA 指定了一些可拜访的修饰符如:private,protected,public。每个修饰符都有它对应的权限,public 权限最大,为了阐明问题,咱们假如 main 办法是用 private 润饰的,那么 main 办法出了 Demo 这个类对外是不可见的。那么,JVM 就拜访不到 main 办法了。因而,为了保障 JVM 在任何状况下都能够拜访到 main 办法,就用 public 润饰

这个说法我集体了解是有点欠妥的,首先是 java 外面有反射机制,拜访修饰符的存在在 JVM 标准外面说的最多的是因为 平安 问题,并不是 JVM 能不能拜访的问题,因为 JVM 外面有一百种形式去拜访一个 private。

LauncherHelper 被执行调用的中央

从堆栈看,checkAndLoadMain 下层没有了,那猜想可能就是有底层 JVM(c 局部)来驱动的。持续去扒一下,在 jdk 的 java.c 文件中捞到了如下代码片段:

jclass GetLauncherHelperClass(JNIEnv *env) {if (helperClass == NULL) {
        NULL_CHECK0(helperClass = FindBootStrapClass(env,
                "sun/launcher/LauncherHelper"));
    }
    return helperClass;
}
复制代码

到这也论证了后面的猜想,的确是由底层来驱动执行的。那么既然都看到这里了,也有必要看下咱们的 JAVA 程序启动、JVM 启动过程是怎么的。

JVM 是如何驱动 JAVA 程序执行的

这里我的思路还是从能够见的代码及堆栈一层一层往下来拨的,通过 GetLauncherHelperClass 找到了 LoadMainClass,前面再找打整体启动入口。

LoadMainClass

上面是代码(代码的可读性和了解要比文字更间接):

/*
 * Loads a class and verifies that the main class is present and it is ok to
 * call it for more details refer to the java implementation.
 */
static jclass LoadMainClass(JNIEnv *env, int mode, char *name) {
    jmethodID mid;
    jstring str;
    jobject result;
    jlong start, end;
    // 去找到 LauncherHelper
    jclass cls = GetLauncherHelperClass(env);
    NULL_CHECK0(cls);
    // 依据 _JAVA_LAUNCHER_DEBUG 环境变量决策是否设置来打印 debug 信息
    if (JLI_IsTraceLauncher()) {start = CounterGet();
    }
    // 这里能够看到就是调用 LauncherHelper#checkAndLoadMain 的入口
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));
    // 创立类名的 String 对象,也就是咱们的启动类名
    str = NewPlatformString(env, name);
    // 调用动态对象办法 -> main
    result = (*env)->CallStaticObjectMethod(env, cls, mid, USE_STDERR, mode, str);
    
    if (JLI_IsTraceLauncher()) {end   = CounterGet();
        printf("%ld micro seconds to load main classn",
               (long)(jint)Counter2Micros(end-start));
        printf("----%s----n", JLDEBUG_ENV_ENTRY);
    }

    return (jclass)result;
}
复制代码

Java 程序的 Entry point

对于 C/C++ 来说,其启动入口和 java 一样,也都是 main。上面咱们略过一些无关代码,将 JAVA 程序驱动启动的外围流程代码梳理下

1、入口,main.c 的 main 办法 -> JLI_Launch

int
main(int argc, char **argv) {
    // 省略其余代码 ... 
    return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), const_jargs,
                   sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
                   FULL_VERSION,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
                   const_cpwildcard, const_javaw, const_ergo_class);
}
复制代码

2、JLI_Launch,JVM 的理论 Entry point

/*
 * Entry point.
 */
int
JLI_Launch(int argc, char ** argv,              /* main argc, argc */
        int jargc, const char** jargv,          /* java args */
        int appclassc, const char** appclassv,  /* app classpath */
        const char* fullversion,                /* full version defined */
        const char* dotversion,                 /* dot version defined */
        const char* pname,                      /* program name */
        const char* lname,                      /* launcher name */
        jboolean javaargs,                      /* JAVA_ARGS */
        jboolean cpwildcard,                    /* classpath wildcard*/
        jboolean javaw,                         /* windows-only javaw */
        jint ergo                               /* ergonomics class policy */
) {
    // 省略无关代码
    
    // main class
    char *main_class = NULL;
    // jvm 门路
    char jvmpath[MAXPATHLEN];
    // jre 门路
    char jrepath[MAXPATHLEN];
    // jvm 配置门路
    char jvmcfg[MAXPATHLEN];
    
    // 省略无关代码 ...

    // 抉择运行时 jre 的版本,会有一些规定
    SelectVersion(argc, argv, &main_class);
    // 创立执行环境,包含找到 JRE、确定 JVM 类型、初始化 jvmpath 等等
    CreateExecutionEnvironment(&argc, &argv,
                               jrepath, sizeof(jrepath),
                               jvmpath, sizeof(jvmpath),
                               jvmcfg,  sizeof(jvmcfg));
    // 省略无关代码 ...
  
    // 从 jvmpath load 一个 jvm
    if (!LoadJavaVM(jvmpath, &ifn)) {return(6);
    }
    
    // 设置 classpath
    
       // 解析参数,如 -classpath、-jar、-version、-verbose:gc .....
    if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
    {return(ret);
    }

    /* java -jar 启动的话,要笼罩 class path */
    if (mode == LM_JAR) {SetClassPath(what);
    }
    
   // 省略无关代码 ...

   //
    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
复制代码

JVMInit

JVMInit 对于不同的操作系统有不同的实现,这里以 linux 的实现为例:

int JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
        int argc, char **argv,
        int mode, char *what, int ret) {ShowSplashScreen();
    // 新线程的入口函数进行执行,新线程创立失败就在原来的线程持续反对这个函数
    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
复制代码

这里比拟深,ContinueInNewThread 外面又应用了一个 ContinueInNewThread0,从代码解释来看,大略意思是:先把以后线程阻塞,而后应用一个新的线程去执行,如果新线程创立失败就在原来的线程持续反对这个函数。外围代码:

rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
复制代码

JavaMain

1、这里第一个比拟要害的就是 InitializeJVM,初始化创立一个 Java Virtual Machine(jvm.so -> CreateJavaVM 代码比拟多,实际上真正的初始化和启动 jvm,是由 jvm.so 中的 JNI_CreateJavaVM 实现)。

2、接下来就是到咱们后面反推到的 LoadMainClass 了,找到咱们真正 java 程序的入口类,就是咱们应用程序带有 main 函数的类。

3、获取应用程序 Class -> GetApplicationClass,这里简略说下,因为和最初的那个 demo 无关,也和本文的题目无关。

 // 在某些状况下,当启动一个须要助手的应用程序时,// 例如,一个没有主办法的 JavaFX 应用程序,mainClass 将不是应用程序本人的主类,// 而是一个助手类
    appClass = GetApplicationClass(env);
复制代码

4、调用 main 函数执行利用过程启动

(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
复制代码

简略回顾

对于平时咱们常见的 java 应用程序来说,main 函数的确作为执行入口,这个是有底层 JVM 驱动执行逻辑决定。然而从整个剖析过程也能够看出,main 函数并不是惟一一种入口,那就是以非 main 入口启动的形式,也就是 JavaFX。

应用 FX Application 形式启动 java 程序

JAVA GUI 的旅程开始于 AWT,起初被一个更好的 GUI 框架所取代,其被称为 Swing。Swing 在 GUI 畛域有将近 20 年的历史。然而,它不足许多当今需要的视觉性能,其不仅要求可在多个设施上运行,还要有很好的外观和感觉。在 JAVA GUI 畛域最有前景的是 JavaFX,JAVA 自带的三个 GUI 工具包 –AWT,Swing,和 JavaFX — 它们做简直雷同的工作。而 JavaFX 应用一些不同的办法进行 GUI 编程,本文不针对 JavaFX 开展细说,有趣味的同学能够自行查阅。

每一个 JavaFX 应用程序是应用程序类的扩大,其提供了应用程序执行的入口点。一个独立的应用程序通常是通过调用这个类定义的静态方法来运行的。应用程序类定义了三个生命周期的办法:init(),start() 和 stop()。

那么联合上一节中对于启动入口的探讨,这里给出一个小 demo 来把一个 springboot 工程启动起来(基于 ide,java -jar 可能会有区别,这里未验证)

@SpringBootApplication
public class Log4j2GuidesApplication extends Application {
    // main -> mains
    public static void mains(String[] args) throws Exception {SpringApplication.run(Log4j2GuidesApplication.class, args);
        System.out.println("222");
    }
    
    @Override
    public void start(Stage stage) throws Exception {mains(new String[0]);
        System.out.println("111");
    }
}
复制代码

这里有一个有意思的状况,个别状况下,如果没有非守护线程存活(通常是 web 模块提供)时过程会在启动完之后就退出,然而这里我没有开启 web 端口,然而启动完时,过程并没有退出,即便在 start 外面抛出异样,也不能显示的去阻断,这和 JavaFX Application 的生命周期无关,后面有提到。

总结

JAVA 利用的启动不肯定是非要是 main 作为入口,对于其余的疏导启动形式没有持续调研,如果大家有晓得其余形式,也欢送留言补充。

参考:《2020 最新 Java 根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/691863…

正文完
 0