原文:https://blog.fengjx.com/pages...背景在java项目开发测试过程中,需要反复修改代码,编译、部署,在一些大型项目中,整个编译部署过程可能需要几分钟,甚至几十分钟。前后端接口联合调试或修改测试问题时,可能只修改一个参数,前后端和测试都需要等待几十分钟。如果java能支持热加载,减少不必要的时间消耗,也能像nodejs一样有开发效率。问题分析要实现java代码的热部署,首先需要了解java代码是如何运行的。Java方法执行过程一个方法的执行过程是非常复杂的。区分静态分析和动态调度。为了方便理解,可以大致认为是如下过程:一个ja??va对象会在堆内存中包含对应的类指针,通过类名+方法名+方法描述(参数,返回值)找到对应的方法在相应的班级。在静态分析的情况下,可以直接得到一个方法引用地址。对于动态调度的方法,可以获取符号引用,通过符号引用可以找到目标方法地址,通过方法地址获取方法实例,将对应的方法字节码指令压入栈帧,获取执行细节.详见:《深入理解Java虚拟机-第3版》第8.3章小结:对象方法的调用会在对应的Class中查找到相关的方法字节码,并加载到线程栈中frameforexecution,那么我们只需要获取一个新类,并将新类加载或替换到方法区即可实现热加载功能如何在运行时获取一个Java类(字节码)使用javac或JavaCompiler下的相关APIjavax.tools包编译源码使用ASM、Javassist、Bytebuddy等第三方库生成字节码(网上有很多介绍,详细用法)直接写java字节码指令(如果不是jvm开发者,能直接写字节码指令写程序的人应该不多,说服,说服)java.lang.instrument.Instrumentation类通过什么方式在运行时加载?redefineClasses提供了两种方法:提供一个类文件,重新定义一个类,可以改变方法体、常量池和属性,不能增删改名字段或方法。如果字节码错误,将抛出异常。retransformClasses:是更新一个类,这个方法不会触发类初始化,所以静态变量的值会保持调用前的状态。官方api文档:https://docs.oracle.com/javas...但是使用Instrumentation重定义类还是有一些不足。为了安全起见,jvm限制了运行时重新定义的类,只能修改方法和属性中的逻辑和常量(可以使用dcevmjdk解决)。即使dcevmjdk可以支持新的类、方法和字段,一些第三方框架在启动时也会进行一些内部的初始化操作。比如spring启动的时候会扫描bean并实例化,使用Instrumentation的redefineClasses来定义新的@Service类之后不会在spring中注册,所以需要一个机制来通知spring加载新的beandcevmjdk是jdk的定制版本,可以支持类、方法、字段重定义。项目主页:http://dcevm.github.io/现在我们知道可以使用Instrumentation的redefineClasses来重新定义一个类,那么如何获取Instrumentation对象呢?从jdk5开始,可以使用java编写agent实现,可以在agent类中定义premain或者agentmain方法来获取Instrumentation实例。premain在main方法启动前执行,jvm启动时通过–javaagent参数加载agent类//优先级1大于2[1]publicstaticvoidpremain(StringagentArgs,Instrumentationinst);[2]publicstaticvoidpremain(StringagentArgs);ManiFest中需要指定Premain-Class:org.example.MyAgentagentmainloadedviaAttachAPI//Priority1isgreaterthan2[1]publicstaticvoidagentmain(StringagentArgs,Instrumentationinst);[2]publicstaticvoidagentmain(字符串agentArgs);ManiFest中需要指定Agent-Class:org.example.MyAgentexamplepackageorg.example;publicclassMyAgent{/***loadatstartup*/publicstaticvoidpremain(Stringargs,Instrumentationinst){System.out.println("预维护");}/***运行时加载(附加api)*/publicstaticvoidagentmain(Stringargs,Instrumentationinst){System.out.println("agentmain");}}maven包org.apache.maven.pluginsmaven-assembly-plugin3.3.0singlepackagejar-with-dependenciesorg.example.MyAgentorg.example.MyAgent配置>启动时使用jvm加载#fat指任意可执行文件fatjarjava-javaagent:myagent.jarfat是通过AttachAPI加载的太阳。工具.attach.*;公共类AttachTest?{publicstaticvoidmain(String[]args)throwsIOException,AttachNotSupportedException,AgentLoadException,AgentInitializationException{if(args.length<=1){System.out.println("Usage:javaAttachTest/path/to/myagent。罐”);返回;}VirtualMachinevm=VirtualMachine.attach(args[0]);vm.loadAgent(args[1]);}}确定实施方案通过以上相关知识的回顾和实际开发场景的分析,我们可以初步得出以下实施步骤来监控项目源代码文件(.java)的变化。通过nio2的WatchService监控目录文件变化。apachecommons.io包下的FileAlterationListenerAdaptor类已经封装了文件监听相关的处理逻辑,使用源码文件进行变更更方便。字节码文件(.class)可以通过ide(如:IntelliJIDEA)的自动编译功能生成。通过javac或JavaCompiler相关api编译class文件。使用AttachAPI加载自定义代理类,通过参数传递类名和字节。将代码文件路径传递给目标jvm进程,从参数和字节流中获取字节码文件路径。自定义代理类获取Instrumentation对象,通过redefineClasses方法重新定义类。redefineClasses方法只需要获取要重定义的类名和字节码字节流即可。定义类,那么也可以在远程服务器上开一个服务器,提供文件上传接口,将字节码文件上传到服务器,实现远程jvm进程热加载架构设计的源码实现。相关代码实现已经放在github和gitee中,可以参考refergithub:https://github.com/fengjx/jav...gitee:https://gitee.com/fengjx/java...参考https://tech.meituan.com/2019...https://tech.meituan.com/2019...https://tech.meituan.com/2020...https://leokongwq.github.io/2...https://blog.csdn.net/program...延伸阅读tomcat如何实现jsp热加载我们都知道jsp文件最终会被编译成Servlet实现类看tomcat实现jsp热加载的几个关键源码//JspCompilationContext.javapublicvoidcompile()throwsJasperException,FileNotFoundException{createCompiler();//判断文件是否发生变化if(jspCompiler.isOutDated()){if(isRemoved()){thrownewFileNotFoundException(jspUri);}try{//删除之前编译好的文件jspCompiler.removeGeneratedFiles();//如果设置为null,则会创建一个新的jspLoader(因为在同一个ClassLoader中,不允许重复加载Class,所以需要创建一个新的Classloader)jspLoader=null;//将jsp编译成ServletjspCompiler.compile();jsw.setReload(真);jsw.setCompilationException(null);}抓住(JasperExceptionex){//缓存编译异常jsw.setCompilationException(ex);if(options.getDevelopment()&&options.getRecompileOnFail()){//强制在下一次访问时重新编译尝试jsw.setLastModificationTest(-1);}扔前;}catch(FileNotFoundExceptionfnfe){//重新抛出让调用者处理这个-将导致404throwfnfe;}catch(Exceptionex){JasperExceptionje=newJasperException(Localizer.getMessage("jsp.error.unable.compile"),ex);//缓存编译异常jsw.setCompilationException(je);扔je;}}}//JspServletWrapper.javapublicServletgetServlet()抛出ServletException{if(getReloadInternal()||theServlet==null){synchronized(this){if(getReloadInternal()||theServlet==null){destroy();最终的Servletservlet;尝试{InstanceManagerinstanceManager=InstanceManagerFactory.getInstanceManager(config);//通过新创建的JasperLoader创建servlet实例servlet=(Servlet)instanceManager.newInstance(ctxt.getFQCN(),ctxt.getJspLoader());}catch(Exceptione){Throwablet=ExceptionUtils.unwrapInvocationTargetException(e);ExceptionUtils.handleThrowable(t);抛出新的JasperException(t);}servlet.init(配置);if(theServlet!=null){ctxt.getRuntimeContext().incrementJspReloadCount();}theServlet=servlet;重新加载=假;}}}returntheServlet;}//JspCompilationContext.javpublicClassLoadergetJspLoader(){//之前设置为null,这里会创建一个新的JasperLoaderif(jspLoader==null){jspLoader=newJasperLoader(newURL[]{baseUrl},getClassLoader(),rctxt.getPermissionCollection());}returnjspLoader;}整体流程如下定时扫描jsp文件目录,比较文件修改时间,看是否有变化Classload并加载新的Servlet类通过新建的Classloader创建Servlet实例