`

探索 Java 热部署

阅读更多

在 JAVA 开发领域,热部署一直是一个难以解决的问题,目前的 JAVA 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。对于某些大型的应用来说,每次的重启都需要花费大量的时间成本。虽然 OSGI 架构的出现,让模块重启成为可能,但是如果模块之间有调用关系的话,这样的操作依然会让应用出现短暂的功能性休克。本文将探索如何在不破坏 JAVA 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启就完成某个类的更新。

类加载的探索

首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。

另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。本文将具体探索如何实现这个方案。首先需要了解一下 Java 虚拟机现有的加载机制。目前的加载机制,称为双亲委派,系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。这种自上而下的加载方式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是这种方式却使加载顺序非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难题。

不过我们可以换一个思路,虽然无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。

下面来简单列举一下需要做的工作。

  • 创建自定义的 classloader,加载需要监听改变的类,在 class 文件发生改变的时候,重新加载该类。
  • 改变创建对象的行为,使他们在创建时使用自定义 classloader 加载的 class。

 

自定义加载器的实现

自定义加载器仍然需要执行类加载的功能。这里却存在一个问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不会变,而 classloader 只能在虚拟机停止前销毁已经加载的类,这样 classloader 就无法加载更新后的类了。这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的 class,比如加载 Test.class 时,保存在内存中的类是 Test_v1.class,当类发生改变时,重新加载的类名是 Test_v2.class。但是真正执行加载 class 文件创建 class 的 defineClass 方法是一个 native 的方法,修改起来又变得很困难。所以面前还剩一条路,那就是直接修改编译生成的 class 文件。

利用 ASM 修改 class 文件

可以修改字节码的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先来介绍一下 class 文件的结构,class 文件包含了以下几类信息,一个是类的基本信息,包含了访问权限信息,类名信息,父类信息,接口信息。第二个是类的变量信息。第三个是方法的信息。ASM 会先加载一个 class 文件,然后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新的 class。

首先看一下如何利用 ASM 修改类信息。

清单 1. 利用 ASM 修改字节码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
       ClassReader cr = null;    
       String enhancedClassName = classSource.getEnhancedName();
       try {
           cr = new ClassReader(new FileInputStream(
                   classSource.getFile()));
       } catch (IOException e) {
           e.printStackTrace();
           return null;
       }
       ClassVisitor cv = new EnhancedModifier(cw,
               className.replace(".", "/"),
               enhancedClassName.replace(".", "/"));
       cr.accept(cv, 0);

ASM 修改字节码文件的流程是一个责任链模式,首先使用一个 ClassReader 读入字节码,然后利用 ClassVisitor 做个性化的修改,最后利用 ClassWriter 输出修改后的字节码。

之前提过,需要将读取的 class 文件的类名做一些修改,加载成一个全新名字的派生类。这里将之分为了 2 个步骤。

第一步,先将原来的类变成接口。

清单 2. 重定义的原始类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   public Class<?> redefineClass(String className){
       ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
       ClassReader cr = null;
       ClassSource cs = classFiles.get(className);
       if(cs==null){
           return null;
       }
       try {
           cr = new ClassReader(new FileInputStream(cs.getFile()));
       } catch (IOException e) {
           e.printStackTrace();
           return null;
       }
       ClassModifier cm = new ClassModifier(cw);
       cr.accept(cm, 0);
       byte[] code = cw.toByteArray();
       return defineClass(className, code, 0, code.length);
}

首先 load 原始类的 class 文件,此处定义了一个增强组件 ClassModifier,作用是修改原始类的类型,将它转换成接口。原始类的所有方法逻辑都会被去掉。

第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。这样做的目的是不论如何修改,同一个 class 的派生类都有一个共同的接口,他们之间的转换变得对外不透明。

清单 3. 定义一个派生类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   // 在 class 文件发生改变时重新定义这个类
   private Class<?> redefineClass(String className, ClassSource classSource){
       ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
       ClassReader cr = null;
       classSource.update();
       String enhancedClassName = classSource.getEnhancedName();      
       try {
           cr = new ClassReader(
                   new FileInputStream(classSource.getFile()));
       } catch (IOException e) {
           e.printStackTrace();
           return null;
       }
       EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"),
               enhancedClassName.replace(".", "/"));
       ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"),
               enhancedClassName.replace(".", "/"));
       cr.accept(exm, 0);
       byte[] code = cw.toByteArray();
       classSource.setByteCopy(code);
       Class<?> clazz = defineClass(enhancedClassName, code, 0, code.length);
       classSource.setClassCopy(clazz);
       return clazz;
}

再次 load 原始类的 class 文件,此处定义了两个增强组件,一个是 EnhancedModifier,这个增强组件的作用是改变原有的类名。第二个增强组件是 ExtendModifier,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。

自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。

 

改变创建对象的行为

Java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。

由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。

对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。

清单 4. 替换后的指令集所对应的逻辑
1
2
3
4
5
// 原始逻辑  
  Greeter p = new Greeter();
// 改变后的逻辑
  IGreeter p = (IGreeter)MyClassLoader.getInstance().
  findClass("com.example.Greeter").newInstance();

这里又需要用到 ASM 来修改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。

清单 5. 利用 ASM 修改方法体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void visitTypeInsn(int opcode, String type) {
    if(opcode==Opcodes.NEW && type.equals(className)){
        List<LocalVariableNode> variables = node.localVariables;
        String compileType = null;
        for(int i=0;i<variables.size();i++){
            LocalVariableNode localVariable = variables.get(i);
            compileType = formType(localVariable.desc);
            if(matchType(compileType)&&!valiableIndexUsed[i]){
                valiableIndexUsed[i] = true;
                break;
            }
        }
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, CLASSLOAD_TYPE,
        "getInstance", "()L"+CLASSLOAD_TYPE+";");
    mv.visitLdcInsn(type.replace("/", "."));
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CLASSLOAD_TYPE,
        "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class",
        "newInstance", "()Ljava/lang/Object;");
    mv.visitTypeInsn(Opcodes.CHECKCAST, compileType);
    flag = true;
    } else {
        mv.visitTypeInsn(opcode, type);
    }
 }

对于第二种创建方法,需要通过修改 Class.forName()和 ClassLoader.findClass()的行为,使他们通过自定义加载器加载类。

 

使用 JavaAgent 拦截默认加载器的行为

之前实现的类加载器已经解决了热部署所需要的功能,可是 JVM 启动时,并不会用自定义的加载器加载 classpath 下的所有 class 文件,取而代之的是通过应用加载器去加载。如果在其之后用自定义加载器重新加载已经加载的 class,有可能会出现 LinkageError 的 exception。所以必须在应用启动之前,重新替换已经加载的 class。如果在 jdk1.4 之前,能使用的方法只有一种,改变 jdk 中 classloader 的加载行为,使它指向自定义加载器的加载行为。好在 jdk5.0 之后,我们有了另一种侵略性更小的办法,这就是 JavaAgent 方法,JavaAgent 可以在 JVM 启动之后,应用启动之前的短暂间隙,提供空间给用户做一些特殊行为。比较常见的应用,是利用 JavaAgent 做面向方面的编程,在方法间加入监控日志等。

JavaAgent 的实现很容易,只要在一个类里面,定义一个 premain 的方法。

清单 6. 一个简单的 JavaAgent
1
2
3
4
5
6
public class ReloadAgent {
   public static void premain(String agentArgs, Instrumentation inst){
       GeneralTransformer trans = new GeneralTransformer();
       inst.addTransformer(trans);
   }
}

然后编写一个 manifest 文件,将 Premain-Class属性设置成定义一个拥有 premain方法的类名即可。

生成一个包含这个 manifest 文件的 jar 包。

1
2
3
manifest-Version: 1.0
 Premain-Class: com.example.ReloadAgent
 Can-Redefine-Classes: true

最后需要在执行应用的参数中增加 -javaagent参数 , 加入这个 jar。同时可以为 Javaagent增加参数,下图中的参数是测试代码中 test project 的绝对路径。这样在执行应用的之前,会优先执行 premain方法中的逻辑,并且预解析需要加载的 class。

图 1. 增加执行参数

 s

这里利用 JavaAgent替换原始字节码,阻止原始字节码被 Java 虚拟机加载。只需要实现 一个 ClassFileTransformer的接口,利用这个实现类完成 class 替换的功能。

清单 7. 替换 class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public byte [] transform(ClassLoader paramClassLoader, String paramString,
     Class<?> paramClass, ProtectionDomain paramProtectionDomain,
     byte [] paramArrayOfByte) throws IllegalClassFormatException {
    String className = paramString.replace("/", ".");
    if(className.equals("com.example.Test")){
        MyClassLoader cl = MyClassLoader.getInstance();
        cl.defineReference(className, "com.example.Greeter");
        return cl.getByteCode(className);
    }else if(className.equals("com.example.Greeter")){
        MyClassLoader cl = MyClassLoader.getInstance();
        cl.redefineClass(className);
        return cl.getByteCode(className);
    }
    return null;
 }

至此,所有的工作大功告成,欣赏一下 hotswap 的结果吧。

图 2. Test 执行结果

d

 

结束语

解决 hotswap 是个困难的课题,本文解决的仅仅是让新实例化的对象使用新的逻辑,并不能改变已经实例化对象的行为,如果 JVM 能够重新设计 class 的生命周期,支持运行时重新更新一个 class,hotswap 就会成为 Java 的一个闪亮新特性。官方的 JVM 一直没有解决热部署这个问题,可能也是由于无法完全克服其中的诸多难点,希望未来的 Jdk 能解决这个问题,让 Java 应用对于更新更友好,避免不断重启应用浪费的时间。

 

分享到:
评论

相关推荐

    深入探索 Java 热部署

    Java 热部署技术是开发过程中的一大挑战,它旨在实现在不重启 Java 虚拟机(JVM)的情况下更新应用程序的部分代码。由于 JVM 默认仅在启动时加载类,一旦类加载,即使原始的 .class 文件发生变化,JVM 也不会自动...

    深入探索Java热部署

    本文将探索如何在不破坏 Java 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启完成某个类的更新。  类加载的探索  首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提

    SpringBoot框架简单搭建+具有热部署

    在这个“SpringBoot框架简单搭建+具有热部署”的项目中,我们将深入探讨如何创建一个基本的SpringBoot项目,并启用DevTools实现热部署功能。 首先,SpringBoot的核心特性在于其“约定优于配置”的设计理念,这意味...

    基于SpringBoot远程热部署的探索和应用.docx

    1. **DevTools**:Spring Boot DevTools是专为开发人员设计的一组工具,它可以在应用重启时快速加载更改过的类,实现了基本的热部署功能。 2. **JRebel**:JRebel是一款商业的热部署插件,它可以实时更新代码而无需...

    Idea配置热部署的详细教程

    - 修改jsp或java文件测试热部署是否成功。要注意的是,Idea的热部署并非完全实时,可能有延迟,如果未立即生效,可以等待class reload提示或手动触发(快捷键Ctrl + F10)。 2. **Spring Boot程序热部署** - 同样...

    java类重载,可以用于热更新

    但有一种称为"热部署"(Hot Deployment)或"热替换"(Hot Swap)的技术,可以在不重启应用的情况下替换已加载的类。这通常在开发环境中使用,以提高调试效率。JVM通过一些工具和插件,如JRebel、JBoss ClassLoader、...

    idea jrebel1.7免费版热部署插件

    ### idea jrebel1.7免费版热部署插件 #### 知识点一:jRebel 插件概述 jRebel 是一款强大的 Java 应用程序开发工具,能够帮助开发者实现在不重启应用程序的情况下对代码进行实时更新。这对于提高开发效率、减少...

    SOFAArk 是一款基于 Java 实现的轻量级类隔离容器,主要提供类隔离和应用(模块)动态部署能力

    SOFAArk 是一款基于 Java 实现的动态热部署和轻量级类隔离框架,由蚂蚁集团开源贡献,主要提供应用模块的动态热部署和类隔离能力。基于 Fat Jar 技术,可以将多个应用模块打包成一个自包含可运行的 Fat Jar,应用既...

    实战JAVA虚拟机随书源码

    3. ClassPath Hacking:理解如何修改运行时的类路径,实现类的动态替换,从而实现热部署。 四、实战经验 源码中的实战案例将提供实际场景下的问题解决思路,如性能瓶颈分析、日志分析、监控工具的使用等,这将帮助...

    sun公司的java教程

    类加载器负责在运行时动态加载类,是Java实现“热部署”和模块化的重要部分。 11. **API和库**:Java标准库(Java API)提供了大量的预定义类和方法,涵盖了从基本类型操作到复杂的网络和数据库访问功能,极大地...

    Java学习入门资料 PPT版本

    - **部署技术**:Java Web Start等技术帮助开发者部署应用程序。 - **用户界面工具箱**:Swing和Java 2D toolkits等库用于创建图形用户界面(GUI)。 - **综合库**:如JDBC、JNDI、RMI等,支持数据库连接、名称服务、...

    java classloader

    这使得Java可以支持许多高级功能,例如插件系统、热部署和模块化系统(如Java 9的Jigsaw项目)。 `JAVA2深度历险.jpg`可能是关于Java 2 SDK的深度探索的图像资料,可能涵盖了ClassLoader的部分内容。而`CH_02.深入...

    一篇文章带你搞定SpringBoot中的热部署devtools方法

    在SpringBoot开发过程中,热部署是一个非常实用的功能,它允许开发者在修改代码后无需手动重启应用就能看到改动的效果,大大提高了开发效率。本文将详细介绍如何在SpringBoot中使用devtools进行热部署,并探讨其工作...

    java电子商城源代码

    1. **Java J2EE**:Java企业版(Java Enterprise Edition,简称J2EE)是一个用于构建和部署分布式企业级应用的开放标准框架。它提供了各种服务,如安全、事务管理、邮件服务等,使得开发者能够构建可扩展且健壮的Web...

    Java深度历险-让你了解JDK

    同时,动态加载和热部署也是JVM的重要特性,它们使得Java应用能在运行时修改和加载新的类,增加了程序的灵活性。 进一步,我们关注Java的异常处理机制,它通过try-catch-finally语句块提供了一种错误处理方式,保证...

    java深度历险2

    书中可能详细讲解了自定义类加载器的创建、类加载的生命周期、以及如何利用类加载机制实现动态加载和热部署。 接下来,“CH_03.Java与MS Office.pdf”这部分可能会探讨Java与Microsoft Office系统的集成。Java提供...

    java深度历险(繁体)

    总的来说,《Java深度历险》是一本深度探索Java技术的宝典,涵盖了从基础到高级的各个层面,对于Java开发者来说,无论是初学者还是经验丰富的专业人士,都能从中受益匪浅,提升自己的技能水平。通过学习这些内容,...

    hot:java的热更新演示例子

    JRebel是一款流行的商业工具,它极大地简化了Java应用的热部署过程。JRebel能够在代码保存后立即反映到运行中的应用,无需重启。它通过动态加载和卸载类来实现热更新,提高了开发效率。在"hot-master"项目中,可能...

    java oa项目开发日记

    4. **快速启动与“热”部署**:JBoss的快速启动特性使其在资源占用方面优于Sun的J2EE,并支持“热”部署,即无需重启服务器即可更新部署的Bean,极大地提高了开发效率和系统可用性。 #### 安装与配置JBoss应用...

Global site tag (gtag.js) - Google Analytics