本文主要是根据classloader的特性,结合实际产品环境中遇到的问题,来探讨下JAVA应用中局部模块热部署的可行性。
我们知道,一些web应用提供了自动检测装载webapp的功能,但大部分的时候,就是相当于重新启动了一遍Webapp,存储在内存中的数据也会丢失,并不能灵活地满足需要。而OSGI框架,虽然也提供了模块的热部署,但为了用热部署而将应用限制在OSGI的框框中,有些时候得不偿失。于是想根据实际需要来定制classloader,灵活地指定哪些类重载,哪些类不需要。
言归正传,进行我们的实践,这里先简单介绍下JAVA的classloader机制:
从上图可以看出虚拟机中的Classloader的层次结构, 由最外层的Classloader去load全限定名指定的class,如果load不到,则委托给该classloader的父classloader,直至到root classloader。
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
这里我们的自定义Classloader可以通过重载loadClass方法,仅对指定package下的类进行加载,其余全部委托父Classloader来加载 , 这里需要用到classloader的defineClass方法,以便我们的classloader可以载入任意指定位置的class文件。
public class MyClassLoader extends ClassLoader{
public static ConcurrentHashMap> classes = new ConcurrentHashMap>();
public static MqClassLoader instance = new MyClassLoader();
//构造自定义Classloader, 并指定父Classloader
public MyClassLoader() {
super(Thread.currentThread().getContextClassLoader());
}
public Class> load(String name, byte[] data, boolean resolve) {
Class> klass = defineClass(name, data, 0, data.length);
if (resolve)
resolveClass(klass);
classes.put(name, klass);
return klass;
}
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Object value = classes.get(name); // 检查缓存
if (value != null && value != INVALID) {
Class> klass = (Class>) value;
if (resolve)
resolveClass(klass);
return klass;
} else { // 缓存中不存在
byte[] data = read(findClassFile(name)); // 读取类文件
if (data == null)
return super.loadClass(name, resolve); // 交由父classloader去load类文件
else {
try {
lock.lock();
Object cc = classes.get(name); // 检查缓存
if (cc != null) {
return (Class>) cc;
} else
return instance.load(name, data, resolve); // 自己load类文件
} finally {
lock.unlock();
}
}
}
}
}
上面的代码描述了自定义classloader的载入逻辑,findClassFile() 就是自己定义的从哪里找需要的类文件方法。
defineClass 方法可以灵活地用来实现分布式动态计算,hadoop mapreduce应该就是使用了这一方法来保证服务器集群间的处理类的传输和运行。
/**
* 重新初始化,以便实现重载所指定的类
*/
public static void reset() {
instance = new MyClassLoader();
classes.clear();
}
其实每次reset都会产生一个新的classloader实例, 该实例会在所有这个实例装载的的类全部被回收后才被回收,这里比较奔放的全部reset掉,经过测试尚未发现内存溢出问题。
调用这些类的方法:
public static void invoke(String method ,Object[] obj, Class>[] parameterTypes){
try {
Object cls = MyClassLoader.instance.loadClass("类全限定名", true).newInstance();
cls.getClass().getMethod(method,parameterTypes).invoke(cls ,obj);
} catch (Exception e) {
logger.error("reloadable error " + method, e);
}
}
这里只能通过反射的方式调用,由于classloader的安全机制,同样的类,父classloader装载的类 和 子classloader装载的类不能互相转换。
需要强调的是,被重载的类重载后所有的变量都会被重新初始化,因此一些重要数据变量还是得交由父Classloader来管理。
该方式存在一些缺点,把核心逻辑归并到一起,将变量分离出去,从而影响了应用本身的结构。
目前这种方式正应用在一个消息服务中,主要避免由于一些微小的改动而重新启动服务,重启消息服务这对于大型系统来说是很麻烦的 ,是否值得还很难说,权当对java的深入学习吧。