在文章《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格式