Wicket1.3中Class热加载--揭秘篇
在文章《Wicket1.3中Class热加载--使用篇》中,展示了如何使用Wicket1.3提供的ReloadingWicketFilter来动态加载修改后的类(包括修改了签名的类),从而实现高效开发。但在该文章中,只是给出了如何使用该功能的说明,而本篇文章将明确解析Wicket1.3类热加载魔法的奥秘所在。
在上一篇文章中,是通过修改Wicket项目中web.xml文件,将其中的
org.apache.wicket.protocol.http.WicketFilter
全部替换成
org.apache.wicket.protocol.http.ReloadingWicketFilter
从而开启了Wicket类热加载的功能。那么为了探究其魔法奥秘,入手点自然就选择ReloadingWicketFilter这个类了。
先来看一下ReloadingWicketFilter的代码,惊人的少:
public class ReloadingWicketFilter extends WicketFilter { private ReloadingClassLoader reloadingClassLoader;
/** * Instantiate the reloading class loader */ public ReloadingWicketFilter() { // Create a reloading classloader reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader()); }
/** * @see org.apache.wicket.protocol.http.WicketFilter#getClassLoader() */ protected ClassLoader getClassLoader() { return reloadingClassLoader; }
/** * @see org.apache.wicket.protocol.http.WicketFilter#init(javax.servlet.FilterConfig) */ public void init(final FilterConfig filterConfig) throws ServletException { reloadingClassLoader.setListener(new IChangeListener() { public void onChange() { // Remove the ModificationWatcher from the current reloading class loader reloadingClassLoader.destroy();
/* * Create a new classloader, as there is no way to clear a ClassLoader's cache. This * supposes that we don't share objects across application instances, this is almost * true, except for Wicket's Session object. */ reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader()); try { init(filterConfig); } catch (ServletException e) { Throw new RuntimeException(e); } } });
super.init(filterConfig); } }
|
其中最引人注目的就是那个ReloadingClassLoader,再打开WicketFilter类中,很容易找到以下代码,它表示使用自定义的ClassLoader来加载类。
final ClassLoader newClassLoader = getClassLoader(); Thread.currentThread().setContextClassLoader(newClassLoader); |
而ReloadingWicketFilter则是重载了getClassLoader方法,以返回了自定义的ReloadingClassLoader。也就是说Wicket的魔法其实是使用了一个自定义的ReloadingClassLoader来实现类的热加载(包括对签名被修改的类)。为了让大家更清楚理解Wicket这一方法以,在分析ReloadingClassLoader之前,先来简单的过一下JVM的类加载机制。
在JDK1.2以后,JVM在加载类时默认采用的是双亲委托机制(早期的类加载机制存在安全漏洞)。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,所有 ClassLoaders 的根都是系统 ClassLoader,它会以缺省方式装入类,即从本地文件系统加载(可能是Jar包,也可能是Class文件),如果父类加载器可以完成类加载任务,就成功返回加载后的类;但如果父类加载器无法完成此加载任务时,那么这个特定的类加载才自己去加载指定名称的类。这样的双亲委托机制可以保证象java.io.*这种基础类库的内容一定是被系统ClassLoader加载,从而保证类加载的安全性。
下面是双亲委派机制的示意图:
实例分析Web应用下的类加载顺序:
为了更好的方便大家理解类的加载机制,并说明Wicket如何使用自定义的ClassLoader来加载更改后的类,下面将有一个简单的实例来说明。
首先将前一篇文章中的HelloWorld代码修改为:
public class HelloWorld extends WicketExamplePage { /** * Constructor */ public HelloWorld() { ClassLoader classLoader = this.getClass().getClassLoader();
while (null != classLoader) { System.err.println("loader " + classLoader.hashCode()+" "+classLoader.getClass()); classLoader = classLoader.getParent(); }
add(new Label("message", "New Hello World!")); } } |
修改以后的代码,可以在对象被创建时,输出HelloWorld类的ClassLoader及其父ClassLoader,接下来恢复web.xml文件中的filter为WicketFilter,不使用RelodingWicketFilter,从而观察原先的Class加载顺序,得到的结果为:
loader 2011334 class org.apache.catalina.loader.WebappClassLoader loader 19608393 class org.apache.catalina.loader.StandardClassLoader loader 13756574 class org.apache.catalina.loader.StandardClassLoader loader 26726999 class sun.misc.Launcher$AppClassLoader loader 7494106 class sun.misc.Launcher$ExtClassLoader |
接下来仍然按照上一篇文章中的操作修改web.xml,使用RelodingWicketFilter,开启Wicket类的热加载功能。再看一下输出结果:
loader 8310913 class org.apache.wicket.application.ReloadingClassLoader loader 2011334 class org.apache.catalina.loader.WebappClassLoader loader 19608393 class org.apache.catalina.loader.StandardClassLoader loader 13756574 class org.apache.catalina.loader.StandardClassLoader loader 26726999 class sun.misc.Launcher$AppClassLoader loader 7494106 class sun.misc.Launcher$ExtClassLoader |
可见通过代码
final ClassLoader newClassLoader = getClassLoader(); Thread.currentThread().setContextClassLoader(newClassLoader); |
ReloadingWicketFilter使用ReloadingClassLoader作为当前类的ClassLoader,也就是说它接管了所有WEB-INF/classes目录下面类文件的加载。这样它就可以根据实际情况来加载一个类。但有经验的程序员都有知道,一般来说Class一旦被加载,就表示在整个JVM生命周期的过程中,不会自动释放,而是放置在内存中。那么Wicket又是怎么释放已经加载的类,同时加载修改后的类呢?看一段ReloadingWicketFilter中init方法的代码:
/** * @see org.apache.wicket.protocol.http.WicketFilter#init(javax.servlet.FilterConfig) */ public void init(final FilterConfig filterConfig) throws ServletException { reloadingClassLoader.setListener(new IChangeListener() { public void onChange() { // Remove the ModificationWatcher from the current reloading class loader reloadingClassLoader.destroy();
/* * Create a new classloader, as there is no way to clear a ClassLoader's cache. This * supposes that we don't share objects across application instances, this is almost * true, except for Wicket's Session object. */ reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader()); try { init(filterConfig); } catch (ServletException e) { Throw new RuntimeException(e); } } });
super.init(filterConfig); }
|
这段代码表示,一旦发现类文件的改变,将会销毁当前的reloadingClassLoader ,同时新建一个ReloadingClassLoader的实例,因为ClassLoader被销毁了,所以由该ClassLoader加载的类都将被销毁,然后再由新的ReloadingClassLoader进行加载,因此即使是修改了签名的类也是可以被正确加载成功的。当然这种做法,可能会引起Session中的数据不能正确识别和转换。但相对于开发环境下,开发人员通过另起一个新的Session就可以开始正常的工作了,还是可以有效的提高开发效率。
Wicket类加载的一个潜在问题:
如果仅仅使用Wicket开发程序,那么Wicket1.3引入的热加载机制,对开发人员来说,会是一件非常幸福的事情,但如果同时使用了jsp,也就是说同时在一个Web应用程序(不是Web应用服务器)中同时使用Wicket和JSP,而且在Wicket和JSP代码分别向Session中写入相同的对象,如用户信息之类的数据对象,那么就会出现一些不必要的问题,最觉的莫过于ClassCastException。虽然共用Wicket+JSP的情况比较少,出问题的机率也比较少,但还是要额外提出来作为一个警示。
下面是一个简单的类Person代码,用来展示如何出现ClassCastException:
public class Person {
/** * Default constructor */ public Person() { super();
ClassLoader classLoader = this.getClass().getClassLoader();
while (null != classLoader) { System.err.println("loader " + classLoader.hashCode()+" "+classLoader.getClass()); classLoader = classLoader.getParent(); } } } |
在构造函数中的那段代码,可以输出它的ClassLoader顺序,接下来象先前一样访问那个HelloWorld应用,得到如下的ClassLoader顺序:
loader 8310913 class org.apache.wicket.application.ReloadingClassLoader loader 3862294 class org.apache.catalina.loader.WebappClassLoader loader 19608393 class org.apache.catalina.loader.StandardClassLoader loader 13756574 class org.apache.catalina.loader.StandardClassLoader loader 26726999 class sun.misc.Launcher$AppClassLoader loader 7494106 class sun.misc.Launcher$ExtClassLoader |
然后再写一个run.jsp,代码如下:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@page import="org.apache.wicket.examples.Person"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Class Loader Demo</title> </head> <body> <% new Person(); %> </body> </html> |
在run.jsp中,初始化一个Person对象,同样观察它的输出结果,得到另外一个不同的Class加载顺序:
loader 3862294 class org.apache.catalina.loader.WebappClassLoader loader 19608393 class org.apache.catalina.loader.StandardClassLoader loader 13756574 class org.apache.catalina.loader.StandardClassLoader loader 26726999 class sun.misc.Launcher$AppClassLoader loader 7494106 class sun.misc.Launcher$ExtClassLoader |
可见在JSP中的Person与在HelloWorld中的Person是由不同的ClassLoader加载的(JSP编译成Servlet执行,在Tomcat中,org.apache.jasper.servlet.JasperLoader负责JSP编译后的类加载),根据JVM规范,这两个Class是不等价的。因此进行转换的时候,会引起ClassCastException,这是特别需要注意的一点。
点击这里下载Word格式