网易首页 > 网易号 > 正文 申请入驻

启动一个没有 main 函数的 java 程序

0
分享至

  作为一名 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 class\n", (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

  intmain(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. */intJLI_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 可能会有区别,这里未验证)

  @SpringBootApplicationpublic 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 作为入口,关于其他的引导启动方式没有继续调研,如果大家有知道其他方式,也欢迎留言补充。

  

作者|glmapper|掘金

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐

美媒:“最高机密”文件显示,沙特王储与卡舒吉被杀案有关

环球网资讯
2021-02-25 14:13:11

欧文提议NBA换科比作LOGO?杰里-韦斯特:非常乐意让出此殊荣

直播吧
2021-02-25 08:30:04

3月1日起,“囤房族”或全面消失?新规出台,楼市或迎来拐点

你的置业顾问
2021-02-25 01:27:22

原材料疯涨!新一轮“涨价潮”真的要来?

大照明
2021-02-25 10:12:34

33岁女大学生在家啃老十年:你们剪掉了我的翅膀,却怪我不会飞翔

寄予好音乐
2021-02-24 11:22:05

中国平安被指传销式展业,退保率远超同行水平

财经天下周刊
2021-02-25 12:57:16

潘晓婷39岁生日晒美照,肤白貌美,身材圆润,像20多岁的小姑娘!

体育全网通
2021-02-25 17:42:29

重压之下中国足协终于无奈妥协!球迷直言足协早就该这么做了

杨君昊爱娱乐
2021-02-25 14:51:32

起底孟加拉妓女村:1600个女孩,每天要接待3000名嫖客

世界华人周刊
2021-02-24 09:34:31

美媒发布全球军力报告:俄罗斯反超中国跃居第二,印度排名上升

海峡新讯
2021-02-23 14:12:10

不瞒你说,灵活运用它可以提高性和谐度

第十一诊室
2021-02-25 10:31:24

我42岁,老婆38岁,她为我手下一个24岁员工向我提离婚

我是木子李
2021-02-22 00:35:19

古巴女孩恋上云南小伙,放弃读大学追到中国,如今两人已结婚

尘缘亦绝
2021-02-24 15:53:21

苏宁易购回应转让:今后或将转身成为一家国有控股企业

虎扑足球
2021-02-25 16:30:03

上架就售空,可惜真正识货却没有几人?这三款手机的用料是真的猛

权威科技控
2021-02-25 18:49:34

老婆跟她领导去外地出差,还给我发了张自拍照,看完我泪流满面!

爱笑的白羊座
2021-02-24 10:32:40

美国人最喜欢的总统是谁?最新的调查告诉你

未来就来
2021-02-24 07:01:50

郑爽事件新进展:张恒已胜诉,有望携孩子回国,郑爽却极力阻挠!

苏苏情感
2021-02-24 20:40:30

新华社发长文点评贾浅浅后,贾浅浅入职浙大当教授?浙大风评被害

大琦说教育
2021-02-25 16:03:31

故事:一个女大学生被包养的全过程,结局太凄惨

后殇忘川
2021-02-24 12:09:28
2021-02-26 07:25:03
自驾游推荐
自驾游推荐
给你分享不一样的自驾游
422文章数 20103关注度
往期回顾 全部

科技要闻

复盘,苏宁为什么没干过京东

头条要闻

新中国最大银行贪污案:贪40亿 为绿卡妻子与外国人假结婚

头条要闻

新中国最大银行贪污案:贪40亿 为绿卡妻子与外国人假结婚

体育要闻

今日趣图:我,我回来了

娱乐要闻

文咏珊装扮清凉小秀玲珑身材

财经要闻

汽车要闻

小号Mach-E?福特纯电新SUV用大众平台

态度原创

旅游
房产
本地
教育
军事航空

旅游要闻

住600年古堡 这个妹子的生活有点妙

房产要闻

深圳部分银行跟进 “挂钩”二手房成交参考价

本地新闻

你分清爱情和婚姻的区别了吗?爱情最重要的是...

教育要闻

目前教育最疯狂的不是学校,不是老师,而是家长!

军事要闻

美国空军最高勋章竟是一把大宝剑!