Java程序可以不从main()方法开始吗?撸起袖子就是干!

790 阅读5分钟

Launcher

我们知道很多语言的开始函数都是main函数,java也一样,那么我们可不可以更改java的入口函数呢?让他从我们自己的方法开始,当然可以,我们可以修改一下openjdk,改变程序入口的方法名,然后重新编译即可,其实说白了这章我们简单了解一下Launcher的执行过程,Launcher就是一个用于启动JVM进程的启动器,他有两种类型,一种是正式版的启动器,也就是我们经常用到的java.exe,还有一种是javaw.exe,第一个就是控制台应用,后面的是GUI程序,Launcher负责维护JVM的整个生命周期,可想而知,了解他的执行原理是非常有意义的。

Launcher从启动到结束的过程分为6步,如下如所示。

Launcher启动后,同样先会到main()函数,main()函数运行后,会启动一个新的线程去调用JavaMain()函数,JavaMain()则负责调用InitializeJVM()去初始化JVM的相关工作。

JVM初始化完成后,Launcher接着会调用LoadClass()函数和GetStaticMethodID()函数,用于获取Java程序的启动类和启动方法,一下步就是关键,调用CallStaticVoidMethod()函数执行java程序的main()方法,所以,我们要想更改java程序的入口,将会涉及到这三个方法。

最后Launcher还会调用本地函数DetachCurrentThread0断开与主线程的连接,当成功与主线程断开连接后,Launcher会一直等待程序中所有的非守护线程全部执行结束,然后调用本地函数DestroyJavaVM()对JVM进行销毁。

JavaMain()

/openjdk11/openjdk/src/java.base/share/native/launcher/main.c下开始,兜兜转转到/openjdk11/openjdksrc\java.base\share\native\libjli\java.c下,这里就是重点,我们只需要修改GetStaticMethodID函数的参数就可以了,他的第三个参数就是方法入口名称,第四个参数是方法参数的信息,[Ljava/lang/String;)V代表String数组,方法的返回值是void。然后通过CallStaticVoidMethod去调用此方法,那么现在知道要改哪里了吗?改完之后重新make一下,如果是第一次make,时间比较慢,第二次就快了,这里我们改成newMain

外加一个知识点,这种表示形式被称为描述符,字段有字段的描述符,方法有方法的描述符,这里的L代表的是对象类型,而[代表数组,合在一起就是对象数组,V代表无返回值(void)。

int JNICALL
JavaMain(void * _args)
{
    printf("JavaMain\n");
	....
     mainClass = LoadMainClass(env, mode, what);
     
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");

    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    CHECK_EXCEPTION_NULL_LEAVE(mainID);

 
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

    LEAVE();
}

但是还有一点,只更改这里的话,那么生成的jdk虽然可从public static void newMain(String[] args)这个方法中开始运行,但是在编译(javac)的时候会报错,实际原因其实是找不到newMain()函数,但是会提示你找不到main()函数,这里是因为会设计加载不同主类导致的,在运行javac的时候,加载的类是com.sun.tools.javac.Main,然后会有一步判断这里类中main()方法是否存在,如果不存在则抛出异常,因为我们这里已经修改了名称,最后其实是因为com.sun.tools.javac.Main中没有newMain导致的,我们只要修改原来Main.java中的main方法名称即可,位于、openjdk/src/jdk.compiler/share/classes/com/sun/tools/javac下。

上面说了还有一步验证,这一步在/openjdk11/openjdk/src/java.base/share/classes/sun/launcher/LauncherHelper.java的validateMainClass()函数下。

最后来验证一下。

##>touch Test.java

##>gedit Test.java

public class Test{
	
	public static void newMain(String[] args){
		System.out.println("Hello JVM");
	}
}

##> ./build/linux-x86_64-normal-server-release/jdk/bin/javac Test.java
##> ./build/linux-x86_64-normal-server-release/jdk/bin/java Test
Hello JVM

帮助信息是从哪打印的?

在只输入java的时候,会打印如下帮助信息,那这些信息是在哪里打印的呢?

hxl@hxl-PC:~$ java
用法:java [options] <主类> [args...]
           (执行类)
   或  java [options] -jar <jar 文件> [args...]
           (执行 jar 文件)
   或  java [options] -m <模块>[/<主类>] [args...]
       java [options] --module <模块>[/<主类>] [args...]
           (执行模块中的主类)
   或  java [options] <源文件> [args]
           (执行单个源文件程序)

 将主类、源文件、-jar <jar 文件>、-m 或
 --module <模块>/<主类> 后的参数作为参数

其实还是在JavaMain()函数中,其中有下面这一段,注释意思是如果用户没有指定类名也没有指定JAR文件则走这个过程,在进入PrintUsage()方法后,将会打印这些信息,但是这些信息不是在c代码中完成的,而是在java代码中,之后LEAVE()函数会终止这个JVM进程,使其不会继续往下执行。

 /* If the user specified neither a class name nor a JAR file */
    if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
        PrintUsage(env, printXUsage);
        CHECK_EXCEPTION_LEAVE(1);
        LEAVE();
    }

在PrintUsage()函数下,会获取到sun.launcher.LauncherHelper.class这个类,然后依次调用里面的方法。

static void
PrintUsage(JNIEnv* env, jboolean doXUsage)
{
    printf("PrintUsage\n");
  jmethodID initHelp, vmSelect, vmSynonym, printHelp, printXUsageMessage;
  jstring jprogname, vm1, vm2;
  int i;
  jclass cls = GetLauncherHelperClass(env);
  NULL_CHECK(cls);
  if (doXUsage) {
    NULL_CHECK(printXUsageMessage = (*env)->GetStaticMethodID(env, cls,
                                        "printXUsageMessage", "(Z)V"));
    (*env)->CallStaticVoidMethod(env, cls, printXUsageMessage, printTo);
  } else {
  	
    //初始化帮助信息
    NULL_CHECK(initHelp = (*env)->GetStaticMethodID(env, cls,
                                        "initHelpMessage", "(Ljava/lang/String;)V"));

    NULL_CHECK(vmSelect = (*env)->GetStaticMethodID(env, cls, "appendVmSelectMessage",
                                        "(Ljava/lang/String;Ljava/lang/String;)V"));

    NULL_CHECK(vmSynonym = (*env)->GetStaticMethodID(env, cls,
                                        "appendVmSynonymMessage",
                                        "(Ljava/lang/String;Ljava/lang/String;)V"));

	//打印
    NULL_CHECK(printHelp = (*env)->GetStaticMethodID(env, cls,
                                        "printHelpMessage", "(Z)V"));

    NULL_CHECK(jprogname = (*env)->NewStringUTF(env, _program_name));

    /* Initialize the usage message with the usual preamble */
    (*env)->CallStaticVoidMethod(env, cls, initHelp, jprogname);
    CHECK_EXCEPTION_RETURN();


    for (i=1; i<knownVMsCount; i++) {
      if (knownVMs[i].flag == VM_KNOWN) {
        NULL_CHECK(vm1 =  (*env)->NewStringUTF(env, knownVMs[i].name));
        NULL_CHECK(vm2 =  (*env)->NewStringUTF(env, knownVMs[i].name+1));
        (*env)->CallStaticVoidMethod(env, cls, vmSelect, vm1, vm2);
        CHECK_EXCEPTION_RETURN();
      }
    }
    for (i=1; i<knownVMsCount; i++) {
      if (knownVMs[i].flag == VM_ALIASED_TO) {
        NULL_CHECK(vm1 =  (*env)->NewStringUTF(env, knownVMs[i].name));
        NULL_CHECK(vm2 =  (*env)->NewStringUTF(env, knownVMs[i].alias+1));
        (*env)->CallStaticVoidMethod(env, cls, vmSynonym, vm1, vm2);
        CHECK_EXCEPTION_RETURN();
      }
    }

    (*env)->CallStaticVoidMethod(env, cls, printHelp, printTo);
  }
  return;
}

LauncherHelper的源码就比较简单了,是java代码,里面借助ResourceBundle来完成, 这些信息都做了国际化处理,可以在/openjdk11/openjdk/src/java.base/share/classes/sun/launcher/resources下找到。

公众号:《码农十四式》