Java HotSwap Ⅰ-Getting Started
1.前言
本篇通过自定义的一个ClassLoader初步实现基础的HotSwap。
2.HotSwapClassLoader源代码,解释详见源代码注释
package
com.mavsplus.example.java.classloader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 一个用于Hot-Swap的classloader,继承自{ @link java.lang.ClassLoader}
*
* <pre>
* 1.Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader。
* 这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。
* 2.BootStrapClassLoader 处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包。
* ExtClassLoader 的加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载
* AppClassLoader 的加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 选型进行指定。
* 用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
* 3.一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,
* 如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常
* </pre>
*
* <pre>
* 1.每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
* 2.实现 Java 类的热替换,首先必须要实现系统中同名类的不同版本实例的共存。要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。
* 即不能把这些类的加载工作委托给系统加载器来完成,因为它们只有一份。
* 3.不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的父加载器必须设置为 null
//landon:其实也可以使用类加载器委托规则,但是前提是加载的class不能在classpath下,否则的话会优先被系统类加载器加载到
* </pre>
*
* <a href = " http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/ "></a>
*
* @author landon
* @since 1.8.0_25
*/
public class HotSwapClassLoader extends ClassLoader {
/** 加载的类文件根目录目录,存放加载的.class文件 */
private URI reloadBaseDir;
/** 加载的类名集合,Full-Name,不在此集合的类委托给系统加载器来完成 */
private Set < String > reloadClazzs;
public HotSwapClassLoader(URI loadDir) {
// 指定父加载器为null
super ( null );
this .reloadBaseDir = loadDir;
reloadClazzs = new HashSet <> ();
}
/**
* 指定加载的类
*
* <p>
* 通过根目录+Full-Name找到.class
*
* @param clazzNames
*/
public void assignLoadedClazzs(String clazzNames) {
for (String clazzName : clazzNames) {
defineClassFromPath(getLoadedClassPath(clazzName), clazzName);
}
// 添加至加载的集合
reloadClazzs.addAll(Arrays.asList(clazzNames));
}
/**
* 根据类名获取所在路径
*
* @param clazzName
* @return
*/
private Path getLoadedClassPath(String clazzName) {
String pathName = clazzName.replace( ' . ' , File.separatorChar);
String classPathName = pathName + " .class " ;
return Paths.get(reloadBaseDir).resolve(classPathName);
}
/**
* 从指定的Path接收类字节码->转换为Class实例
*
* @param path
* @param clazzFullName
* @return
*/
private Class <?> defineClassFromPath(Path path, String clazzFullName) {
File classFile = path.toFile();
int fileLength = ( int ) classFile.length();
byte [] rawBytes = new byte [fileLength];
try {
InputStream in = Files.newInputStream(path);
in.read(rawBytes);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
// Converts an array of bytes into an instance of class Class. Before
// the Class can be used it must be resolved.
return defineClass(clazzFullName, rawBytes, 0 , fileLength);
}
// Loads the class
// 本类加载器只加载reloadClazzs中的类,其余的类委托给系统类加载器进行加载
protected Class <?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class <?> clazz = null ;
// 每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间.
// 该方法即在该名字空间中寻找指定的类是否存在,如果存在旧返回给类的引用;否则就返回null;这里的直接是指存在于该类加载器的加载路径上并由该加载器完成加载,
// 间接是指由类加载器把类的加载工作委托给其他类加载器完成类的实际加载
// landon:1.因在loadClass之前,我们调用了{@link #defineClassFromPath}->通过调用{@link
// #defineClass}->已经实现了将reloadClazzs中的class的装载工作->所以如果通过loadClass方法加载的class是reloadClazzs中的,
// 则通过findLoadedClass方法可直接获取
// 2.当然还有一种做法是直接覆盖findClass方法,在该方法中调用defineClass方法.而findClass的调用时机则是在没有找到加载的类时调用的.
// 其默认实现是直接抛出一个ClassNotFoundException
clazz = findLoadedClass(name);
if ( ! this .reloadClazzs.contains(name) && clazz == null ) {
clazz = getSystemClassLoader().loadClass(name);
}
if (clazz == null ) {
throw new ClassNotFoundException(name);
}
if (resolve) {
// Links the specified class
resolveClass(clazz);
}
return clazz;
}
}
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 一个用于Hot-Swap的classloader,继承自{ @link java.lang.ClassLoader}
*
* <pre>
* 1.Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader。
* 这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。
* 2.BootStrapClassLoader 处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包。
* ExtClassLoader 的加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载
* AppClassLoader 的加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 选型进行指定。
* 用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
* 3.一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,
* 如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常
* </pre>
*
* <pre>
* 1.每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
* 2.实现 Java 类的热替换,首先必须要实现系统中同名类的不同版本实例的共存。要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。
* 即不能把这些类的加载工作委托给系统加载器来完成,因为它们只有一份。
* 3.不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的父加载器必须设置为 null
//landon:其实也可以使用类加载器委托规则,但是前提是加载的class不能在classpath下,否则的话会优先被系统类加载器加载到
* </pre>
*
* <a href = " http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/ "></a>
*
* @author landon
* @since 1.8.0_25
*/
public class HotSwapClassLoader extends ClassLoader {
/** 加载的类文件根目录目录,存放加载的.class文件 */
private URI reloadBaseDir;
/** 加载的类名集合,Full-Name,不在此集合的类委托给系统加载器来完成 */
private Set < String > reloadClazzs;
public HotSwapClassLoader(URI loadDir) {
// 指定父加载器为null
super ( null );
this .reloadBaseDir = loadDir;
reloadClazzs = new HashSet <> ();
}
/**
* 指定加载的类
*
* <p>
* 通过根目录+Full-Name找到.class
*
* @param clazzNames
*/
public void assignLoadedClazzs(String clazzNames) {
for (String clazzName : clazzNames) {
defineClassFromPath(getLoadedClassPath(clazzName), clazzName);
}
// 添加至加载的集合
reloadClazzs.addAll(Arrays.asList(clazzNames));
}
/**
* 根据类名获取所在路径
*
* @param clazzName
* @return
*/
private Path getLoadedClassPath(String clazzName) {
String pathName = clazzName.replace( ' . ' , File.separatorChar);
String classPathName = pathName + " .class " ;
return Paths.get(reloadBaseDir).resolve(classPathName);
}
/**
* 从指定的Path接收类字节码->转换为Class实例
*
* @param path
* @param clazzFullName
* @return
*/
private Class <?> defineClassFromPath(Path path, String clazzFullName) {
File classFile = path.toFile();
int fileLength = ( int ) classFile.length();
byte [] rawBytes = new byte [fileLength];
try {
InputStream in = Files.newInputStream(path);
in.read(rawBytes);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
// Converts an array of bytes into an instance of class Class. Before
// the Class can be used it must be resolved.
return defineClass(clazzFullName, rawBytes, 0 , fileLength);
}
// Loads the class
// 本类加载器只加载reloadClazzs中的类,其余的类委托给系统类加载器进行加载
// Foo的接口类IFoo加载也会调用此方法,不过其实用系统类加载器进行加载的
@Override
protected Class <?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class <?> clazz = null ;
// 每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间.
// 该方法即在该名字空间中寻找指定的类是否存在,如果存在旧返回给类的引用;否则就返回null;这里的直接是指存在于该类加载器的加载路径上并由该加载器完成加载,
// 间接是指由类加载器把类的加载工作委托给其他类加载器完成类的实际加载
// landon:1.因在loadClass之前,我们调用了{@link #defineClassFromPath}->通过调用{@link
// #defineClass}->已经实现了将reloadClazzs中的class的装载工作->所以如果通过loadClass方法加载的class是reloadClazzs中的,
// 则通过findLoadedClass方法可直接获取
// 2.当然还有一种做法是直接覆盖findClass方法,在该方法中调用defineClass方法.而findClass的调用时机则是在没有找到加载的类时调用的.
// 其默认实现是直接抛出一个ClassNotFoundException
clazz = findLoadedClass(name);
if ( ! this .reloadClazzs.contains(name) && clazz == null ) {
clazz = getSystemClassLoader().loadClass(name);
}
if (clazz == null ) {
throw new ClassNotFoundException(name);
}
if (resolve) {
// Links the specified class
resolveClass(clazz);
}
return clazz;
}
}
3.用于reload的测试类IFoo,Foo
package
com.mavsplus.example.java.classloader;
/**
* IFoo接口
*
* @author landon
* @since 1.8.0_25
*/
public interface IFoo {
public void say();
}
/**
* IFoo接口
*
* @author landon
* @since 1.8.0_25
*/
public interface IFoo {
public void say();
}
package
com.mavsplus.example.java.classloader;
/**
* 用于hotswap的类
*
* @author landon
* @since 1.8.0_25
*/
public class Foo implements IFoo {
@Override
public void say() {
// 打印了类所有的ClassLoader
System.out.println( " Hello,HotSwap.[version 1][ClassLoader- " + getClass().getClassLoader().getClass() + " ] " );
}
}
/**
* 用于hotswap的类
*
* @author landon
* @since 1.8.0_25
*/
public class Foo implements IFoo {
@Override
public void say() {
// 打印了类所有的ClassLoader
System.out.println( " Hello,HotSwap.[version 1][ClassLoader- " + getClass().getClassLoader().getClass() + " ] " );
}
}
4.入口调用类HotSwapExample,解释详见源代码注释
package
com.mavsplus.example.java.classloader;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* Hot-Swap例子
*
* @author landon
* @since 1.8.0_25
*/
public class HotSwapExample {
public static void main(String[] args) throws Exception {
HotSwapExample example = new HotSwapExample();
while ( true ) {
example.systemLoad();
example.hotswapLoad();
TimeUnit.SECONDS.sleep( 3 );
}
}
// 这里为了测试,每次调用方法的时候都会初始化一个HotSwapClassLoader对象
// 线上产品只需要在hotswap的时候初始化一个HotSwapClassLoader对象即可
public void hotswapLoad() throws Exception {
// 初始化ClassLoader,指定加载根目录及加载的类,这里通过ClassLoader.getSystemResource获取classpath所在的目录(即target\classes目录)
// 这样我们直接就可以eclipse中直接进行修改Foo,编译->下一次HotSwapClassLoader就直接会加载最新的Foo.class
HotSwapClassLoader classLoader = new HotSwapClassLoader(ClassLoader.getSystemResource( "" ).toURI());
classLoader.assignLoadedClazzs( " com.mavsplus.example.java.classloader.Foo " );
// 调用loadClass方法加载类
Class <?> clazz = classLoader.loadClass( " com.mavsplus.example.java.classloader.Foo " );
// 实例化
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod( " say " );
method.invoke(foo);
// 这样直接调用会抛出java.lang.ClassCastException,因为即使是同一个类文件,如果是由不同的类加载器实例加载的,那么他们的类型就是不同的
// clazz对象是由HotSwapClassLoader加载的,而foo2的类型生命和转型的Foo类都是由方法所属的类加载(默认是AppClassLoader)加载的,因此是
// 完全不同的类型,所以会抛出转型异常
// Foo foo2 = (Foo)clazz.newInstance();
// foo2.say();
// 这里通过接口进行调用,并没有抛出ClassCastException.因为HotSwapClassLoader只指定加载了Foo,IFoo接口的加载则会委托给系统类加载器加载
// 所以转型可成功
IFoo foo3 = (IFoo) clazz.newInstance();
foo3.say();
}
public void systemLoad() throws Exception {
Foo foo = new Foo();
foo.say();
}
}
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* Hot-Swap例子
*
* @author landon
* @since 1.8.0_25
*/
public class HotSwapExample {
public static void main(String[] args) throws Exception {
HotSwapExample example = new HotSwapExample();
while ( true ) {
example.systemLoad();
example.hotswapLoad();
TimeUnit.SECONDS.sleep( 3 );
}
}
// 这里为了测试,每次调用方法的时候都会初始化一个HotSwapClassLoader对象
// 线上产品只需要在hotswap的时候初始化一个HotSwapClassLoader对象即可
public void hotswapLoad() throws Exception {
// 初始化ClassLoader,指定加载根目录及加载的类,这里通过ClassLoader.getSystemResource获取classpath所在的目录(即target\classes目录)
// 这样我们直接就可以eclipse中直接进行修改Foo,编译->下一次HotSwapClassLoader就直接会加载最新的Foo.class
HotSwapClassLoader classLoader = new HotSwapClassLoader(ClassLoader.getSystemResource( "" ).toURI());
classLoader.assignLoadedClazzs( " com.mavsplus.example.java.classloader.Foo " );
// 调用loadClass方法加载类
Class <?> clazz = classLoader.loadClass( " com.mavsplus.example.java.classloader.Foo " );
// 实例化
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod( " say " );
method.invoke(foo);
// 这样直接调用会抛出java.lang.ClassCastException,因为即使是同一个类文件,如果是由不同的类加载器实例加载的,那么他们的类型就是不同的
// clazz对象是由HotSwapClassLoader加载的,而foo2的类型生命和转型的Foo类都是由方法所属的类加载(默认是AppClassLoader)加载的,因此是
// 完全不同的类型,所以会抛出转型异常
// Foo foo2 = (Foo)clazz.newInstance();
// foo2.say();
// 这里通过接口进行调用,并没有抛出ClassCastException.因为HotSwapClassLoader只指定加载了Foo,IFoo接口的加载则会委托给系统类加载器加载
// 所以转型可成功
IFoo foo3 = (IFoo) clazz.newInstance();
foo3.say();
}
public void systemLoad() throws Exception {
Foo foo = new Foo();
foo.say();
}
}
5.测试,控制台输出,红色部分即是我们直接修改了Foo的实现后的结果
Hello,HotSwap.[version 1
][ClassLoader
-
class
sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 3 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 3 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 2 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 1 ][ClassLoader - class sun.misc.Launcher$AppClassLoader]
Hello,HotSwap.[version 3 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
Hello,HotSwap.[version 3 ][ClassLoader - class com.mavsplus.example.java.classloader.HotSwapClassLoader]
6.总结:
本篇用一个例子讲解了基础的Java实现HotSwap的方式。后续会继续深入讲解.