JAVA实现代码热更新

Posted 热爱编程的大忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA实现代码热更新相关的知识,希望对你有一定的参考价值。

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();
        
        classBytesCache以上是关于JAVA实现代码热更新的主要内容,如果未能解决你的问题,请参考以下文章

JAVA实现代码热更新

nodejs热更新

纯java代码实现热部署原理(不依赖任何第三方框架)

lua代码的检查

使用Arthas实现JAVA热更新

如何在OpenResty里实现代码热更新