此前的一系列日志中,我们介绍了 SPI 机制
不遵循双亲委派的类加载器 -- 线程上下文类加载器
我们今天要做的事情和 SPI 非常像,我们要在代码运行时进行 class 的替换和重新加载,这就是 java 类的热替换
我们知道,jvm 类加载器通过双亲委派原则实现了唯一性原则,也就是保证了一个类只被加载一次,如果要实现热替换,就需要对相同的类进行两次加载,这样就必须要编写我们自己的类加载器才可以实现了,并且我们自己的类加载器还不能实现双亲委派
在此前的日志中我们介绍了类加载类 ClassLoader 的主要方法
java 类加载器详解 -- 双亲委派模式及实现
有以下的重要方法我们本次需要重新定义:
- findLoadedClass -- 查看类是否已经被加载,防止一个加载器加载两个相同的类
- getSystemClassLoader -- 获取系统默认类加载器,在我们的加载器中,我们只加载我们希望他加载的类,其他的类我们仍然委托给系统默认类加载器来加载
- defineClass -- 这是 ClassLoader 的一个非常重要的方法,他将以字节数组表示的类字节码转换成 Class 实例,但在此之前,该类的父类和所有他依赖接口类都必须先被加载
- loadClass -- ClassLoader 的入口方法,显式加载的过程,我们重新定义该方法就可以实现自己的加载过程了,但是 jdk 并不推荐我们这么做,而是推荐我们通过复写 findClass 来实现相同功能,这里我们这么做主要是为了探究内部的原理
- 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
替换