一、类加载过程
类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。一个类可以有多个实例,但是只有一个对应的Class对象。
类加载器不止有一个,一般程序运行时,会有三个ClassLoader,分别是:
- 启动类加载器(Bootstrap ClassLoader)—— 由Java虚拟机创建,负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar;
- 扩展类加载器(Extension ClassLoader)—— 对应sun.misc.Launcher$ExtClassLoader,负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext;
- 应用类加载器(Application ClassLoader)—— 对应sun/misc.Launcher$AppClassLoader,负责加载应用程序的类。
这三个类加载器虽然不是父子继承关系,但是存在父子委派关系,即子ClassLoader委托父ClassLoader优先加载,加载失败后再自己尝试加载。比如AppClassLoader加载类时会优先通过parent变量指向的ExtClassLoader加载,ExtClassLoader又委派Bootstrap ClassLoader加载。这种“双亲委派”机制能避免Java类库被覆盖的问题。
ClassLoader主要通过loadClass方法加载类:
/** * 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: */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,检查是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { 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) { //如果依然找不到,调用findClass方法去加载,自定义ClassLoader一般需要重写此方法 c = findClass(name); } } return c; } }
二、自定义ClassLoader
Java类加载机制的强大之处在于,我们可以自定义ClassLoader,也就是按照自己的逻辑寻找.class字节码文件,并生成Class对象。这在发挥Java语言的动态性上起着非常重要的作用。
通过上面的例子知道,ClassLoader的最主要方法是loadClass(),loadClass方法找不到类后最后会调用findClass(name),这是自定义ClassLoader需要重写的主要方法,可以仿照如下方式重写:
public class MyClassLoader extends ClassLoader { private static final String BASE_DIR = "data/c87/"; @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String fileName = name.replaceAll("\.", "/"); fileName = BASE_DIR + fileName + ".class"; //得到字节码文件完整路径 try { /** * 首先将.class文件转化为二进制字节流 */ FileInputStream is = new FileInputStream(fileName); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); } } catch (IOException e) { e.printStackTrace(); } byte[] bytes = bos.toByteArray(); is.close(); bos.close(); //通过defineClass方法将二进制字节流转化为Class对象 return defineClass(name, bytes, 0, bytes.length); } catch (IOException ex) { throw new ClassNotFoundException("failed to load class " + name, ex); } } }
其中defineClass方法由本地native方法完成,不需要自己写。自定义ClassLoader后,有两大好处:
- 加载时机更加灵活,我们可以按照自己的逻辑在任意时刻重新加载Class对象。
- .class文件存储位置更加灵活,不光可以存储于类目录中,也可以存储于网络或数据库等位置。
三、热部署
自定义ClassLoader的一个重要应用场景:热部署。所谓热部署,就是在不重启应用的情况下,当类的定义即字节码文件被修改后,能够替换该Class创建的对象。
主要流程是:
- 创建自定义ClassLoader
- 加载指定目录下目标类的.class字节码文件
- 运用反射创建目标类实例
- 监听.class字节码文件变化
- .class字节码文件被修改后,重新加载并创建实例
首先,定义目标类:我们定义一个接口IHelloService,并定义其实现类HelloImpl,HelloImpl即我们的目标类,编译后得到字节码文件HelloImpl.class
public interface IHelloService { public void sayHello(); } public class HelloImpl implements IHelloService { public void sayHello() { System.out.println("hello"); } }
然后,仿照上面自定义ClassLoader,加载HelloImpl.class字节码文件,并运用反射生成HelloImpl实例:
//单例,获取HelloImpl实例 public static IHelloService getHelloService() { if (helloService != null) { return helloService; } synchronized (HotDeployDemo.class) { if (helloService == null) { helloService = createHelloService(); } return helloService; } } private static IHelloService createHelloService() { try { MyClassLoader cl = new MyClassLoader(); //自定义ClassLoader Class<?> cls = cl.loadClass(CLASS_NAME); //最终调用findClass和defineClass方法加载我们编译好的HelloImpl.class字节码文件,生成HelloImpl类 if (cls != null) { return (IHelloService) cls.newInstance(); //反射创建HelloImpl实例 } } catch (Exception e) { e.printStackTrace(); } return null; }
调用HelloImpl实例:
public static void client() { Thread t = new Thread() { @Override public void run() { try { while (true) { IHelloService helloService = getHelloService(); helloService.sayHello(); Thread.sleep(1000); //为方便验证,每隔1s调用一次 } } catch (InterruptedException e) { } } }; t.start(); }
此时,输出如下:
hello
hello
hello
监听.class字节码文件变化,如果监听到HelloImpl.class文件发生变化,则重新创建HelloImpl实例:
//这里以线程轮寻方式监听.class文件修改日期变化 public static void monitor() { Thread t = new Thread() { private long lastModified = new File(FILE_NAME).lastModified(); @Override public void run() { try { while (true) { Thread.sleep(100); long now = new File(FILE_NAME).lastModified(); if (now != lastModified) { lastModified = now; //如果监听到HelloImpl.class文件发生变化,则重新创建HelloImpl实例 reloadHelloService(); } } } catch (InterruptedException e) { } } }; t.start(); } private static void reloadHelloService() { helloService = createHelloService(); }
我们修改HelloImpl源码,并将重新编译的class字节码文件覆盖原文件:
public class HelloImpl implements IHelloService { public void sayHello() { System.out.println("hello,world"); //修改原文件逻辑 } }
此时,不用重启应用,发现输出已经发生了变化:
hello
hello
hello
hello,world
hello,world
至此,我们已经完成了一次热部署。