一 热部署原理

  • 众所周知ava是静态的强类型语言,一旦编译好Class文件后,那程序就会按照编译好的Class文件执行,变量、 类型、逻辑将都无法更改。如果需要进行逻辑修改,我们必须要重新编码、编译、替换原始Class,并重新启动程序,这对于我们一般无状态服务是没有问题的,但是对于基础数据服务却是非常的麻烦。
  • 以Solr Cloud为例,如果我们需要更新自定义的Solr分词器,在没有热部署的情况下,我们必须替换Solr集群的依赖Solr plugin jar,并重启整个集群。这样的话,就会非常的耗时,也存在风险。目前热部署里面比较成熟的框架ASM,我们今天暂时不讨论这个方向,为了研究Solr的热部署原理,我们从比较简单的ClassLoader出发。

1.1 分析问题的实质

所谓热部署就是要在程序运行的时候,我们需要替换其在内存里的Class文件(或者说Class对象),阅读java.lang.Classloader源码可以知道,Class对象由加载此Class文件的ClassLoader持有。如果想要替换内存中的Class对象,我们就必须知道Class是如何从磁盘加载至内存中的。

  1. 从loadClass方法可以知道,获取Class对象的过程:
  2. 从本ClassLoad分配的元空间内存中获取Class对象
  3. 如果没有load过,则分配父的ClassLoader进行加载class
  4. 如果没有父ClassLoader,则说明此ClassLoader是顶层的BootstrapClassLoader,则使用findBootstrapClass(native方法)查找Class对象。
  5. 如果经过2或者3步骤class仍然为null,则执行本ClassLoader的findClass方法(native方法),一般从磁盘的jar中查找。
  6. 如果找到class,jvm则负责注册至元空间,并返回此Class对象
  /**
    * Loads the class with the specified <a href="#name">binary name</a>.  The
    * default implementation of this method searches for classes in the
    * following order:
    *
    * <ol>
    *
    *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
    *   has already been loaded.  </p></li>
    *
    *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
    *   on the parent class loader.  If the parent is <tt>null</tt> the class
    *   loader built-in to the virtual machine is used, instead.  </p></li>
    *
    *   <li><p> Invoke the {@link #findClass(String)} method to find the
    *   class.  </p></li>
    *
    * </ol>
    *
    * <p> If the class was found using the above steps, and the
    * <tt>resolve</tt> flag is true, this method will then invoke the {@link
    * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
    *
    * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
    * #findClass(String)}, rather than this method.  </p>
    *
    * <p> Unless overridden, this method synchronizes on the result of
    * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
    * during the entire class loading process.
    *
    * @param  name
    *         The <a href="#name">binary name</a> of the class
    *
    * @param  resolve
    *         If <tt>true</tt> then resolve the class
    *
    * @return  The resulting <tt>Class</tt> object
    *
    * @throws  ClassNotFoundException
    *          If the class could not be found
    */
   protected Class<?> loadClass(String name, boolean resolve)
       throws ClassNotFoundException
   {
       synchronized (getClassLoadingLock(name)) {
           // First, check if the class has already been loaded
           Class<?> c = findLoadedClass(name);
           if (c == null) {
               long t0 = System.nanoTime();
               try {
                   if (parent != null) {
                       c = parent.loadClass(name, false);
                   } else {
                       c = findBootstrapClassOrNull(name);
                   }
               } catch (ClassNotFoundException e) {
                   // ClassNotFoundException thrown if class not found
                   // from the non-null parent class loader
               }

               if (c == null) {
                   // If still not found, then invoke findClass in order
                   // to find the class.
                   long t1 = System.nanoTime();
                   c = findClass(name);

                   // this is the defining class loader; record the stats
                   sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                   sun.misc.PerfCounter.getFindClasses().increment();
               }
           }
           if (resolve) {
               resolveClass(c);
           }
           return c;
       }
   }

1.2 如何才能替换内存里的Class对象

结合JVM知识可以知道,Class对象存放于元空间中,不会被GC和清除掉,除非加载此Classloader的,ClassLoader中Vector<Class<?>> classes源码注释也可以知道,这个classes是为了防止Class对象被GC直到这个ClassLoader的被GC。

// The classes loaded by this class loader. The only purpose of this table
// is to keep the classes from being GC'ed until the loader is GC'ed.
private final Vector<Class<?>> classes = new Vector<>();


// Invoked by the VM to record every loaded class with this loader.
void addClass(Class<?> c) {
    classes.addElement(c);
}

其实从这里我们就可以知道,除字节码修改框架的另外一种热部署的方式,直接将整个ClassLoader GC掉,然后更新Jar,重新从磁盘loader新的class。

二 Solr的热部署方案

2.1 Solr自定义ClassLoader

从上面解读的ClassLoader源码中可以知道Classloader load class采用的是双亲委托机制。我们一般的Java程序都是有三层ClassLoader(这里说的三层ClassLoader并不是指的三个ClassLoader的实现类,而是三个ClassLoader对象):

  • BootStrapClassLoader: 顶层Class Loader, 用于加载jre/ib
  • ExtClassLoader:次一级Class Loader, 用子加載ire/lib/ext
  • AppClassloader:进程级ClassLoader,用于加载Java进程启动时定义的用户ClassPath
  • Solr还有额外的一层ClassLoader,resourceClassLoader,此ClassLoader被SolrCore对象所持有,通过阅读SolrCore源码可以知道,此对象在创建Plugin时用来加载额外的Jar包和资源。
public PluginHolder<T> createPlugin(PluginInfo info) {
    if ("true".equals(String.valueOf(info.attributes.get("runtimeLib")))) {
      log.debug(" {} : '{}'  created with runtimeLib=true ", meta.getCleanTag(), info.name);
      LazyPluginHolder<T> holder = new LazyPluginHolder<>(meta, info, core, "true".equals(System.getProperty("enable.runtime.lib")) ?
          core.getMemClassLoader() :
          core.getResourceLoader(), true); //使用memClassLoader也是resource loader

      return meta.clazz == UpdateRequestProcessorFactory.class ?
          (PluginHolder<T>) new UpdateRequestProcessorChain.LazyUpdateProcessorFactoryHolder(holder) :
          holder;
    } else if ("lazy".equals(info.attributes.get("startup")) && meta.options.contains(SolrConfig.PluginOpts.LAZY)) {
      log.debug("{} : '{}' created with startup=lazy ", meta.getCleanTag(), info.name);
      return new LazyPluginHolder<T>(meta, info, core, core.getResourceLoader(), false);
    } else {
      T inst = core.createInstance(info.className, (Class<T>) meta.clazz, meta.getCleanTag(), null, core.getResourceLoader());
      initInstance(inst, info);
      return new PluginHolder<>(info, inst);
    }
  }

2.2 Solr热加载Class的方式

通过阅读org.apache.solr.core.CoreContainer.reload(String)方法源码可以知道,Solr在进行core reload时,会将core所持有的所有资源进行关闭并且重新初始化,这里就包含resourceClassLoader,所有的插件的对象实例和对应的Class都会被关闭,并且重新初始化,这就是Solr Plugin热部署的原理。

reload方法中会重新打开新的classLoader

ConfigSet coreConfig = coreConfigService.getConfig(cd); //reload方法中会重新创建新的Solr Resource ClassLoader 见源代码

org.apache.solr.core.ConfigSetService.getConfig(CoreDescriptor):

SolrResourceLoader coreLoader = createCoreResourceLoader(dcore);


@Override
public SolrResourceLoader createCoreResourceLoader(CoreDescriptor cd) {
      Path instanceDir = locateInstanceDir(cd);
      return new SolrResourceLoader(instanceDir, parentLoader.getClassLoader(), cd.getSubstitutableProperties());
}

三、代码片段

阅读了Solr源码中热部署加载Solr Plugin的jar,我们也可以自己动手实现Service的热部署,我们可以自定义一个MyClassLoader,利用这个ClassLoader去加载制定目录的jar包即可。见代码:

package com.cheng.jungao.test;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader {
	//使用URLClassLoader从磁盘加载jar
	private URLClassLoader extClassLoader;
	
	public MyClassLoader (String extClassPath) throws MalformedURLException {
		this.extClassLoader = new URLClassLoader(getJars (extClassPath),this.getClass().getClassLoader() );
	}
	
	//通过类名使用无参构造器实例化需要热部署的class的对象
	@SuppressWarnings("unchecked")
	public <T>T getInstance (String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException {
		Class<T> clazz =  (Class<T>) extClassLoader.loadClass (className);
		return (T) clazz.getConstructor();
	}
	
	//reloader extClassLoader,实现Class更新
	public boolean reload(String extClassPath) throws IOException {
		URLClassLoader newExtClassLoader = new URLClassLoader(getJars (extClassPath), this.getClass().getClassLoader() );
		URLClassLoader oldClassLoader = extClassLoader;
		extClassLoader = newExtClassLoader;
		oldClassLoader.close ();
		return true;
	}
	
	@SuppressWarnings ("deprecation")
	private URL[] getJars (String extClassPath) throws MalformedURLException {
		File dir = new File (extClassPath);
		File[] jars = dir.listFiles ();
		URL[] urls = new URL[jars.length];
		for (int i = 0; i < jars.length; i++) {
			urls[i] = jars[i].toURL();
		}
		return urls;
	}
}