天天看点

深入探索 Java 热部署

在 java 开发领域,热部署一直是一个难以解决的问题,目前的 java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。对于某些大型的应用来说,每次的重启都需要花费大量的时间成本。虽然 osgi 架构的出现,让模块重启成为可能,但是如果模块之间有调用关系的话,这样的操作依然会让应用出现短暂的功能性休克。本文将探索如何在不破坏 java 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启就完成某个类的更新。

首先谈一下何为热部署(hotswap),热部署是在不重启 java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。java 类是通过 java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 jvm 升级埋下了一个大坑。

另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。本文将具体探索如何实现这个方案。首先需要了解一下 java 虚拟机现有的加载机制。目前的加载机制,称为双亲委派,系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。这种自上而下的加载方式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是这种方式却使加载顺序非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难题。

不过我们可以换一个思路,虽然无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。

下面来简单列举一下需要做的工作。

创建自定义的 classloader,加载需要监听改变的类,在 class 文件发生改变的时候,重新加载该类。

改变创建对象的行为,使他们在创建时使用自定义 classloader 加载的 class。

自定义加载器仍然需要执行类加载的功能。这里却存在一个问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不会变,而 classloader 只能在虚拟机停止前销毁已经加载的类,这样 classloader 就无法加载更新后的类了。这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的 class,比如加载 test.class 时,保存在内存中的类是 test_v1.class,当类发生改变时,重新加载的类名是 test_v2.class。但是真正执行加载 class 文件创建 class 的 defineclass 方法是一个 native 的方法,修改起来又变得很困难。所以面前还剩一条路,那就是直接修改编译生成的 class 文件。

可以修改字节码的框架有很多,比如 asm,cglib。本文使用的是 asm。先来介绍一下 class 文件的结构,class 文件包含了以下几类信息,一个是类的基本信息,包含了访问权限信息,类名信息,父类信息,接口信息。第二个是类的变量信息。第三个是方法的信息。asm 会先加载一个 class 文件,然后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新的 class。

首先看一下如何利用 asm 修改类信息。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<code>classwriter cw =</code> <code>new</code> <code>classwriter(classwriter.compute_maxs);</code>

<code>       </code><code>classreader cr =</code> <code>null</code><code>;    </code>

<code>       </code><code>string enhancedclassname = classsource.getenhancedname();</code>

<code>       </code><code>try</code> <code>{</code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code><code>new</code> <code>fileinputstream(</code>

<code>                   </code><code>classsource.getfile()));</code>

<code>       </code><code>}</code> <code>catch</code> <code>(ioexception e) {</code>

<code>           </code><code>e.printstacktrace();</code>

<code>           </code><code>return</code> <code>null</code><code>;</code>

<code>       </code><code>}</code>

<code>       </code><code>classvisitor cv =</code> <code>new</code> <code>enhancedmodifier(cw,</code>

<code>               </code><code>classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>               </code><code>enhancedclassname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>));</code>

<code>       </code><code>cr.accept(cv,</code> <code>0</code><code>);</code>

asm 修改字节码文件的流程是一个责任链模式,首先使用一个 classreader 读入字节码,然后利用 classvisitor 做个性化的修改,最后利用 classwriter 输出修改后的字节码。

之前提过,需要将读取的 class 文件的类名做一些修改,加载成一个全新名字的派生类。这里将之分为了 2 个步骤。

第一步,先将原来的类变成接口。

15

16

17

18

<code>   </code><code>public</code> <code>class&lt;?&gt; redefineclass(string classname){</code>

<code>       </code><code>classwriter cw =</code> <code>new</code> <code>classwriter(classwriter.compute_maxs);</code>

<code>       </code><code>classreader cr =</code> <code>null</code><code>;</code>

<code>       </code><code>classsource cs = classfiles.get(classname);</code>

<code>       </code><code>if</code><code>(cs==</code><code>null</code><code>){</code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code><code>new</code> <code>fileinputstream(cs.getfile()));</code>

<code>       </code><code>classmodifier cm =</code> <code>new</code> <code>classmodifier(cw);</code>

<code>       </code><code>cr.accept(cm,</code> <code>0</code><code>);</code>

<code>       </code><code>byte</code><code>[] code = cw.tobytearray();</code>

<code>       </code><code>return</code> <code>defineclass(classname, code,</code> <code>0</code><code>, code.length);</code>

<code>}</code>

首先 load 原始类的 class 文件,此处定义了一个增强组件 <code>classmodifier</code>,作用是修改原始类的类型,将它转换成接口。原始类的所有方法逻辑都会被去掉。

第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。这样做的目的是不论如何修改,同一个 class 的派生类都有一个共同的接口,他们之间的转换变得对外不透明。

19

20

21

22

23

24

<code>   </code><code>// 在 class 文件发生改变时重新定义这个类</code>

<code>   </code><code>private</code> <code>class&lt;?&gt; redefineclass(string classname, classsource classsource){</code>

<code>       </code><code>classsource.update();</code>

<code>       </code><code>string enhancedclassname = classsource.getenhancedname();      </code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code>

<code>                   </code><code>new</code> <code>fileinputstream(classsource.getfile()));</code>

<code>       </code><code>enhancedmodifier em =</code> <code>new</code> <code>enhancedmodifier(cw, classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>       </code><code>extendmodifier exm =</code> <code>new</code> <code>extendmodifier(em, classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>       </code><code>cr.accept(exm,</code> <code>0</code><code>);</code>

<code>       </code><code>classsource.setbytecopy(code);</code>

<code>       </code><code>class&lt;?&gt; clazz = defineclass(enhancedclassname, code,</code> <code>0</code><code>, code.length);</code>

<code>       </code><code>classsource.setclasscopy(clazz);</code>

<code>       </code><code>return</code> <code>clazz;</code>

再次 load 原始类的 class 文件,此处定义了两个增强组件,一个是 <code>enhancedmodifier</code>,这个增强组件的作用是改变原有的类名。第二个增强组件是 <code>extendmodifier</code>,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。

自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。

java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。

由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。

对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。

<code>// 原始逻辑  </code>

<code>  </code><code>greeter p =</code> <code>new</code> <code>greeter();</code>

<code>// 改变后的逻辑</code>

<code>  </code><code>igreeter p = (igreeter)myclassloader.getinstance().</code>

<code>  </code><code>findclass(</code><code>"com.example.greeter"</code><code>).newinstance();</code>

这里又需要用到 asm 来修改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。

25

26

<code>@override</code>

<code>public</code> <code>void</code> <code>visittypeinsn(</code><code>int</code> <code>opcode, string type) {</code>

<code>    </code><code>if</code><code>(opcode==opcodes.new &amp;&amp; type.equals(classname)){</code>

<code>        </code><code>list&lt;localvariablenode&gt; variables = node.localvariables;</code>

<code>        </code><code>string compiletype =</code> <code>null</code><code>;</code>

<code>        </code><code>for</code><code>(</code><code>int</code> <code>i=</code><code>0</code><code>;i&lt;variables.size();i++){</code>

<code>            </code><code>localvariablenode localvariable = variables.get(i);</code>

<code>            </code><code>compiletype = formtype(localvariable.desc);</code>

<code>            </code><code>if</code><code>(matchtype(compiletype)&amp;&amp;!valiableindexused[i]){</code>

<code>                </code><code>valiableindexused[i] =</code> <code>true</code><code>;</code>

<code>                </code><code>break</code><code>;</code>

<code>            </code><code>}</code>

<code>        </code><code>}</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokestatic, classload_type,</code>

<code>        </code><code>"getinstance"</code><code>,</code> <code>"()l"</code><code>+classload_type+</code><code>";"</code><code>);</code>

<code>    </code><code>mv.visitldcinsn(type.replace(</code><code>"/"</code><code>,</code> <code>"."</code><code>));</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokevirtual, classload_type,</code>

<code>        </code><code>"findclass"</code><code>,</code> <code>"(ljava/lang/string;)ljava/lang/class;"</code><code>);</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokevirtual,</code> <code>"java/lang/class"</code><code>,</code>

<code>        </code><code>"newinstance"</code><code>,</code> <code>"()ljava/lang/object;"</code><code>);</code>

<code>    </code><code>mv.visittypeinsn(opcodes.checkcast, compiletype);</code>

<code>    </code><code>flag =</code> <code>true</code><code>;</code>

<code>    </code><code>}</code> <code>else</code> <code>{</code>

<code>        </code><code>mv.visittypeinsn(opcode, type);</code>

<code>    </code><code>}</code>

<code> </code><code>}</code>

对于第二种创建方法,需要通过修改 <code>class.forname()</code>和 <code>classloader.findclass()</code>的行为,使他们通过自定义加载器加载类。

之前实现的类加载器已经解决了热部署所需要的功能,可是 jvm 启动时,并不会用自定义的加载器加载 classpath 下的所有 class 文件,取而代之的是通过应用加载器去加载。如果在其之后用自定义加载器重新加载已经加载的 class,有可能会出现 linkageerror 的 exception。所以必须在应用启动之前,重新替换已经加载的 class。如果在 jdk1.4 之前,能使用的方法只有一种,改变 jdk 中 classloader 的加载行为,使它指向自定义加载器的加载行为。好在 jdk5.0 之后,我们有了另一种侵略性更小的办法,这就是 javaagent 方法,javaagent 可以在 jvm 启动之后,应用启动之前的短暂间隙,提供空间给用户做一些特殊行为。比较常见的应用,是利用 javaagent 做面向方面的编程,在方法间加入监控日志等。

javaagent 的实现很容易,只要在一个类里面,定义一个 premain 的方法。

<code>public</code> <code>class</code> <code>reloadagent {</code>

<code>   </code><code>public</code> <code>static</code> <code>void</code> <code>premain(string agentargs, instrumentation inst){</code>

<code>       </code><code>generaltransformer trans =</code> <code>new</code> <code>generaltransformer();</code>

<code>       </code><code>inst.addtransformer(trans);</code>

<code>   </code><code>}</code>

然后编写一个 manifest 文件,将 <code>premain-class</code>属性设置成定义一个拥有 <code>premain</code>方法的类名即可。

生成一个包含这个 manifest 文件的 jar 包。

<code>manifest-version: 1.0</code>

<code> </code><code>premain-class: com.example.reloadagent</code>

<code> </code><code>can-redefine-classes: true</code>

最后需要在执行应用的参数中增加 <code>-javaagent</code>参数 , 加入这个 jar。同时可以为 <code>javaagent</code>增加参数,下图中的参数是测试代码中 test project 的绝对路径。这样在执行应用的之前,会优先执行 <code>premain</code>方法中的逻辑,并且预解析需要加载的 class。

深入探索 Java 热部署

这里利用 <code>javaagent</code>替换原始字节码,阻止原始字节码被 java 虚拟机加载。只需要实现 <code>一个 classfiletransformer</code>的接口,利用这个实现类完成 class 替换的功能。

<code>public</code> <code>byte</code> <code>[] transform(classloader paramclassloader, string paramstring,</code>

<code>     </code><code>class&lt;?&gt; paramclass, protectiondomain paramprotectiondomain,</code>

<code>     </code><code>byte</code> <code>[] paramarrayofbyte)</code> <code>throws</code> <code>illegalclassformatexception {</code>

<code>    </code><code>string classname = paramstring.replace(</code><code>"/"</code><code>,</code> <code>"."</code><code>);</code>

<code>    </code><code>if</code><code>(classname.equals(</code><code>"com.example.test"</code><code>)){</code>

<code>        </code><code>myclassloader cl = myclassloader.getinstance();</code>

<code>        </code><code>cl.definereference(classname,</code> <code>"com.example.greeter"</code><code>);</code>

<code>        </code><code>return</code> <code>cl.getbytecode(classname);</code>

<code>    </code><code>}</code><code>else</code> <code>if</code><code>(classname.equals(</code><code>"com.example.greeter"</code><code>)){</code>

<code>        </code><code>cl.redefineclass(classname);</code>

<code>    </code><code>return</code> <code>null</code><code>;</code>

至此,所有的工作大功告成,欣赏一下 hotswap 的结果吧。

解决 hotswap 是个困难的课题,本文解决的仅仅是让新实例化的对象使用新的逻辑,并不能改变已经实例化对象的行为,如果 jvm 能够重新设计 class 的生命周期,支持运行时重新更新一个 class,hotswap 就会成为 java 的一个闪亮新特性。官方的 jvm 一直没有解决热部署这个问题,可能也是由于无法完全克服其中的诸多难点,希望未来的 jdk 能解决这个问题,让 java 应用对于更新更友好,避免不断重启应用浪费的时间。

特别说明:尊重作者的劳动成果,转载请注明出处哦~~~http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt231