spring

当从编译到部署再到测试的开发周期花费太长时间时,人们希望能够及时替换正在运行的代码,而无需重新启动应用程序服务器并等待部署完成。 在这种情况下,像JRebel这样的商业解决方案或像Grails这样的开源框架就可以提供帮助。

JVM不支持开箱即用地替换代码,例如您可以使用Class.forName()动态加载类。 基本上,您有以下选择:

  • HotSwap:Java 1.4引入的技术,可让您在调试器会话中重新定义类。 这种方法非常有限,因为它仅允许您更改方法的主体,而不能添加新的方法或类。
  • OSGi:此技术使您可以定义包。 在运行时,可以用此捆绑包的较新版本替换捆绑包。
  • 一次性的类加载器:通过在模块的所有类上包装单独的类加载器,可以在模块的新版本可用时丢弃该类加载器并进行替换。
  • 使用Java代理检测类:Java代理可以在定义类之前进行检测。 这样,它可以将代码注入到已加载的类中,该类将此类与类文件的一个版本连接起来。 一旦有新版本可用,就会执行新代码。

Grails所采用的技术称为“弹簧加载”,并使用“ Java代理”方法来处理从文件系统而不是从jar文件加载的类。 但是,这在后台如何工作?

为了了解弹簧负载,我们建立了一个小样本项目,使我们可以更详细地研究该技术。 该项目仅包含两个类: Main类调用ToBeChanged类的print()方法,并Hibernate一会儿:

public static void main(String[] args) throws InterruptedException {while (true) {ToBeChanged toBeChanged = new ToBeChanged();toBeChanged.print();Thread.sleep(500);}
}

print()方法仅打印出一个版本,以便我们可以看到它已更改。 此外,我们还打印出堆栈跟踪,以查看随着时间的变化:

public void print() {System.out.println("V1");StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();for (StackTraceElement element : stackTrace) {System.out.println("\t" + element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber());}
}

启动应用程序时,我们必须使用选项javaagent提供包含Java Agent的jar文件。 当弹簧加载以验证者不喜欢的方式修改字节码时,我们必须通过将选项noverify传递给JVM来禁用字节码的验证。 最后,我们使用cp传递包含类文件的文件夹,并告诉JVM包含main()方法的类:

java -javaagent:springloaded-1.2.4.BUILD-SNAPSHOT.jar -noverify -cp target/classes com.martinsdeveloperworld.springloaded.Main

ToBeChanged类的版本从V1更新到V2并使用mvn package重建项目后,我们看到以下输出:

...
V1java.lang.Thread.getStackTrace:-1com.martinsdeveloperworld.springloaded.ToBeChanged.print:7com.martinsdeveloperworld.springloaded.Main.main:8
V2java.lang.Thread.getStackTrace:-1com.martinsdeveloperworld.springloaded.ToBeChanged$$EPBF0gVl.print:7com.martinsdeveloperworld.springloaded.ToBeChanged$$DPBF0gVl.print:-1com.martinsdeveloperworld.springloaded.ToBeChanged.print:-1com.martinsdeveloperworld.springloaded.Main.main:8
...

版本V1的stacktrace看起来像我们期望的那样。 从Main.main()调用方法ToBeChanged.print() 。 对于版本V2这是不同的。 在这里,方法ToBeChanged.print现在调用方法ToBeChanged$$DPBF0gVl.print() 。 另请注意,调用ToBeChanged.print()的行号已从8更改为-1,表示该行未知。

新的行号-1强烈表明Java代理已经以一种允许它调用新方法而不执行旧代码的方式来检测方法ToBeChanged.print() 。 为了证明这一假设,我在spring-loaded的代码中添加了一些日志记录语句,并添加了一个功能,可将每个插入的文件转储到本地硬盘上。 通过这种方式,我们可以检查ToBeChanged.print()后方法ToBeChanged.print()外观:

0 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type>3 ldc #72 <0>5 invokevirtual #85 <org/springsource/loaded/ReloadableType.changed>8 dup9 ifeq 42 (+33)12 iconst_113 if_icmpeq 26 (+13)16 new #87 <java/lang/NoSuchMethodError>19 dup20 ldc #89 <com.martinsdeveloperworld.springloaded.ToBeChanged.print()V>22 invokespecial #92 <java/lang/NoSuchMethodError.<init>>25 athrow26 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type>29 invokevirtual #56 <org/springsource/loaded/ReloadableType.fetchLatest>32 checkcast #58 <com/martinsdeveloperworld/springloaded/ToBeChanged__I>35 aload_036 invokeinterface #94 <com/martinsdeveloperworld/springloaded/ToBeChanged__I.print> count 241 return42 pop43 getstatic #100 <java/lang/System.out>46 ldc #102 <V1>48 invokevirtual #107 <java/io/PrintStream.println>51 invokestatic #113 <java/lang/Thread.currentThread>54 invokevirtual #117 <java/lang/Thread.getStackTrace>57 astore_1
...
152 return

getstatic操作码检索新字段r$type的值并将其压入堆栈(操作码ldc )。 然后,调用方法ReloadableType.changed()来调用之前已推入堆栈的对象引用。 顾名思义,方法ReloadableType.changed()检查此类型的新版本是否存在。 如果方法未更改,则返回0;如果方法已更改,则返回1。 如果返回值为零,即方法未更改,则以下操作码ifeq跳至第42行。 从第42行开始,我们看到了最初的实现,我在这里将其缩短了一点。

如果值为1,则if_icmpeq指令跳至第26行,在此再次读取静态字段r$type 。 此引用用于在其上调用方法ReloadableType.fetchLatest() 。 下面的checkcast指令验证返回的引用的类型为ToBeChanged__I 。 在这里,我们第一次偶然发现了这种弹簧接口为每种类型生成的人工接口。 它反映了原始类在进行检测时所具有的方法。 两行之后,此接口用于在ReloadableType.fetchLatest()返回的引用上调用方法print() ReloadableType.fetchLatest()

此引用不是对该类的新版本的引用,而是对所谓的调度程序的引用。 调度程序实现ToBeChanged__I接口,并通过以下指令实现方法print()

0 aload_1
1 invokestatic #21 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBF0gVl.print>
4 return

动态生成的类ToBeChanged$$EPBF0gVl是所谓的执行程序,体现了该类型的新版本。 对于每个新版本,都会创建一个新的调度程序和执行程序,只有接口保持不变。 一旦有新版本可用,则在新调度程序上调用接口方法,并且在最简单的情况下,该接口方法将转发至执行程序中包含的代码的新版本。 不能直接在执行器上调用接口方法的原因是,弹簧加载还可以处理在新版本的类中添加方法的情况。 由于此方法在旧版本中不存在,因此将通用方法__execute()添加到接口和调度程序。 然后,此动态方法可以将调用调度到新方法,如从生成的调度程序中获取的以下指令集中所示:

0 aload_31 ldc #25 <newMethod()V>3 invokevirtual #31 <java/lang/String.equals>6 ifeq 18 (+12)9 aload_2
10 checkcast #33 <com/martinsdeveloperworld/springloaded/ToBeChanged>
13 invokestatic #36 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBFaboY.newMethod>
16 aconst_null
17 areturn
18 aload_3
...
68 areturn

在这种情况下,我向类ToBeChanged添加了一个名为newMethod()的新方法。 __execute()方法的开头比较调用的描述符是否与新方法匹配。 在这种情况下,它将调用转发给新的执行者。 为了使此工作有效,必须将新方法的所有调用都重写为__execute()方法。 这也可以通过对原始类的检测来完成,并且也可以进行反射。

结论

Spring加载演示了可以在运行时用较新版本“替换”一个类。 为了实现这一点,利用了一系列Java技术,例如Java Agent和字节码检测。 通过仔细研究实现,可以大致了解有关JVM和Java的许多知识。

翻译自: https://www.javacodegeeks.com/2015/05/updating-code-at-runtime-spring-loaded-demystified.html

spring

spring_在运行时更新代码(已Spring解密)相关推荐

  1. 在运行时更新代码(已Spring解密)

    当从编译到部署再到测试的开发周期花费太长时间时,人们希望能够及时替换正在运行的代码,而无需重新启动应用程序服务器并等待部署完成. 在这种情况下,像JRebel这样的商业解决方案或像Grails这样的开 ...

  2. spring解密_在运行时更新代码(已Spring解密)

    spring解密 当从编译到部署再到测试的开发周期花费太长时间时,人们希望能够及时替换正在运行的代码,而无需重新启动应用程序服务器并等待部署完成. 在这种情况下,像JRebel这样的商业解决方案或像G ...

  3. [Unity脚本运行时更新]C#7新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第5篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

  4. python运行时修改代码会怎样_python运行时修改代码的方法——monkey patch

    monkey patch (猴子补丁) 用来在运行时动态修改已有的代码,而不需要修改原始代码. 简单的monkey patch 实现: [Python] #coding=utf-8 def origi ...

  5. [Unity脚本运行时更新]C#7.3新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第8篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

  6. [Unity脚本运行时更新]C#7.2新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第7篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

  7. [Unity脚本运行时更新]C#7.1新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第6篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

  8. [Unity脚本运行时更新]C#6新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第4篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

  9. [Unity脚本运行时更新]C#5新特性

    洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号. 本文是该系列<Unity脚本运行时更新带来了什么?>的第3篇. 洪流学堂公众号回复runtime,获取本系列所有文章. Unity2 ...

最新文章

  1. SSAS : 如何禁用SSAS的QueryLog
  2. mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
  3. 树莓派lnmp安装mysql_在树莓派上安装 LNMP
  4. 如何利用URLOS和云存储打造一个不惧怕宕机的网站环境
  5. 对Spring框架的理解(转)
  6. PHP 初学者资源收集
  7. vue引入自定义字体otf、ttf字体的方法
  8. 论文发表费用如何收费
  9. Legion 一款网络渗透工具
  10. 1024,20个入行故事,浮世中见证着程序员的奋斗
  11. 华为笔试题--直角三角形周长
  12. Electron--桌面应用开发(基本应用,快速入门)
  13. 2021年HECTF部分Writeup
  14. 优思学院|“元宇宙“是什么东西?
  15. mysql 唯一性榆树_榆树有什么特点?
  16. windows电影杀毒linux程序,两部Linux有关的电影:《操作系统革命》《代码》
  17. linux终端窗口如何切换快捷键,【linux基础】Ubuntu下的终端多标签切换快捷键
  18. 3B大战 GS很受伤
  19. 解决jdbcTemplate处理sql带in的多个参数问题
  20. OBS键盘插件自定义diy

热门文章

  1. ylb:使用sql语句实现添加、删除约束
  2. Java的最大优势还是跨平台么?
  3. PHP学习资源收集~
  4. 自定义SeekBarPreference控件(老外出品,直接在preferences文件中使用,无需其他代码)...
  5. Windows文件系统过滤驱动开发教程(4,5)
  6. myeclipse安装svn插件的多种方式
  7. python3 all any 判断迭代参数 是否全部 是否有 为true
  8. golang 执行命令 设置超时
  9. golang rune类型简介
  10. linux shell 替换字符串的几种方法,变量替换${},sed,awk