java实现代码热更新(代码片段)

热爱编程的大忽悠 热爱编程的大忽悠     2023-01-11     260

关键词:

JAVA实现代码热更新


引言

本文将带领大家利用Java的类加载器加SPI服务发现机制实现一个简易的代码热更新工具。

类加载相关知识可以参考: 深入理解JVM虚拟机第三版, 深入理解JVM虚拟机(第二版)—国外的,自己动手写JVM


类加载器

JVM通过ClassLoader将.class二进制流读取到内存中,然后为其建立对应的数据结构:

/*
ClassFile 
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];

*/
//伪代码,不全
type Class struct 
	accessFlags       uint16
	name              string // thisClassName
	superClassName    string
	interfaceNames    []string
	constantPool      *ConstantPool
	fields            []*Field
	methods           []*Method
	sourceFile        string
	loader            *ClassLoader
	superClass        *Class
	interfaces        []*Class
	instanceSlotCount uint
	staticSlotCount   uint
	staticVars        Slots
	initStarted       bool
	jClass            *Object
	...

接着对Class执行验证,准备和解析,当然将符号引用解析为直接引用的过程一般用到的时候才会去解析,这也说明了为什么类只会在用到的时候才会进行初始化。

如果想要在内存中唯一确定一个类,需要通过加载该类的类加载实例和当前类本身来唯一确定,因为每个类加载器都有自己的命名空间:

//伪代码
type ClassLoader struct 
	//负责从哪些路径下加载class文件
	cp          *classpath.Classpath
	//简易版本命令空间隔离实现
	classMap    map[string]*Class // loaded classes

对于由不同类加载实例对象加载的类而言,他们是不相等的,这里的不相等包括Class对象的equals方法,isAssignableFrom方法,isInstance方法,Instanceof关键字,包括checkcast类型转换指令。

同一个类加载实例不能重复加载同一个类两次,否则会抛出连接异常。


实现热更新思路

  • 自定义类加载器,重写loadClass,findClass方法
/**
 * @author 大忽悠
 * @create 2023/1/10 10:31
 */
public class DynamicClassLoader extends ClassLoader
    ...
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 
        synchronized (getClassLoadingLock(name)) 
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null)
                return c;
            
            //1.父类加载
            if (getParent() != null) 
                try
                    c = getParent().loadClass(name);
                catch (ClassNotFoundException e)
                
            
            //2.自己加载
            if(c==null)
                c = findClass(name);
            
            //3.是否对当前class进行连接
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException 
        byte[] classBytes=getClassBytes(name);
        return defineClass(name,classBytes, 0, classBytes.length);
    
    
        /**
     * @param name 全类名
     * @param resolve 是否需要对加载得到类进行link过程--验证,准备,解析(一般都是懒解析)
     */
    public static Class<?> dynamicLoadClass(String name,Boolean resolve) throws ClassNotFoundException 
        DynamicClassLoader dynamicClassLoader = new DynamicClassLoader();
        return dynamicClassLoader.loadClass(name,resolve);
    

    /**
     * @param name 全类名
     */
    public static Class<?> dynamicLoadClass(String name) throws ClassNotFoundException 
        return dynamicLoadClass(name,false);
    
    
    ...

dynamicLoadClass作为新增的静态方法,每次都会重新创建一个DynamicClassLoader自定义类加载器实例,并利用该实例去加载我们指定的类:

    public static void main(String[] args) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException 
        invokeSay();
        Thread.sleep(15000);
        invokeSay();
    

    private static void invokeSay() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException 
        Class<?> aClass = DynamicClassLoader.dynamicLoadClass("com.exm.A");
        Object newInstance = aClass.newInstance();
        Method method = aClass.getMethod("say");
        method.invoke(newInstance);
    

我们只需要在休眠的这15秒内,替换掉对应的class文件实现,即可完成代码的热更新,并且同时确保父类加载器不能够找到同类路径的类,否则就不能让自定义加载器得到机会重新读取二进制流到内存并建立相应的数据结构了。

默认的父类加载器是类路径加载器,也被称作系统类路径加载器

该系统类加载器就是默认创建用来加载启动类的加载器,因为我们在启动类中通过方法调用引用了DynamicClassLoader,因此我们自定义的类加载器也是通过加载启动类的加载器进行加载的。
在本类中引用到的类都会使用加载本类的加载器进行加载


多种多样的加载来源

class二进制流数据可以来自于文件,网络,数据库或者其他地方,因此为了支持多种多样的加载来源,我们可以定义一个ClassDataLoader接口:

/**
 * @author 大忽悠
 * @create 2023/1/10 11:37
 */
public interface ClassDataLoader 
    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    byte[] loadClassData(String name);

  • 这里给出一个从文件中加载classData的实现案例:
package com;

import java.io.*;

/**
 * @author 大忽悠
 * @create 2023/1/10 11:48
 */
public class FileClassDataLoader implements ClassDataLoader
    /**
     * 默认从当前项目路径找起
     */
    private String basePath="";

    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    @Override
    public byte[] loadClassData(String name) 
        return getClassData(new File(basePath+name.replace(".","/")+".class"));
    

    private static byte[] getClassData(File file) 
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
                ByteArrayOutputStream()) 
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) 
                baos.write(buffer, 0, bytesNumRead);
            
            return baos.toByteArray();
         catch (IOException e) 
            e.printStackTrace();
        
        return new byte[] ;
    



DynamicClassLoader自定义加载器内部新增两个属性:

    /**
     * 负责根据全类名加载class二进制流
     */
    private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
    /**
     * 所有DynamicClassLoader加载器共享一个缓存
     */
    private final static Map<String,byte[]> classBytesCache =new HashMap<>();

    public static void registerClasDataLoader(ClassDataLoader classDataLoader)
          classDataLoaderList.add(classDataLoader);
    

    public static void cacheUpdateHook(String name,byte[] classData)
         classBytesCache.put(name,classData);
    

对应的loadClass方法被修改为如下:

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 
        synchronized (getClassLoadingLock(name)) 
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null)
                return c;
            
            //1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
            if (classBytesCache.get(name)==null && getParent() != null) 
                try
                    c = getParent().loadClass(name);
                catch (ClassNotFoundException e)
                
            
            //2.自己加载
            if(c==null)
                c = findClass(name);
            
            //3.是否对当前class进行连接
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException 
        byte[] classBytes = classBytesCache.get(name);
        if(classBytes==null)
            for (ClassDataLoader classDataLoader : classDataLoaderList) 
                      if((classBytes=classDataLoader.loadClassData(name))!=null)
                               break;
                      
            
        
        if (classBytes==null || classBytes.length == 0) 
            throw new ClassNotFoundException();
        
        classBytesCache.put(name,classBytes);
        return defineClass(name,classBytes, 0, classBytes.length);
    

DynamicClassLoader内部内置了多个ClassData数据源,我们通过遍历数据源列表,只要其中一个返回结果不为空,我们就立刻返回。

为了避免每次都需要重新从数据源中读取数据,我们可以将从数据源中获取到的二进制字节码缓存起来,然后让ClassDataLoader通过cacheUpdateHook钩子函数更新缓存达到动态更新的效果。


我们自定义的FileClassDataLoader通过回调registerClassDataLoader接口,将自身注册到DynamicClassLoader的数据源列表中去:

    static 
        DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());
    

但是如何让FileClassDataLoader静态代码块能够执行,也就是FileClassDataLoader类需要被初始化,如何做到?


SPI服务发现机制

在不通过new指令,不调用类里面的方法和访问类中字段的情况下,想要类能够被初始化,我们可以通过Class.forName完成:

forName的重载方法有一个Initialize参数,表明加载了当前类后,是否需要初始化该类,如果我们调用单参数的forName,那么默认为true。

所以,现在,我们只需要通过一种方式获取到ClassDataLoader的所有实现类类名,然后挨个使用Class.forName方法,完成实现类的初始化,就可以让实现类都注册到DynamicClassLoader中去。

SPI可以使用Java提供的serviceLoader,或者参考Spring的spring.factories实现,这里我给出一个简单的实现方案:

/**
 * @author 大忽悠
 * @create 2023/1/10 12:03
 */
public class SPIService 
    /**
     * 服务文件地址
     */
    private static final String SERVICE_PATH = "META-INF" + File.separator + "SPI.properties";
    /**
     * 服务信息存储
     */
    private static Properties SERVICE_MAP;

    static 
        try 
            SERVICE_MAP = new Properties();
            SERVICE_MAP.load(SPIService.class.getClassLoader().getResourceAsStream(SERVICE_PATH));
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

    /**
     * @param name 需要寻找的服务实现的接口的全类名
     * @return 找寻到的所有服务实现类
     */
    public List<Class<?>> loadService(String name) 
        if (SERVICE_MAP == null) 
            return null;
        
        String[] classNameList = SERVICE_MAP.getProperty(name).split(",");
        ArrayList<Class<?>> classList = new ArrayList<>(classNameList.length);
        for (String classDataClassName : classNameList) 
            try 
                classList.add(Class.forName(classDataClassName));
             catch (ClassNotFoundException e) 
                //忽略不可被解析的服务实现类
                e.printStackTrace();
            
        
        return classList;
    


DynamicClassLoader新增代码:

    /**
     * 负责提供SPI服务发现机制
     */
    private final static SPIService spiService=new SPIService();

    static 
        //通过SPI机制寻找classDataLoader
        spiService.loadService(ClassDataLoader.class.getName());
    


完整代码

package com;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 大忽悠
 * @create 2023/1/10 10:31
 */
public class DynamicClassLoader extends ClassLoader
    /**
     * 负责根据全类名加载class二进制流
     */
    private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
    /**
     * 所有DynamicClassLoader加载器共享一个缓存
     */
    private final static Map<String,byte[]> classBytesCache =new HashMap<>();
    /**
     * 负责提供SPI服务发现机制
     */
    private final static SPIService spiService=new SPIService();

    static 
        //通过SPI机制寻找classDataLoader
        spiService.loadService(ClassDataLoader.class.getName());
    

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 
        synchronized (getClassLoadingLock(name)) 
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null)
                return c;
            
            //1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
            if (classBytesCache.get(name)==null && getParent() != null) 
                try
                    c = getParent().loadClass(name);
                catch (ClassNotFoundException e)
                
            
            //2.自己加载
            if(c==null)
                c = findClass(name);
            
            //3.是否对当前class进行连接
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException 
        byte[] classBytes = classBytesCache.get(name);
        if(classBytes==null)
            for (ClassDataLoader classDataLoader : classDataLoaderList) 
                      if((classBytes=classDataLoader.loadClassData(name))!=null)
                               break;
                      
            
        

        if (classBytes==null || classBytes.length == 0) 
            throw new ClassNotFoundException();
        
        classBytesCachejava实现代码热更新(代码片段)

JAVA实现代码热更新引言类加载器实现热更新思路多种多样的加载来源SPI服务发现机制完整代码类加载器共享空间机制Tomcat如何实现JSP的热更新Spring反向访问用户程序类问题补充细节推荐资源引言本文将带领大家利用Java的类加载... 查看详情

android手动实现热更新(代码片段)

前言在上篇AndroidClassLoader浅析中我们分析了安卓ClassLoader和热更新的原理,这篇我们在上篇热更新分析的基础上写个简单的demo实践一下。概述我们先回顾下热更新的原理PathClassLoader是安卓中默认的类加载器,加载类是通... 查看详情

class热更新(代码片段)

class热更新class热更新功能与特性maven坐标使用说明功能与特性支持基于java源码,走文件进行class热更新支持基于java源码,走内存进行class热更新支持基于class字节码,走文件进行class热更新支持基于class字节码,走... 查看详情

class热更新(代码片段)

class热更新class热更新功能与特性maven坐标使用说明功能与特性支持基于java源码,走文件进行class热更新支持基于java源码,走内存进行class热更新支持基于class字节码,走文件进行class热更新支持基于class字节码,走... 查看详情

xlua热更新实现热更新(代码片段)

一、环境配置要实现热更新功能,我们首先需要开启热更新的宏。操作方法是在「File->BuildSettings->PlayerSettings->Player->OtherSettings->ScriptingDefineSymbols」选项中添加HOTFIX_ENABLE开启后,在xLua的菜单中就出现了「HotfixIn... 查看详情

巧用import.meta实现热更新(代码片段)

...xff0c;它包含了这个模块的信息。import.meta对象是由ECMAScript实现的,它带有一个null的原型对象。这个对象可以扩展,并且它的属性都是可写,可配置和可枚举的。<scripttype="module&# 查看详情

cocoscreator热更新[lv.1](代码片段)

...做深入研究。以实际操作做为出发点,帮助读者快速实现并且掌握热更新。系列文章CocosCreator热 查看详情

(十四)配置的热更新(代码片段)

...的信息,网站也能同步更改。还是继续沿用OptionsBindSample实现也比较简单,改下Index.cshtml1@usingMicrosoft.Extensions.Options2@injectIOptionsSnapshot<OptionsBindSample.Class>ClassAccesser//原来的IOption 查看详情

uniapp实现热更新(代码片段)

...软件版本控制的方式,在uniapp进行使用热更新将软件实现更新操作思路:服务器中存储着最新版本号,前端进行查询可以在首次进入应用时进行请求版本号进行一个匹对如果版本号一致则不提示,反之则提示进行更新... 查看详情

typescript服务端热更新(代码片段)

...ypeScript编写的项目并不需要使用webpack或者babel进行编译。实现热更新功能只需要额外两个库:concurrently和nodemon。concurrently的用处是同时运行多个命令,不使用concurrently用npmruncommand1&np 查看详情

nacos配置管理——配置热更新(代码片段)

...中无需重启即可让配置生效,也就是配置热更新。要实现配置热更新,可以使用两种方式:1.方式一在@Value注入的变量所在类上添加注解@RefreshScope:& 查看详情

热加载原理解析与实现(代码片段)

...:热加载可以在修改完代码后,不重启应用,实现类信息更新,以节省开发时等待启动时间。本文主要从热加载概念、原理、常见框架、实现等角度为你揭开热加载的层层面纱。一.热部署与热加载概念:热部... 查看详情

热加载原理解析与实现(代码片段)

...:热加载可以在修改完代码后,不重启应用,实现类信息更新,以节省开发时等待启动时间。本文主要从热加载概念、原理、常见框架、实现等角度为你揭开热加载的层层面纱。一.热部署与热加载概念:热部... 查看详情

groovy实现热部署(代码片段)

Groovy实现热部署一、概述二、准备工作2.1规则接口IRule三、非Spring环境Groovy文件方式3.1Groovy文件3.2读取并生成实例3.3使用这个实现四、数据库Groovy脚本方式4.1Groovy脚本4.2读取并生成实例五、Spring中使用Groovy的方式5.1Groovy文件5.2读... 查看详情

浅谈node.js热更新(代码片段)

...起步的时候,我在去前东家的入职面试也被问到了要如何实现Node.js服务的热更新。其实早期从Php-fpm/Fast-cgi转过来的Noder,肯定非常喜欢这种更新业务逻辑代码无需重启服务器即可生效的部署方案,它的优势也非常明显:无需重... 查看详情

牛叉了-arthas热更新mybatismapperxml(代码片段)

测试环境能够热更新class能否热更新mapperxml?arthas群有同学提了这样的一个需求,必须满足满足arthas-idea-plugin2.8版本https://plugins.jetbrains.com/plugin/13581-arthas-idea1、基本思路1.1流程图1.2实现效果echo`redis-cli-h'127.0.0.1'-p6 查看详情

用ecmascript4(actionscript3)实现unity的热更新--热更新live2d(代码片段)

 live2D是一个很强大的2D动画组件。我们可以使用AS3脚本对它进行热更新。live2D在Unity中的使用请看这里:如何获取Live2D总得来说,我们可以先去live2D官网下载它的UnitySDK,然后即可在Unity中使用。我们这里使用的是live2d2.1版。我... 查看详情

用ecmascript4(actionscript3)实现unity的热更新--使用fairygui(代码片段)

上次讲解了FairyGUI的最简单的热更新办法,并对其中一个Demo进行了修改并做成了热更新的方式。这次我们来一个更加复杂一些的情况:Emoji.FairyGUI的 Example04-Emoji场景是一个聊天对话框。玩家可以输入文本和表情,对面的机器... 查看详情