一开始写java,所有demo一定是写一个main方法。后面你发现这个main方法其实和普通方法也没什么区别,可以被其他类的方法调用。而且你可以在多个java文件里写main方法,只不过是需要你指定main类,java也可以根据你指定的类找到main方法执行。
基于上面的疑问,我们去虚拟机中找找答案。
虚拟机的实现
下面是直接从openjdk中获取的代码,你会发现,他的方式竟然是jni的调用。
mainClass = LoadMainClass(env, mode, what);
CHECK_EXCEPTION_NULL_LEAVE(mainClass);
appClass = GetApplicationClass(env);
NULL_CHECK_RETURN_VALUE(appClass, -1);
PostJVMInit(env, appClass, vm);
CHECK_EXCEPTION_LEAVE(1);
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
CHECK_EXCEPTION_NULL_LEAVE(mainID);
mainArgs = CreateApplicationArgs(env, argv, argc);
CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
为什么java的main方式必须声明成如下的模式
public static void main(String[] args){}
jvm中写的比较明了,通过jni,获取一个名字是main,返回值是void,方法参数是String[](这里的部分都是从方法签名中解读([Ljava/lang/String;)V)。
可以理解这是jvm硬编码的结果,如果把openjdk中的代码改成查找main1。再编译出来的jdk,就只会去找class文件中的main1方法了。
硬编码的风险
大家虽然都这么使用,但是这么硬编码也是带来了风险,这里的public static void main(String[] args)相当于一个约定,如果出现了约定不满足使用条件的情况,给迁移就会带来很大的风险,而且还要保证新旧的兼容性,以及特定版本要求以上的硬条件。
在应用层,这个缺点更明显,例如用asm等字节码工具做bci注入,实现的任何细节改变都会导致bci的失败。例如java中的http的注入
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
包括自己写servlet的时候都是继承这个类,去覆盖里面的doXX方法。如果有特殊情况改了方法。那么整体的都无法使用了。
我们在使用混淆工具的时候,会特别注意string常量,如果是用在反射等情况下,这个都是混淆失败的,得做特殊处理,我们常用的混淆工具,是可以处理class.forname这种的操作的,但是无法混淆classloader.loadClass。
以上的情况都是硬编码带来的麻烦。
硬编码的好处
硬编码在编程上似乎没什么好处,但是在使用中确实是一个好处。例如做加密,解密方式不可配置,硬编码成一种,这个带来的好处就是以前的版本他都可以不认可。例如做在密钥检测里,生成密钥的方式都变了,解密的方式也一起变,很快就能阻止有人破解的方式。如果不是硬编码的,在破解起来就很方便了。往往大家花时间反编译找出了校验之处,如果可以配置,直接改个配置就可以运行软件了。
小结
- jvm为什么认可 public static void main(String[] args),主要是jvm中jni硬编码调用导致。只要修改掉这里的jni调用。就等于换了一个规范。
- 硬编码的坏处就是不能完美衔接改变,如果是定义在协议上,改动就得做到新旧兼容,否则就出现强制升级的情况,影响范围变大。
- 硬编码的好处就是稳固。使用在校验,编解码等上面可以防止破解和过期的接入。
共同学习,写下你的评论
评论加载中...
作者其他优质文章