JAVA实现代码热更新

article/2025/8/27 6:16:55

JAVA实现代码热更新

  • 引言
  • 类加载器
  • 实现热更新思路
    • 多种多样的加载来源
    • SPI服务发现机制
  • 完整代码
  • 类加载器共享空间机制
  • Tomcat如何实现JSP的热更新
  • Spring反向访问用户程序类问题
  • 补充细节
  • 推荐资源


引言

本文将带领大家利用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       uint16name              string // thisClassNamesuperClassName    stringinterfaceNames    []stringconstantPool      *ConstantPoolfields            []*Fieldmethods           []*MethodsourceFile        stringloader            *ClassLoadersuperClass        *Classinterfaces        []*ClassinstanceSlotCount uintstaticSlotCount   uintstaticVars        SlotsinitStarted       booljClass            *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{...@Overrideprotected 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;}}@Overridepublic 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 加载得到的二进制文件流*/@Overridepublic 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 = newByteArrayOutputStream()) {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方法被修改为如下:

    @Overrideprotected 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;}}@Overridepublic 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机制寻找classDataLoaderspiService.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机制寻找classDataLoaderspiService.loadService(ClassDataLoader.class.getName());}@Overrideprotected 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;}}@Overridepublic 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);}/*** @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);}public static void registerClasDataLoader(ClassDataLoader classDataLoader){classDataLoaderList.add(classDataLoader);}public static void cacheUpdateHook(String name,byte[] classData){classBytesCache.put(name,classData);}
}
/*** @author 大忽悠* @create 2023/1/10 11:37*/
public interface ClassDataLoader {/*** @param name 全类名* @return 加载得到的二进制文件流*/byte[] loadClassData(String name);
}
package com;import java.io.*;/*** @author 大忽悠* @create 2023/1/10 11:48*/
public class FileClassDataLoader implements ClassDataLoader{/*** 默认从当前项目路径找起*/private String basePath="";static {DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());}/*** @param name 全类名* @return 加载得到的二进制文件流*/@Overridepublic 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 = newByteArrayOutputStream()) {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[] {};}
}
package com;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;/*** @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;}}

完整项目架构:
在这里插入图片描述


类加载器共享空间机制

请注意,在类加载器之间具有了委派关系,首先发起装载要求的类加载器不必是定义该类型的加载器。

实际定义了那个类型的类装载器被称为该类型的定义类装载器,要求某个类装载器去装载一个类型,但是却返回了其他类装载器装载的类型,这种装载器被称为是那个类型的初始类装载器。

在这里插入图片描述
被委派的类装载器装载的这个类型,会在所有被标记为该类型的初始类装载器的命名空间中共享。

最后通过一个案例理解一下类加载器共享空间机制:
在这里插入图片描述
测试:
在这里插入图片描述
当我们通过自定义的加载器加载A类时,首先会确保A类的父类和实现的接口都会被先被加载,类加载的简易版本代码如下所示:

func (self *ClassLoader) LoadClass(name string) *Class {if class, ok := self.classMap[name]; ok {// already loadedreturn class}var class *Class//加载数组对象需要区别对待if name[0] == '[' { // array classclass = self.loadArrayClass(name)} else {//class = self.loadNonArrayClass(name)}if jlClassClass, ok := self.classMap["java/lang/Class"]; ok {class.jClass = jlClassClass.NewObject()class.jClass.extra = class}return class
}func (self *ClassLoader) loadArrayClass(name string) *Class {class := &Class{accessFlags: ACC_PUBLIC, // todoname:        name,loader:      self,initStarted: true,superClass:  self.LoadClass("java/lang/Object"),interfaces: []*Class{self.LoadClass("java/lang/Cloneable"),self.LoadClass("java/io/Serializable"),},}self.classMap[name] = classreturn class
}func (self *ClassLoader) loadNonArrayClass(name string) *Class {data, entry := self.readClass(name)class := self.defineClass(data)link(class)if self.verboseFlag {fmt.Printf("[Loaded %s from %s]\n", name, entry)}return class
}func (self *ClassLoader) readClass(name string) ([]byte, classpath.Entry) {data, entry, err := self.cp.ReadClass(name)if err != nil {panic("java.lang.ClassNotFoundException: " + name)}return data, entry
}// jvms 5.3.5
func (self *ClassLoader) defineClass(data []byte) *Class {class := parseClass(data)hackClass(class)class.loader = selfresolveSuperClass(class)resolveInterfaces(class)self.classMap[class.name] = classreturn class
}func parseClass(data []byte) *Class {cf, err := classfile.Parse(data)if err != nil {//panic("java.lang.ClassFormatError")panic(err)}return newClass(cf)
}// jvms 5.4.3.1
func resolveSuperClass(class *Class) {if class.name != "java/lang/Object" {class.superClass = class.loader.LoadClass(class.superClassName)}
}
func resolveInterfaces(class *Class) {interfaceCount := len(class.interfaceNames)if interfaceCount > 0 {class.interfaces = make([]*Class, interfaceCount)for i, interfaceName := range class.interfaceNames {class.interfaces[i] = class.loader.LoadClass(interfaceName)}}
}

本例中很明显,Interface接口是被DynamicClassLoader委托给了父类系统类加载器进行加载的,因此DynamicClassLoader和系统类加载器是Interface的初始类加载器.

在这里插入图片描述

此处在启动类中使用Interface类型时,还是会交给当前系统类加载器进行加载,此时因为系统类加载器已经加载过了,因此直接返回先前加载得到的Class对象。

这里因为DynamicClassLoader负责加载的A类,其在内存中生成的class数据结构,其中

//假设这是类A在内存中对应的class数据结构
type Class struct {...//值为DynamicClassLoaderloader            *ClassLoader//Object--委派给了启动类加载器加载superClass        *Class//继承的Inteface委派给了系统类加载器加载interfaces        []*Class...
}

因为被委派的类装载器装载的这个类型,会在所有被标记为该类型的初始类装载器的命名空间中共享。

所以superClass指向的实际是启动类加载器命名空间下的Object Class内存数据结构:

//假设这是启动类加载器
type ClassLoader struct {cp          *classpath.ClasspathverboseFlag bool//A Class中的superClass实际是Object Class,指向的是启动类加载器命令空间下的同一个Object Class classMap    map[string]*Class // loaded classes
}
//假设这是系统类加载器
type ClassLoader struct {cp          *classpath.ClasspathverboseFlag bool//A Class中的interfaceClass实际是Interface Class,指向的是系统类加载器命令空间下的同一个Interface Class classMap    map[string]*Class // loaded classes
}

这也就是为什么执行checkCast强制类型转换指令不会报错的原因。

这部分内容没看明白的,可以去参考一下下面这本书的第8章内容:
在这里插入图片描述
在这里插入图片描述


Tomcat如何实现JSP的热更新

这里给出一张深入理解JVM虚拟机第三版中第9章关于tomcat 6之前的类加载器体系结构:
在这里插入图片描述
JasperLoader的加载范围仅仅是这个JSP文件编译出来的那一个class文件,当服务器检测到JSP文件修改时,会创建一个新的JasperLoader来加载被修改的JSP文件实现HotSwap功能。

注意: 每一个JSP文件都会被转换为一个Servlet类,JSP在JSP文件被解析后,得到对应的html字节流文件,servelt类负责将html字节流文件响应给浏览器.

所以过程就是 JSP–>JspServlet.java–>JspServlet.class–>JasperLoader加载—>因为JspServlet.class实现了Servlet相关规范接口,所以直接转换为对应的接口(为什么可以直接转换,参考上面案例),然后调用接口方法,完成响应内容输出。

大家联系上面我给出的案例,会发现其实本质是一样的,JSP热更新只需要每次重新创建一个JasperLoader实例,然后加载解析生成的JspServlet.class文件即可


Spring反向访问用户程序类问题

该问题是在深入理解JVM虚拟机第三版第9章被提出的:
在这里插入图片描述
要解决这个问题,我们还是先回顾一下Tomcat类加载器体系结构:
在这里插入图片描述
Tomcat的类加载体系结构并没有破坏原本的双亲委派机制,而是进一步扩展了原本的双亲链,WebApp类加载器负责加载/WebApp/WEB-INF下.class文件,而加载Spring jar包的工作是通过CommonClassLoader完成,CommonClassLoader作为WebAppClassLoader的父类加载器(是双亲链上的父类,不是继承上的),因此应用程序类可以委托父类加载器,访问到Spring jar下的类,但是目前Spring jar下的类似乎无法反向访问应用程序的类。

父类加载器将加载类请求委派给子类加载器做一做法显然打破了双亲委派机制,因此我们的思路就是如何打破双亲委派机制,让CommonClassLoader将加载应用程序类的请求委派给子类WebAppClassLoader呢?

思路有两种:

  • 自定义类加载器,重写loadClass方法,这里很难实现,因为Spring jar需要提前确定需要从哪里加载用户应用程序的类。
  • 通过线程上下文加载器,Spring设置自己的线程上下文加载器为WebAppClassLoader,如果后面需要加载用户程序的类,那么直接通过线程上下文加载器加载即可。

显然,如果Spring底层的servlet服务器实现是Tomcat,然后整合Tomcat时,只需要将自身当前线程上下文加载器设置为WebAppClassLoader即可。

提出一个问题: 什么情况下会出现spring需要访问用户程序类的场景呢?

  • spring从配置文件或者包扫描途径加载be时,肯定需要通过Class.forName(用户程序类的全类名)来加载用户程序类的

像JDK核心库中很多SPI顶层接口,如JDBC,JNDI等,这些顶层接口的具体实现由第三方厂商提供,这样做的好处是提供了高度抽象,应用程序则只需要面向接口编程即可,不用关心具体实现,但问题在于所有顶层接口都由JDK提供,加载这些接口的类加载器是根加载器,第三方厂商提供的类库驱动则是由系统类加载器加载的。因此此时去需要通过线程上下文类加载器,将加载第三方厂商实现类的加载请求委托为子类加载器实现。

线程上下文类加载器


补充细节

只有当前类加载器通过自己的defineClass加载某个class字节流到JVM完成加载后,才会将当前被自己加载的类放入自己的已加载类缓存中去,这样我们通过findLoadedClass方法才会从缓存中寻找到这个已经被当前类加载器加载的类。

如果当前类加载器无法加载A类,而是委托给父类加载器加载,那么父类加载成功后,会将A类加入自己的类缓存,当前类加载器不会将类A加入自己的已加载类缓存中。


推荐资源

这里推荐三本书: 深入理解JVM第三版 , 自己动手写JVM , 深入理解JVM虚拟机第二版 , 虚拟机设计与实现

文章:

【转】Java类加载器:类加载原理解析

Java类加载器:线程上下文类加载器

如果想彻底弄懂,还是推荐阅读源码,可以看别人的源码分析文章。


http://chatgpt.dhexx.cn/article/ag9oQdac.shtml

相关文章

cordova打包app热更新问题

定义&#xff1a; 基于 cordova 框架能将web应用 (js, html, css, 图片等) 打包成 App。当 App 在终端上安装后&#xff0c;不需要重新下载app&#xff0c;实现内壳更新。 原理&#xff1a;1.在项目根目录的config.xml文件中添加指向服务器的地址 2.在www目录中添加chcp.json配…

【热更新】游戏热更新方案

游戏热更新方案 热更新演化热更新方案【1】 进程切换1.1 利用fork、exec切换1.2 利用网关切换1.3 微服务- 进程切换注意要点 【2】 动态库替换【3】 脚本语言热更新热更新探究最简单的实现热更的方法最简单的实现热更的方法的局限性热更新全局替换模块方法的局限性 工程实现1. …

Addressable热更新

文章目录 前提配置代码实现 前提配置 &#xff08;1&#xff09;勾选AddressableAssetSettings设置的Disable Catalog Update On Startup选项 &#xff08;2&#xff09;相应的热更戏资源分组配置&#xff08;注&#xff1a;此文采用的是动态资源更新&#xff09; Can Change …

Nacos配置热更新的4种方式、读取项目配置文件的多种方式,@value,@RefreshScope,@NacosConfigurationProperties

nacos实现配置文件的热更新&#xff0c;服务不用重启即可读取到nacos配置中心修改发布后的最新值&#xff0c;spring&#xff0c;springboot项目读取本地配置文件的各种方式&#xff1b;文章中介绍了一下注解的使用&#xff1a;NacosConfigurationProperties&#xff0c;NacosP…

Unity 热更新技术 | (一) 热更新的基本概念原理及主流热更新方案介绍

&#x1f3ac; 博客主页&#xff1a;https://xiaoy.blog.csdn.net &#x1f3a5; 本文由 呆呆敲代码的小Y 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;Unity系统学习专栏 &#x1f332; 游戏制作专栏推荐&#xff1a;游戏制作 &…

JAVA热更新

引言 知识储备先看这篇文章&#xff1a;JAVA Instrument 在这个案例中我们会利用Instrument机制实现一个简单的热更新案例。 总体来说&#xff0c;步骤如下&#xff1a; 创建一个带premain方法的jar包。这个方法定时检测某个文件然后进行热更新。命令行启动业务类时使用参数…

热更新 深度解析

APP热更新方案 为什么要做热更新 当一个App发布之后&#xff0c;突然发现了一个严重bug需要进行紧急修复&#xff0c;这时候公司各方就会忙得焦头烂额&#xff1a;重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。 重点是还会有原来的版本遗留…

webpack热更新

什么是模热更新&#xff1f;有什么优点 模块热更新是webpack的一个功能&#xff0c;它可以使得代码修改之后&#xff0c;不用刷新浏览器就可以更新。 在应用过程中替换添加删出模块&#xff0c;无需重新加载整个页面&#xff0c;是高级版的自动刷新浏览器。 优点&#xff1a…

electron 热更新

1. electron自带的整体更新方式 &#xff08;全量更新&#xff09; 这种方式为electron官方的升级更新方式&#xff0c;主要是通过主进程中的autoUpdater模块进行检测升级更新的&#xff0c;此方式也是大家常见的大多数electron应用程序的更新方式。 检测到新版本后从服务器拉…

uniApp实现热更新

热更新 热更新是开发中常见且常用的一种软件版本控制的方式&#xff0c;在uniapp进行使用热更新将软件实现更新操作 思路: 服务器中存储着最新版本号&#xff0c;前端进行查询可以在首次进入应用时进行请求版本号进行一个匹对如果版本号一致则不提示&#xff0c;反之则提示进行…

Android热更新详解

一 前言介绍 正好最近又看到热更新&#xff0c;对以前Android 热修复核心原理&#xff1a;ClassLoader类加载机制做了点补充。 从16年开始开始&#xff0c;热修复技术开始在安卓界流行&#xff0c;它以classloader类加载机制为核心&#xff0c;可以不发布新版本就修复线上 bu…

热更新原理

对于热更新的问题就是了解两个点的问题&#xff1a; 如何加载补丁包&#xff0c;也就是如何加载dex 文件的过程&#xff08;dex是补丁包&#xff0c;更改的文件都在补丁包中&#xff09;修复后的类如何替换掉旧的类 通过这篇文章给大家介绍下我理解的热更新的逻辑&#xff0c…

Cocos Creator 3.x 热更新

前言&#xff1a;游戏做热更新 是基本需求&#xff1b; 好在 cocos-creator 已经为我们做好了方案&#xff0c;相对于 U3D 的热更新方案来说&#xff0c;使用起来很简便&#xff01;&#xff0c;不用关注很多细节 本文使用的是 cocos-creator 3.5.2 版本 官方文档 &#xff1…

热更新原理及实践注意

首先要说明几个概念&#xff0c;不要混用&#xff0c;热部署&#xff0c;热加载&#xff1b; 热部署&#xff1a;就是已经运行了项目,更改之后,不需要重新tomcat,但是会清空内存,重新打包,重新解压war包运行&#xff0c;可能好处是一个tomcat多个项目,不必因为tomcat停止而停止…

热更新你都知道哪些?

热更新系列目录 热更新你都知道哪些&#xff1f;热更新Sophix的爬坑之路腾讯热更新Tinker的故事阿里热更新Sophix的故事 Android热更新 前言1. 什么是热更新&#xff1f;2. 主流热更新方案3. 腾讯系热更新4. 阿里系热更新总结 博客创建时间&#xff1a;2020.05.16 博客更新时间…

热更新技术简易原理及技术推荐

为了照顾萌新童鞋&#xff0c;最开始还是对热更新的概念做一个通俗易懂的介绍。 热更新用通俗的讲就是软件不通过应用商店的软件版本更新审核&#xff0c;直接通过应用自行下载的软件数据更新的行为。在用户下载安装App之后&#xff0c;打开App时遇到的即时更新&#xff0c;是…

热更新及其原理

热更新&#xff1a;是app常用的更新方式&#xff0c;只需下载安装更新部分的代码 工作原理&#xff1a;动态下开发代码&#xff0c;使开发者在不发布新版本的情况下修复bug和发布功能&#xff0c;绕开苹果审核机制&#xff0c;避免长时间的审核以及多次被拒绝造成的成本。 优…

HTML/CSS实现小米官网搜索框效果

效果图&#xff1a; 需求分析&#xff1a; 1、输入框焦点事件 onfocus:成为焦点, 点击输入框的时候&#xff0c;出现闪烁光标&#xff0c;此时可以输入内容。 onblur :失去焦点, 点击页面空白区域&#xff0c;光标消失。此时不可以输入内容。 2、获取元素 3、注册事件 2.1…

html中的搜索代码,Web自动化(3):网页自动搜索功能

unsplash.jpg 写在前面 如果我们需要在期刊中搜索我们想要找的文章,那么我们如何才能达到这个目的。我们首先看一下,手动和自动对比图: 网页搜索.png 其实内容全部一样,我们只是用自动化程序,来代替我们手动操作。 1. 创建webdriver驱动对象,驱动打开网页 # 导入包 from …

java搜索代码_Java实现搜索功能代码详解

首先&#xff0c;我们要清楚搜索框中根据关键字进行条件搜索发送的是get请求&#xff0c;并且是向当前页面发送get请求 //示例代码 请求路径为当前页面路径 "/product" 当我们要实现多条件搜索功能时&#xff0c;可以将搜索条件封装为一个map集合&#xff0c;再根据m…