自定义类加载器实现类的热替换

2018-05-23 20:15:57   最后更新: 2018-05-24 12:14:33   访问数量:254




此前的一系列日志中,我们介绍了 SPI 机制

不遵循双亲委派的类加载器 -- 线程上下文类加载器

我们今天要做的事情和 SPI 非常像,我们要在代码运行时进行 class 的替换和重新加载,这就是 java 类的热替换

 

我们知道,jvm 类加载器通过双亲委派原则实现了唯一性原则,也就是保证了一个类只被加载一次,如果要实现热替换,就需要对相同的类进行两次加载,这样就必须要编写我们自己的类加载器才可以实现了,并且我们自己的类加载器还不能实现双亲委派

 

在此前的日志中我们介绍了类加载类 ClassLoader 的主要方法

java 类加载器详解 -- 双亲委派模式及实现

有以下的重要方法我们本次需要重新定义:

  1. findLoadedClass -- 查看类是否已经被加载,防止一个加载器加载两个相同的类
  2. getSystemClassLoader -- 获取系统默认类加载器,在我们的加载器中,我们只加载我们希望他加载的类,其他的类我们仍然委托给系统默认类加载器来加载
  3. defineClass -- 这是 ClassLoader 的一个非常重要的方法,他将以字节数组表示的类字节码转换成 Class 实例,但在此之前,该类的父类和所有他依赖接口类都必须先被加载
  4. loadClass -- ClassLoader 的入口方法,显式加载的过程,我们重新定义该方法就可以实现自己的加载过程了,但是 jdk 并不推荐我们这么做,而是推荐我们通过复写 findClass 来实现相同功能,这里我们这么做主要是为了探究内部的原理
  5. resolveClass -- java 类的链接过程就是这个方法实现的,这是一个在某些情况下确保类可用的必要方法

 

package com.techlog.test.testspring.classloader; import java.io.*; import java.util.HashSet; /** * Created by techlog on 2018/5/23. */ public class HotLoadClassLoader extends ClassLoader { private String basedir; // 需要该类加载器直接加载的类文件的基目录 private HashSet dynaclazns; // 需要由该类加载器直接加载的类名 public HotLoadClassLoader(String basedir, String[] classes) throws IOException { // 指定父类加载器为 null,打破双亲委派原则 super(null); this.basedir = basedir; dynaclazns = new HashSet(); customLoadClass(classes); } // 获取所有文件完整路径及类名,刷入缓存 private void customLoadClass(String[] classes) throws IOException { for (String classStr : classes) { loadDirectly(classStr); dynaclazns.add(classStr); } } // 拼接文件路径及文件名 private Class loadDirectly(String name) throws IOException { Class cls; StringBuilder sb = new StringBuilder(basedir); String classname = name.replace('.', File.separatorChar) + ".class"; sb.append(File.separator).append(classname); File classF = new File(sb.toString()); // 读取并加载 cls = instantiateClass(name,new FileInputStream(classF), classF.length()); return cls; } // 读取并加载类 private Class instantiateClass(String name, InputStream fin, long len) throws IOException { byte[] raw = new byte[(int) len]; fin.read(raw); fin.close(); return defineClass(name, raw, 0, raw.length); } protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class cls; // 判断是否已加载 cls = findLoadedClass(name); if(!this.dynaclazns.contains(name) && cls == null) cls = getSystemClassLoader().loadClass(name); if (cls == null) throw new ClassNotFoundException(name); if (resolve) resolveClass(cls); return cls; } }

 

 

接下来激动人心的时刻就要到了,我们就要用我们的自己的类加载器来加载我们的类了

我们来编写一个web项目并让他一直运行

 

Controller

package com.techlog.test.testspring.controller; import com.techlog.test.testspring.classloader.HotLoadClassLoader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by techlog on 2017/4/3. */ @RestController public class TestController { @RequestMapping("/hello") public String hello() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { ClassLoader classLoader = new HotLoadClassLoader("/Users/techlog/Workspace/intellij_idea/springtest/" + "testspring/target/classes/com/techlog/test/testspring/service", new String[] {"ClassLoaderTestService"}); Class cls = classLoader.loadClass("ClassLoaderTestService"); Object testService = cls.newInstance(); Method method = testService.getClass().getMethod("sayHello"); return (String) method.invoke(testService); } }

 

 

ClassLoaderTestService

/** * Created by techlog on 2018/5/23. */ public class ClassLoaderTestService { public String sayHello() { return "hello"; } }

 

 

执行

我们运行项目,访问相应的链接,就可以看到打印出了 hello

此时,让我们修改一下 ClassLoaderTestService

/** * Created by techlog on 2018/5/23. */ public class ClassLoaderTestService { public String sayHello() { return "world"; } }

 

 

我们删除目标目录中的 ClassLoaderTestService.class,将执行 javac ClassLoaderTestService.java 后生成的 ClassLoaderTestService.class 放入目标目录中,重新访问上述链接,在没有重新部署服务的情况下,输出结果变成了 world

 

上述代码中,我们继承了 ClassLoader,自己编写了文件读取、加载和解析的过程

事实上,官方更推荐的办法是我们通过继承 URLClassLoader 使用默认的实现去处理全部过程,下面我们就来看看继承 URLClassLoader 的加载器版本

 

我们自己的 ClassLoader

package com.techlog.test.testspring.classloader; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; /** * Created by techlog on 2018/5/23. */ public class HotLoadClassLoader extends URLClassLoader { public HotLoadClassLoader(URL[] urls) throws IOException { super(urls, null); // 指定父加载器为 null } }

 

这里我们只是做了一件事,就是通过指定父加载器为 null 来打破双亲委派原则

 

TestController

package com.techlog.test.testspring.controller; import com.techlog.test.testspring.classloader.HotLoadClassLoader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; /** * Created by techlog on 2017/4/3. */ @RestController public class TestController { @RequestMapping("/hello") public String hello() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { File file = new File("/Users/liuzeyu/Workspace/intellij_idea/springtest/" + "testspring/target/classes/com/techlog/test/testspring/service"); //File to URI URI uri=file.toURI(); URL[] urls={uri.toURL()}; ClassLoader classLoader = new HotLoadClassLoader(urls); Class cls = classLoader.loadClass("ClassLoaderTestService"); Object testService = cls.newInstance(); Method method = testService.getClass().getMethod("sayHello"); return (String) method.invoke(testService); } }

 

这样我们看起来就更加清晰了

 

 

 

Making reliable distributed systems in the presence of software errors -- http://erlang.org/download/armstrong_thesis_2003.pdf

Java 类的热替换 —— 概念、设计与实 -- https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/

 






技术帖      技术分享      class      java      加载      classloader      hot      load      替换     


京ICP备15018585号