我们知道很多语言的开始函数都是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
下找到。