引子
在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:
“Debug一下吧。”
“线上机器,没开Debug端口。”
“看日志,看看请求值和返回值分别是什么?”
“那段代码没打印日志。”
“改代码,加日志,重新发布一次。”
“怀疑是线程池的问题,重启会破坏现场。”
长达几十秒的沉默之后:“据说,排查问题的最高境界,就是只通过Review代码来发现问题。”
比几十秒长几十倍的沉默之后:“我轮询了那段代码一十七遍之后,终于得出一个结论。”
“结论是?”
“我还没到达只通过Review代码就能发现问题的至高境界。”
从JSP说起
对于大多数Java程序员来说,早期的时候,都会接触到一个叫做JSP(Java Server Pages)的技术。虽然这种技术,在前后端代码分离、前后端逻辑分离、前后端组织架构分离的今天来看,已经过时了,但是其中还是有一些有意思的东西,值得拿出来说一说。
当时刚刚处于Java入门时期的我们,大多数精力似乎都放在了JSP的页面展示效果上了:
“这个表格显示的行数不对”
“原来是for循环写的有问题,改一下,刷新页面再试一遍”
“嗯,好了,表格显示没问题了,但是,登录人的姓名没取到啊,是不是Sesstion获取有问题?”
“有可能,我再改一下,一会儿再刷新试试”
……
在一遍一遍修改代码刷新浏览器页面重试的时候,我们自己也许并没有注意到一件很酷的事情:我们修改完代码,居然只是简单地刷新一遍浏览器页面,修改就生效了,整个过程并没有重启JVM。按照我们的常识,Java程序一般都是在启动时加载类文件,如果都像JSP这样修改完代码,不用重启就生效的话,那文章开头的问题就可以解决了啊:Java文件中加一段日志打印的代码,不重启就生效,既不破坏现场,又可以定位问题。忍不住试一试:修改、编译、替换class文件。额,不行,新改的代码并没有生效。那为什么偏偏JSP可以呢?让我们先来看看JSP的运行原理。
当我们打开浏览器,请求访问一个JSP文件的时候,整个过程是这样的:
[图片上传失败...(image-ad91d8-1560497185049)]
JSP文件处理过程
JSP文件修改过后,之所以能及时生效,是因为Web容器(Tomcat)会检查请求的JSP文件是否被更改过。如果发生过更改,那么就将JSP文件重新解析翻译成一个新的Sevlet类,并加载到JVM中。之后的请求,都会由这个新的Servet来处理。这里有个问题,根据Java的类加载机制,在同一个ClassLoader中,类是不允许重复的。为了绕开这个限制,Web容器每次都会创建一个新的ClassLoader实例,来加载新编译的Servlet类。之后的请求都会由这个新的Servlet来处理,这样就实现了新旧JSP的切换。
HTTP服务是无状态的,所以JSP的场景基本上都是一次性消费,这种通过创建新的ClassLoader来“替换”class的做法行得通,但是对于其他应用,比如Spring框架,即便这样做了,对象多数是单例,对于内存中已经创建好的对象,我们无法通过这种创建新的ClassLoader实例的方法来修改对象行为。
我就是想不重启应用加个日志打印,就这么难吗?
Java对象行为
既然JSP的办法行不通,那我们来看看还有没有其他的办法。仔细想想,我们会发现,文章开头的问题本质上是动态改变内存中已存在对象的行为的问题。所以,我们得先弄清楚JVM中和对象行为有关的地方在哪里,有没有更改的可能性。
我们都知道,对象使用两种东西来描述事物:行为和属性。举个例子:
public class Person{
private int age;
private String name;
public void speak(String str) {
System.out.println(str);
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
复制代码
上面Person类中age和name是属性,speak是行为。对象是类的事例,每个对象的属性都属于对象本身,但是每个对象的行为却是公共的。举个例子,比如我们现在基于Person类创建了两个对象,personA和personB:
Person personA = new Person(43, "lixunhuan");
personA.speak("我是李寻欢");