# 前言
在之前的 深入浅出 JVM ClassLoader https://www.jianshu.com/p/85eba062b9c1 一文中, 我们说可以通过修改默认的类加载器实现热部署, 但在 Java 开发领域, 热部署一直是一个难以解决的问题, 目前的 Java 虚拟机只能实现方法体的修改热部署, 对于整个类的结构修改, 仍然需要重启虚拟机, 对类重新加载才能完成更新操作. 对于某些大型的应用来说, 每次的重启都需要花费大量的时间成本, 所以, 如果能像我们之前说的那样, 在不重启虚拟机的情况下更新一个类, 在某些业务场景下变得十分重要. 比如很多脚本语言就支持热替换, 例如 PHP, 只要替换了 PHP 源文件, 这种改动就会立即生效, 且无需重启服务器.
今天我们就来一个简单的热部署, 注意: 不要小看他, 这也是 JSP 支持修改的实现方式.
## # 1. 怎么实现?
在上篇文章中, 我们贴了一幅图:
我们知道, 一个类加载器只能加载一个同名类, 在 Java 默认的类加载器层面作了判断, 如果已经有了该类, 则不再重复加载, 如果强行绕过判断并使用自定义类加载器重复加载 (比如调用 defineClass 方法),JVM 将会抛出 LinkageError:attempted duplicate class definition for name.
但请注意, 我们说同一个类加载器不可以加载两个同名的类, 但不同的类加载器是可以加载同名的类的, 加载完成之后, 这两个类虽然同名, 但不是同一个 Class 对象, 无法进行转换.
那么我们是否可以利用这个特性, 实现热部署呢? 如同上图的步骤: 使用自定义的类加载器, 加载一个类, 当需要进行替换类的时候, 我们就丢弃之前的类加载器和类, 使用新的类加载器去加载新的 Class 文件, 然后运行新对象的方法.
让我们按照这个思路写段代码试试吧!
- class AccountMain {
- public static void main(String[] args)
throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
- while (true) {
- ClassLoader loader = new ClassLoader() {
- @Override
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- try {
- String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
- InputStream is = getClass().getResourceAsStream(fileName);
- if (is == null) {
- return super.loadClass(name);
- }
- byte[] b = new byte[is.available()];
- is.read(b);
- return defineClass(name, b, 0, b.length);
- } catch (IOException e) {
- e.printStackTrace();
- throw new ClassNotFoundException(name);
- }
- }
- };
- Class clazz = loader.loadClass("cn.think.in.java.clazz.loader.asm.Account");
- Object account = clazz.newInstance();
- account.getClass().getMethod("operation", new Class[]{}).invoke(account);
- Thread.sleep(20000);
- }
- }
- }
上面这个类是一个 mian 方法类, 该方法是一个间隔 20 秒的死循环, 步骤如下:
创建一个自定义的 ClassLoader 对象, 加载类的步骤不遵守双亲委派模型, 而是直接加载.
使用刚刚创建的类加载器加载指定的类.
得到刚刚的 Class 对象, 使用反射创建对象, 并调用对象的 operation 方法.
为什么间隔 20 秒呢? 因为我们要在启动之后, 修改类, 并重新编译. 因此需要 20 秒时间.
再看看 Account 类:
- public class Account {
- public void operation() {
- System.out.println("operation...");
- try {
- Thread.sleep(10);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
该类很简单, 只有一个方法, 就是打印 operation... 字符串.
我们还需要一个类, 干什么用呢? 我们刚刚说, 需要修改 Account 类, 然后重新编译, 为了方便, 我们创建一个类, 专门用于执行修改后的 Account 类, 因为执行后肯定重新编译了, 省的我们去命令行使用 javac 了.
代码如下:
- class ReCompileAccount {
- public static void main(String[] args) {
- new Account().operation();
- }
- }
如何测试呢?
启动 AccountMain main 方法. 会立刻打印出 operation... 字符串, 并开始等待 20 秒.
修改 Account 类的字符串为 operation.....new,
启动 ReCompileAccount 类, 目的是重新编译 Accoutn 类.
等待 AccountMain 类的打印.
不出意外的话, 最后结果如下:
看到了吧, 我们已经成功的把 Accout 类修改了, 并且是在不重启 JVM 的情况下, 实现了热部署. 就像我们刚刚说的, JSP 支持修改也是这么实现的, 每一个 JSP 页面都对应着一个类加载器, 当 JSP 页面被修改了, 就重新创建类加载器, 然后使用新的类加载器加载 JSP (JSP 其实就是 Java 类).
## # 总结
基于 ClassLoader 的原理, 我们实现了 Java 层面的热部署, 但大家如果自己实现一遍的话, 还是觉得很麻烦, 诚然, JSP 使用这种方式没什么问题, 因为他是自动编译的. 但如果我们自己的应用的话, 难道每次修改一个类, 都要重新编译一遍, 然后在给定的时间里去替换? 我们能不能把这些手工活都交给 JVM 呢? 实际上, Tocmat 也已经通过这种方式实现过了. 限于篇幅, 我们将在下一篇文章中讲述.
good luck!!!!!
来源: http://www.bubuko.com/infodetail-2608802.html