关注我的人可能知道,我在去年就分享了一篇深入理解JVM读书笔记,这里面就有一篇热替换相关的内容。机缘巧合之下,刚到新公司,这周周会轮到我进行技术分享了,于是我就写了这篇文章在会议上讲解,反响还不错
什么是热替换
热替换是在不停止正在运行的系统的情况下进行类(对象)的升级替换;
默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class
复习类加载机制(来自深入理解JVM)
类加载的时机
JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作。也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。那么什么是主动引用,什么是被动引用呢?
- 主动引用
- 遇到 new、getstatic、putstatic、invokestatic 字节码指令,例如:
- 使用 new 实例化对象;
- 读取或设置一个类的 static 字段(被 final 修饰的除外);
- 调用类的静态方法。
- 对类进行反射调用;
- 初始化一个类时,其父类还没初始化(需先初始化父类);
- 这点类与接口具有不同的表现,接口初始化时,不要求其父接口完成初始化,只有真正使用父接口时才初始化,如引用父接口中定义的常量。
- 虚拟机启动,先初始化包含 main() 函数的主类;
- JDK 1.7 动态语言支持:一个 java.lang.invoke.MethodHandle 的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic。
- 遇到 new、getstatic、putstatic、invokestatic 字节码指令,例如:
- 被动引用
- 通过子类引用父类静态字段,不会导致子类初始化;
Array[] arr = new Array[10];
不会触发 Array 类初始化;static final VAR
在编译阶段会存入调用类的常量池,通过ClassName.VAR
引用不会触发 ClassName 初始化。
也就是说,只有发生主动引用所列出的 5 种情况,一个类才会被加载到内存中,也就是说类的加载是 lazy-load 的,不到必要时刻是不会提前加载的,毕竟如果将程序运行中永远用不到的类加载进内存,会占用方法区中的内存,浪费系统资源。
类的显式加载和隐式加载
- 显示加载:
- 调用
ClassLoader#loadClass(className)
或Class.forName(className)
。 - 两种显示加载 .class 文件的区别:
Class.forName(className)
加载 class 的同时会初始化静态域,ClassLoader#loadClass(className)
不会初始化静态域;- Class.forName 借助当前调用者的 class 的 ClassLoader 完成 class 的加载。
- 调用
- 隐式加载:
- new 类对象;
- 使用类的静态域;
- 创建子类对象;
- 使用子类的静态域;
- 其他的隐式加载,在 JVM 启动时:
- BootStrapLoader 会加载一些 JVM 自身运行所需的 Class;
- ExtClassLoader 会加载指定目录下一些特殊的 Class;
- AppClassLoader 会加载 classpath 路径下的 Class,以及 main 函数所在的类的 Class 文件。
类加载的过程
类的生命周期
加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载
|<------- 连接 ------->|
|<------------- 类加载 ---------------->|
类的生命周期一共有 7 个阶段,其中前五个阶段较为重要,统称为类加载,第 2 ~ 4 阶段统称为连接,加载和连接中的三个过程开始的顺序是固定的,但是执行过程中是可以交叉执行的。接下来,我们将对类加载的 5 个阶段进行一一讲解。
加载
加载的 3 个阶段
- 通过类的全限定名获取二进制字节流(将 .class 文件读进内存);
- 将字节流的静态存储结构转化为运行时的数据结构;
- 在内存中生成该类的 Class 对象;
- HotSpot 虚拟机把这个对象放在方法区,非 Java 堆。
分类
- 非数组类
- 系统提供的引导类加载器
- 用户自定义的类加载器
- 数组类
- 不通过类加载器,由 Java 虚拟机直接创建
- 创建动作由 newarray 指令触发,new 实际上触发了
[L全类名
对象的初始化 - 规则
- 数组元素是引用类型
- 加载:递归加载其组件
- 可见性:与引用类型一致
- 数组元素是非引用类型
- 加载:与引导类加载器关联
- 可见性:public
- 数组元素是引用类型
验证
- 目的: 确保 .class 文件中的字节流信息符合虚拟机的要求。
- 4 个验证过程:
- 文件格式验证:是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数”
0xCAFEBABE
- 元数据验证:保证字节码描述信息符号 Java 规范(语义分析)
- 字节码验证:程序语义、逻辑是否正确(通过数据流、控制流分析)
- 符号引用验证:对类自身以外的信息(常量池中的符号引用)进行匹配性校验, 判断该类是否缺少或者被禁止访问它依赖的某些外部类、 方法、 字段等资源, 目的是确保解析行为能正常执行.
- 文件格式验证:是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数”
- 这个操作虽然重要,但不是必要的,可以通过
-Xverify:none
关掉。
准备
-
描述: 为 static 变量在方法区分配内存。
-
static 变量准备后的初始值:
-
public static int value = 123;
- 准备后为 0,value 的赋值指令 putstatic 会被放在
<clinit>()
方法中,<clinit>()
方法会在初始化时执行,也就是说,value 变量只有在初始化后才等于 123。
- 准备后为 0,value 的赋值指令 putstatic 会被放在
-
public static final int value = 123;
- 准备后为 123,因为被
static final
赋值之后 value 就不能再修改了,所以在这里进行了赋值之后,之后不可能再出现赋值操作,所以可以直接在准备阶段就把 value 的值初始化好。
- 准备后为 123,因为被
-
解析
-
描述:
将常量池中的 “符号引用” 替换为 “直接引用”。
- 在此之前,常量池中的引用是不一定存在的,解析过之后,可以保证常量池中的引用在内存中一定存在。
-
什么是 “符号引用” 和 “直接引用” ?
- 符号引用:以一组符号描述所引用的对象(如对象的全类名),引用的目标不一定存在于内存中。
- 直接引用:直接指向被引用目标在内存中的位置的指针等,也就是说,引用的目标一定存在于内存中。
- 解析动作主要针对类或接口、 字段、 类方法、 接口方法、 方法类型、 方法句柄和调用点限定符这7
类符号引用进行
初始化
- 描述: 执行类构造器
<clinit>()
方法的过程。 <clinit>()
方法- 包含的内容:
- 所有 static 的赋值操作;
- static 块中的语句;
<clinit>()
方法中的语句顺序:- 基本按照语句在源文件中出现的顺序排列;
- 静态语句块只能访问定义在它前面的变量,定义在它后面的变量,可以赋值,但不能访问。
- 与
<init>()
方法的不同:- 不需要显示调用父类的
<clinit>()
方法; - 虚拟机保证在子类的
<clinit>()
方法执行前,父类的<clinit>()
方法一定执行完毕。- 也就是说,父类的 static 块和 static 字段的赋值操作是要先于子类的。
- 不需要显示调用父类的
- 接口与类的不同:
- 执行子接口的
<clinit>()
方法前不需要先执行父接口的<clinit>()
方法(除非用到了父接口中定义的 public static final 变量);
- 执行子接口的
- 执行过程中加锁:
- 同一时刻只能有一个线程在执行
<clinit>()
方法,因为虚拟机要保证在同一个类加载器下,一个类只被加载一次。
- 同一时刻只能有一个线程在执行
- 非必要性:
- 一个类如果没有任何 static 的内容就不需要执行
<clinit>()
方法。
- 一个类如果没有任何 static 的内容就不需要执行
- 包含的内容:
注:初始化时,才真正开始执行类中定义的 Java 代码。
类加载器
如何判断两个类 “相等”
- “相等” 的要求
- 同一个 .class 文件
- 被同一个虚拟机加载
- 被同一个类加载器加载
- 判断 “相等” 的方法
instanceof
关键字- Class 对象中的方法:
equals()
isInstance()
isAssignableFrom()
类加载器的分类
- 启动类加载器(Bootstrap)
- <JAVA_HOME>/lib
- -Xbootclasspath 参数指定的路径
- 扩展类加载器(Extension)
- <JAVA_HOME>/lib/ext
- java.ext.dirs 系统变量指定的路径
- 应用程序类加载器(Application)
- -classpath 参数
双亲委派模型
- 工作过程
- 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器
- 因此,所有的类加载请求,都会先被传送到启动类加载器
- 只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载
- 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器
- 实现
- 检查该类是否已经被加载
- 将类加载请求委派给父类
- 如果父类加载器为 null,默认使用启动类加载器
parent.loadClass(name, false)
- 当父类加载器加载失败时
- catch ClassNotFoundException 但不做任何处理
- 调用自己的 findClass() 去加载
- 我们在实现自己的类加载器时只需要
extends ClassLoader
,然后重写findClass()
方法而不是loadClass()
方法,这样就不用重写loadClass()
中的双亲委派机制了
- 我们在实现自己的类加载器时只需要
- 优点
- Java中的类随着它的类加载器一起具备了带有优先级的层次关系,例如类java.lang.Object,他存放在rt.jar之中,无论哪一个类加载器都要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序各种类加载环境中都能保证是同一个类,反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个Object的类,并放在ClassPath中,那么会出现多个不同的Object类,程序会一片混乱
- 破坏双亲委派模型
- 重写loadClass()
- 三个原则
- 「委托性原则」 体现在当子类加载器收到类的加载请求时,会将加载请求向上委托给父类加载器。
- 「可见性原则」 体现在允许子类加载器查看父类加载器加载的所有类,但是父类加载器不能查看子类加载器加载的类。
- 「唯一性原则」 体现在双亲委派整个机制保证了Java类的唯一性,假如你写了一个和JRE核心类同名的类,比如Object类,双亲委派机制可以避免自定义类覆盖核心类的行为,因为它首先会将加载类的请求,委托给ExtClassLoader去加载,ExtClassLoader再委托给BootstrapClassLoader,启动类加载器如果发现已经加载了 Object类,那么就不会加载自定义的Object类。
热替换需要解决的问题
-
同一个类只会被加载一次
-
双亲委派模型
-
每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
实现原理
- 这要求虚拟机中要存在同一个类的两个不同版本。可我们知道,我们是无法将同一个类加载两遍的,想要实现这点,我们需要让虚拟机认为这是两个不同的类,即用两个不同的类加载器去加载这个类不同版本的 class 文件;
- 因此,这个工作就不能由系统提供给我们的启动类加载器,扩展类加载器或者应用程序类加载器来完成,因为这三个类加载器在同一个虚拟机中只有一份,不仅如此,我们还要跳过这些类加载器;
- 想要跳过这些类加载器可不是只要不用这些类加载器就行了,还需要我们跳过双亲委派模型,否则类的加载还会被委派到这些个类加载器,如果恰好某个类之前是由这三个类加载器中的一个加载的,虚拟机就不会再次加载新版本的类了,就无法实现类的热替换了。
实现简单的 Java 类热替换
需求分析
现有一 Foo 类,可以在控制台持续打印:Hello world! version one,我们将在该类运行时,将其 .class 文件替换为修改后的 Foo 类的 .class 文件,修改后的 Foo 会在控制台持续打印:Hello world! version two。也就是说,替换之后,控制台打印的内容发生变化,就说明类的热替换实现成功。
Foo 类的实现:
public class Foo {
public void sayHello() {
System.out.println("Hello world! version one");
// System.out.println("Hello world! version two"); // 之后替换成这个
}
}
然后我们通过如下程序运行 Foo 类:
public class Task extends TimerTask {
@Override
public void run() {
String basePath = "C:\\Users\\Bean\\IdeaProjects\\USTJ\\target\\classes";
// 每执行一次任务都 new 一个新的类加载器
HotswapClassLoader cl = new HotswapClassLoader(
basePath, new String[]{"com.jvm.ch7.hotswap.Foo"});
try {
// 通过我们自己实现的类加载器加载 Foo 类
Class cls = cl.loadClass("com.jvm.ch7.hotswap.Foo", true);
Object foo = cls.newInstance();
Method method = cls.getMethod("sayHello", new Class[]{});
method.invoke(foo, new Object[]{});
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Task(), 0, 1000);
}
}
实现类加载器
HotswapClassLoader
的实现如下,具体的讲解已被写入注释中:
public class HotswapClassLoader extends ClassLoader {
private String basePath;
private HashSet<String> loadedClass; // 用来记录被这个类加载器所加载的类
public HotswapClassLoader(String basePath, String[] classList) {
// 跳过父类加载器,把它设为null
super(null);
this.basePath = basePath;
loadedClass = new HashSet<>();
// 该类加载器在初始化的时候会直接把应该它负责加载的类加载好,
// 这样之后 loadClass 时,会在第一步检验该类是否已经被加载时发现该类已经被加载过了,
// 就无需执行 loadClass 之后的流程,直接返回虚拟机中被加载好的类即可,
// 这样虽然初始化的时间长了点,但是之后 loadClass 时会比较省时间
loadClassByMe(classList);
}
/**
* 加载给定的的 classList 中的类到虚拟机
*/
private void loadClassByMe(String[] classList) {
for (int i = 0; i < classList.length; i++) {
Class cls = loadClassDirectly(classList[i]);
if (cls != null) {
loadedClass.add(classList[i]);
}
}
}
/**
* 通过文件名直接加载类,得到Class对象
*/
private Class loadClassDirectly(String className) {
Class cls = null;
StringBuilder sb = new StringBuilder(basePath);
String classPath = className.replace(".", File.separator) + ".class";
sb.append(File.separator + classPath);
File file = new File(sb.toString());
InputStream fin = null;
try {
fin = new FileInputStream(file);
// 将字节流转化成内存中的Class对象
cls = instantiateClass(className, fin, (int) file.length());
return cls;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 将字节流转化成内存中的Class对象啊,使用defineClass方法!
*/
private Class instantiateClass(String name, InputStream fin, int len) {
byte[] buffer = new byte[len];
try {
fin.read(buffer);
return defineClass(name, buffer, 0, len);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 覆盖原有的loadClass规则,
*/
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class cls = null;
// 应该由 HotswapClassLoader 负责加载的类会通过下面这一行得到类的 Class 对象,
// 因为早在 HotswapClassLoader 类加载器执行构造函数时,它们就被加载好了
cls = findLoadedClass(name);
// 只有在这个类没有被加载,且!这个类不是当前这个类加载器负责加载的时候,才去使用启动类加载器
if (cls == null && !loadedClass.contains(name)) {
cls = findSystemClass(name);
}
if (cls == null) {
throw new ClassNotFoundException(name);
}
// resolveClass是进行连接操作的,即"验证+准备+解析",之后就可以进行初始化了
if (resolve) {
resolveClass(cls);
}
return cls;
}
}
有什么用?
在线升级系统
(项目运行时就进行版本的更新迭代)
- 一个升级服务,管理在线版本升级,当需要进行版本升级时,向工作服务队列发送升级命令
- n个工作服务,接收到升级命令后,更改一个标志位,可进行在线升级的模块察觉到标志位更改后就主动进行类的热替换
- 需要有一个缓存map来存储接口和替换后实现类之间的关系,如果用Spring那么就手动refresh,让他自动管理新的class
JarsLink
通过动态加载模块来进行模块化开发,如阿里巴巴开源框架(适合对外依赖低的模块,可进行快速热拔插)(https://www.cnblogs.com/evan-liang/p/12233942.html)
-
JarsLink (原名Titan)是一个基于JAVA的模块化开发框架,它提供在运行时动态加载模块(一个JAR包)、卸载模块和模块间调用的API。
-
核心代码:后台线程5秒一次扫描jar配置版本,对增删改的jar进行重新加载,然后重新初始化Spring容器上下文
-
JarsLink通过独立的类加载器和Spring上下文实现
类隔离:框架为每个模块的Class使用单独的ClassLoader来加载,每个模块可以依赖同一种框架的不同的版本。
实例隔离:框架为每个模块创建了一个独立的Spring上下文,来加载模块中的BEAN,实例化失败不会影响其他模块。 -
用途:模块层级的热替换保证服务的连续性(不需要像集群部署依靠可用性一个一个重启)、跨应用内部通信,支持应用发布引用 JVM 服务,跨应用既可以使用 RPC 框架,也可以走内部 JVM 服务进行通信.
-
在蚂蚁金服内部,在同一个 JVM 之上部署多个应用,是一件常见的事情。这样带来的主要优势如下:
- 无关应用合并部署:有些应用在独立部署时,相互之间没有服务依赖,而且这些应用承担业务体量都偏小,单独启动 Java 虚拟机比较浪费资源, 将这些应用合并部署,能够节省资源。
- 相关应用合并部署:有些应用之间存在服务依赖,独立部署时,各应用之间使用 RPC 调用,虽然使用了分布式架构,稳定性高,但依然存在网络抖动导致的延时性问题。这些应用合并部署,RPC 调用优先转为 JVM 内部调用,缩减调用开销。
不仅应用间存在合并部署,近端包也有同样的诉求。
近端包是提供一系列公共服务的三方组件,一般由应用作为依赖引入,这种开发模式容易导致两个问题:
- 近端包引入的三方依赖和应用本身的依赖产生冲突,期望能做到隔离部署。
- 近端包由应用作为依赖引入,因此近端包的任何升级改造都需要应用配合升级。但是作为一个公共的功能组件,近端包通常会被很多业务方应用依赖,此时推动业务方改造工作量巨大,因此期望能做到近端包的动态升级。
除了合并部署,蚂蚁金服很多业务场景需要模块的热部署,即在应用运行时,需要动态替换某特定模块而不影响其他模块的正常运行。
Tomcat的热更新
需要在配置文件中开启,不过一般不会开启,额外消耗资源并且容易导致方法区的OOM
-
public void backgroundProcess() { // 如果reloadble=true而且有.class文件被修改过,就会重启上下文件。 if (reloadable && modified()) { try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (context != null) { context.reload(); } } finally { if (context != null && context.getLoader() != null) { Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); } } } }
这个后台线程扫.class文件,如果有修改就使用WebappClassloader去动态加载被修改过的class到JVM中:context.reload()重启上下文。
我们现在有一个A类,一个自定义的WebappClassloader类(打破了双亲委派,尝试自己加载),一开始先用一个WebappClassloader实例加载A类,那么在jvm中就会存在一个A类的class对象,然后进行热加载,先停止上下文,再启动,在停止的时候会杀掉当前应用的所有线程(除开真正执行代码的线程),再启动时又会生成一个WebappClassloader实例来加载A类并缓存起来,如果热加载之前的那个A类的class对象还没有被回收的话,那么此时jvm中其实会存在两个A类的class对象,这是不冲突,因为class对象的唯一标志是类加载器实例对象+类的全限定名。
重启上下文之后,新的servlet对象会被缓存起来,每当有新的请求,会使用这些新的对象。
Jrebel热更新插件
启动远程服务器的热更新,配置好之后,本地代码更改的同时,远程服务器也会同步更改,并热替换更改后的class:https://www.cnblogs.com/sfnz/p/14157833.html
SPI机制
https://www.jianshu.com/p/3a3edbcd8f24
https://www.cnblogs.com/chanshuyi/p/deep_insight_java_spi.html
-
JDBC、Dubbo等的实现都是基于SPI的机制实现的
-
服务的提供者会在自己服务的文件夹META-INF/services下添加服务接口命名的文件,内容是实现类全限定名,并且在实现类的静态代码块中向服务管理器中注册(放入缓存),其中需要服务发现的项目,主动调用ServiceLoader的load(一般在静态代码块中使用),将各实现类通过遍历加载到内存时会触发静态代码块的注册,将实现类放入缓存,使用的地方遍历该缓存结构即可。
-
SPI的热替换:每次调用ServiceLoader的load都会重新读取文件里的内容,使用新的线程上下文类加载器加载实现类,这样就实现了热替换。
-
线程上下文类加载器的由来(https://www.jianshu.com/p/52dbe782ab8a)
Java 提供了很多SPI,允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
ThreadContextClassLoader;
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader 也叫 AppClassLoader)**来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。这时就可以使用线程上下文加载器(在Launch中设置了AppClassLoader)来加载实现类,这样破坏了一部分双亲委派原则(如果一个类引用了另外一个类,那么这两个类的类加载器就必须是一样的。)
注:现在jdbc2.0开始用DataSource来替代DriverManager,如果使用了Druid连接池,那么在Spring中需要配置xml,如果是Springboot那么需要在配置文件中配置spring:datasource:url和username和password,然后Spring初始化容器时,会注入DruidDataSource类,初始化的时候会调用ServiceLoader启动spi机制。
扩展
实现力扣、牛客等在线判题系统
- 将字符串通过SimpleJavaFileObject动态编译成字节码(字节数组)
- 修改字节码中的静态常量依赖如Scanner/System路径(将标准输入输出缓存)
- new一个自定义类加载器,加载该字节码,生成Class对象
- 通过反射调用Class对象的main方法
- 返回标准输出中的内容给客户端
通过cglib动态增加实体类的字段
public class ReflectUtil {
private static Logger logger = LoggerFactory.getLogger(ReflectUtil.class);
public static Object getTarget(Object dest, Map<String, Object> addProperties) {
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
PropertyDescriptor[] descriptors = propertyUtilsBean.getPropertyDescriptors(dest);
Map<String, Class> propertyMap = new HashMap<>();
for (PropertyDescriptor d : descriptors) {
if (!"class".equalsIgnoreCase(d.getName())) {
propertyMap.put(d.getName(), d.getPropertyType());
}
}
addProperties.forEach((k, v) -> {
String sclass = v.getClass().toString();
if (sclass.equals("class java.util.Date")) {//对日期进行处理
propertyMap.put(k, Long.class);
} else {
propertyMap.put(k, v.getClass());
}
});
DynamicBean dynamicBean = new DynamicBean(dest.getClass(), propertyMap);
propertyMap.forEach((k, v) -> {
try {
if (!addProperties.containsKey(k)) {
dynamicBean.setValue(k, propertyUtilsBean.getNestedProperty(dest, k));
}
} catch (Exception e) {
logger.error("动态添加字段出错", e);
}
});
addProperties.forEach((k, v) -> {
try {
String sclass = v.getClass().toString();
if (sclass.equals("class java.util.Date")) {//动态添加的字段为date类型需要进行处理
Date date = (Date) v;
dynamicBean.setValue(k, date.getTime());
} else {
dynamicBean.setValue(k, v);
}
} catch (Exception e) {
logger.error("动态添加字段值出错", e);
}
});
Object obj = dynamicBean.getTarget();
return obj;
}
}
class DynamicBean {
private Object target;
private BeanMap beanMap;
public DynamicBean(Class superclass, Map<String, Class> propertyMap) {
this.target = generateBean(superclass, propertyMap);
this.beanMap = BeanMap.create(this.target);
}
public void setValue(String property, Object value) {
beanMap.put(property, value);
}
public Object getValue(String property) {
return beanMap.get(property);
}
public Object getTarget() {
return this.target;
}
/**
* 根据属性生成对象
*/
private Object generateBean(Class superclass, Map<String, Class> propertyMap) {
BeanGenerator generator = new BeanGenerator();
if (null != superclass) {
generator.setSuperclass(superclass);
}
BeanGenerator.addProperties(generator, propertyMap);
return generator.create();
}
}
在云原生时代,热替换的作用越来越弱了,但也并不意味着这项技术彻底失去了意义,如果你有使用到的场景,请在评论区留言吧