浅谈Spring与字节码生成技术

    


 概要

今天来谈一谈我们熟知的Spring框架和字节码技术有什么联系。

Java程序员几乎都了解Spring。 它的IoC(依赖反转)和AOP(面向切面编程)功能非常强大、易用。而它背后的字节码生成技术(在运行时,根据需要修改和生成Java字节码的技术)就是一项重要的支撑技术。

Java字节码能够在JVM(Java虚拟机)上解释执行,或即时编译执行。其实,除了Java,JVM上的Groovy、Kotlin、Closure、Scala等很多语言,也都需要生成字节码。另外,playscript也可以生成字节码,从而在JVM上高效地运行!

而且,字节码生成技术很有用。 你可以用它将高级语言编译成字节码,还可以向原来的代码中注入新代码,来实现对性能的监测等功能。

目前,我就有一个实际项目的需求。我们的一个产品,需要一个规则引擎,解析自定义的DSL,进行规则的计算。这个规则引擎处理的数据量比较大,所以它的性能越高越好。因此,如果把DSL编译成字节码就最理想了。

既然字节码生成技术有很强的实用价值,那么今天,我就带你掌握它。

我会先带你了解Java的虚拟机和字节码的指令,然后借助ASM这个工具,生成字节码,最后,再实现从AST编译成字节码。通过这样一个过程,你会加深对Java虚拟机的了解,掌握字节码生成技术,从而更加了解Spring的运行机制,甚至有能力编写这样的工具!


Java虚拟机和字节码

字节码是一种二进制格式的中间代码,它不是物理机器的目标代码,而是运行在Java虚拟机上,可以被解释执行和即时编译执行。

在讲后端技术时,我强调的都是,如何生成直接在计算机上运行的二进制代码,这比较符合C、C++、Go等静态编译型语言。但如果想要解释执行,除了直接解释执行AST以外,我没有讲其他解释执行技术。

而目前更常见的解释执行的语言,是采用虚拟机,其中最典型的就是JVM,它能够解释执行Java字节码。

而虚拟机的设计又有两种技术:一是基于栈的虚拟机;二是基于寄存器的虚拟机。

标准的JVM是基于栈的虚拟机(后面简称“栈机”)。

每一个线程都有一个JVM栈,每次调用一个方法都会生成一个栈桢,来支持这个方法的运行。栈桢里面又包含了本地变量数组(包括方法的参数和本地变量)、操作数栈和这个方法所用到的常数。这种栈桢的设计跟之前我们学过C语言的栈桢的结构,其实有很大的相似性。

7a6597d5e4454069b836f9b234eeab6c.png

 栈机是基于操作数栈做计算的。 以“2+3”的计算为例,只要把它转化成逆波兰表达式,“2 3 +”,然后按照顺序执行就可以了。也就是: 先把2入栈,再把3入栈,再执行加法指令,这时,要从栈里弹出2个操作数做加法计算,再把结果压入栈。

15fe7581ac914530b3351fb38b5443ba.png

 你可以看出,栈机的加法指令,是不需要带操作数的,就是简单的“iadd”就行, 这跟你之前学过的IR都不一样。 为什么呢?因为操作数都在栈里,加法操作需要2个操作数,从栈里弹出2个元素就行了。

也就是说,指令的操作数是由栈确定的,我们不需要为每个操作数显式地指定存储位置,所以指令可以比较短, 这是栈机的一个优点。

接下来,我们聊聊字节码的特点。

字节码是什么样子的呢? 我编写了一个简单的类,其中的foo()方法实现了一个简单的加法计算,你可以看看它对应的字节码是怎样的:

public class MyClass {
    public int foo(int a){
        return a + 3;
    }
}

在命令行终端敲入下面两行命令,生成文本格式的字节码文件:

javac MyClass.java
javap -v MyClass > MyClass.bc

打开MyClass.bc文件,你会看到下面的内容片段:

public int foo(int);
  Code:
     0: iload_1     //把下标为1的本地变量入栈
     1: iconst_3    //把常数3入栈
     2: iadd        //执行加法操作
     3: ireturn     //返回

其中,foo()方法一共有四条指令,前三条指令是计算一个加法表达式a+3。这完全是按照逆波兰表达式的顺序来执行的: 先把一个本地变量入栈,再把常数3入栈,再执行加法运算。

如果你细心的话,应该会发现: 把参数a入栈的第一条指令,用的下标是1,而不是0。这是因为,每个方法的第一个参数(下标为0)是当前对象实例的引用(this)。

我提供了字节码中,一些常用的指令,增加你对字节码特点的直观认识,完整的指令集可以参见 JVM的规格书:

a57b6336bcd143fc8c4f6d05d0a81124.png

 其中,每个指令都是8位的,占一个字节,而且iload_0、iconst_0这种指令,甚至把操作数(变量的下标、常数的值)压缩进了操作码里,可以看出,字节码的设计很注重节省空间。

根据这些指令所对应的操作码的数值,MyClass.bc文件中,你所看到的那四行代码,变成二进制格式,就是下面的样子:

73b1161b2d504fd9b6cd9480314d6f25.png

 你可以用“hexdump MyClass.class”显示字节码文件的内容,从中可以发现这个片段(就是橙色框里的内容):

bc25c1be5cf44281812da63dd65f7b69.png

 现在,你已经初步了解了基于栈的虚拟机, 与此对应的是基于寄存器的虚拟机。 这类虚拟机的运行机制跟机器码的运行机制是差不多的,它的指令要显式地指出操作数的位置(寄存器或内存地址)。它的优势是: 可以更充分地利用寄存器来保存中间值,从而可以进行更多的优化。

例如,当存在公共子表达式时,这个表达式的计算结果可以保存在某个寄存器中,另一个用到该公共子表达式的指令,就可以直接访问这个寄存器,不用再计算了。在栈机里是做不到这样的优化的,所以基于寄存器的虚拟机,性能可以更高。而它的典型代表,是Google公司为Android开发的Dalvik虚拟机和Lua语言的虚拟机。

这里你需要注意, 栈机并不是不用寄存器,实际上,操作数栈是可以基于寄存器实现的,寄存器放不下的再溢出到内存里。只不过栈机的每条指令,只能操作栈顶部的几个操作数,所以也就没有办法访问其它寄存器,实现更多的优化。

现在,你应该对虚拟机以及字节码有了一定的了解了。那么,如何借助工具生成字节码呢?你可能会问了:为什么不纯手工生成字节码呢?当然可以,只不过借助工具会更快一些。

就像你生成LLVM的IR时,也曾获得了LLVM的API的帮助。所以,接下来我会带你认识ASM这个工具,并借助它为我们生成字节码。

字节码生成工具ASM

其实,有很多工具会帮我们生成字节码,比如Apache BCEL、Javassist等,选择ASM是因为它的性能比较高,并且它还被Spring等著名软件所采用。

ASM 是一个开源的字节码生成工具。Grovvy语言就是用它来生成字节码的,它还能解析Java编译后生成的字节码,从而进行修改。

ASM解析字节码的过程,有点像XML的解析器解析XML的过程:先解析类,再解析类的成员,比如类的成员变量(Field)、类的方法(Mothod)。在方法里,又可以解析出一行行的指令。

你需要掌握两个核心的类的用法:

  • ClassReader,用来解析字节码。

  • ClassWriter,用来生成字节码。

这两个类如果配合起来用,就可以一边读入,做一定修改后再写出,从而实现对原来代码的修改。

我们先试验一下,用ClassWriter生成字节码,看看能不能生成一个跟前面示例代码中的MyClass一样的类(我们可以称呼这个类为MyClass2),里面也有一个一模一样的foo函数。:

//创建foo方法
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "foo",
        "(I)I",    //括号中的是参数类型,括号后面的是返回值类型
        null, null);

//添加参数a
mv.visitParameter("a", Opcodes.ACC_PUBLIC);

mv.visitVarInsn(Opcodes.ILOAD, 1); //iload_1
mv.visitInsn(Opcodes.ICONST_3);    //iconst_3
mv.visitInsn(Opcodes.IADD);        //iadd
mv.visitInsn(Opcodes.IRETURN);     //ireturn

//设置操作数栈最大的帧数,以及最大的本地变量数
mv.visitMaxs(2,2);

//结束方法
mv.visitEnd();

从这个示例代码中,你会看到两个特点:

  1. ClassWriter有visitClass、visitMethod这样的方法,以及ClassVisitor、MethodVistor这样的类。这是因为ClassWriter用了visitor模式来编程。你每一次调用visitXXX方法,就会创建相应的字节码对象,就像LLVM形成内存中的IR对象一样。

  2. foo()方法里的指令,跟我们前面看到的字节码指令是一样的。

执行这个程序,就会生成MyClass2.class文件。

把MyClass2.class变成可读的文本格式之后,你可以看到它跟MyClass的字节码内容几乎是一样的,只有类名称不同。当然了,你还可以写一个程序调用MyClass2,验证一下它是否能够正常工作。

发现了吗?只要熟悉Java的字节码指令,在ASM的帮助下,你可以很方便地生成字节码!

既然你已经能生成字节码了,那么不如趁热打铁,把编译器前端生成的AST编译成字节码,在JVM上运行?因为这样,你就能从前端到后端,完整地实现一门基于JVM的语言了!

将AST编译成字节码

基于AST生成JVM的字节码的逻辑还是比较简单的,比生成针对物理机器的目标代码要简单得多,为什么这么说呢?主要有以下几个原因:

  • 首先,你不用太关心指令选择的问题。针对AST中的每个运算,基本上都有唯一的字节码指令对应,你直白地翻译就可以了,不需要用到树覆盖这样的算法。

  • 你也不需要关心寄存器的分配,因为JVM是使用操作数栈的;

  • 指令重排序也不用考虑,因为指令的顺序是确定的,按照逆波兰表达式的顺序就可以了;

  • 优化算法,你暂时也不用考虑。

当然了,我们只实现了playscript的少量特性,不过,如果在这个基础上继续完善,你就可以逐步实现一门完整的,基于JVM的语言了。

Spring与字节码生成技术

我在开篇提到,Java程序员大部分都会使用Spring。Spring的IoC(依赖反转)和AOP(面向切面编程)特性几乎是Java程序员在面试时必被问到的问题,了解Spring和字节码生成技术的关系,能让你在面试时更轻松。

Spring的AOP是基于代理(proxy)的机制实现的。在调用某个对象的方法之前,要先经过代理,在代理这儿,可以进行安全检查、记日志、支持事务等额外的功能。

f93d9953fa9242fbbc82d4078aaaaa9c.png

 Spring采用的代理技术有两个: 一个是Java的动态代理(dynamic proxy)技术;一个是采用cglib自动生成代理,cglib采用了asm来生成字节码。

e7e5b6cce98345caad460e38010b6fb5.png

 Java的动态代理技术,只支持某个类所实现的接口中的方法。如果一个类不是某个接口的实现,那么Spring就必须用到cglib,从而用到字节码生成技术来生成代理对象的字节码。

总结

本文主要讲解了字节码生成技术。字节码生成技术是Java程序员非常熟悉的Spring框架背后所依赖的核心技术之一。如果想要掌握这个技术,你需要对Java虚拟机的运行原理、字节码的格式,以及常见指令有所了解。我想强调的重点如下:

  • 运行程序的虚拟机有两种设计:一个是基于栈的;一个是基于寄存器的。

基于栈的虚拟机不用显式地管理操作数的地址,因此指令会比较短,指令生成也比较容易。而基于寄存器的虚拟机,则能更好地利用寄存器资源,也能对代码进行更多的优化。

  • 你要能够在大脑中图形化地想象出栈机运行的过程,从而对它的原理理解得更清晰。

  • ASM是一个字节码操纵框架,它能帮你修改和生成字节码,如果你有这方面的需求,可以采用这样的工具。

我也建议Java程序员,多多了解JVM的运行机制,和Java字节码,这样会更好地把握Java语言的底层机制,从而更利于自己职业生涯的发展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/72886.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

乡村振兴指数与其30余个原始变量数据(2000-2022年)

乡村振兴是当下经济学研究的热点之一,对乡村振兴进行测度,是研究基础。测度乡村振兴水平的学术论文广泛发表在《数量经济技术经济研究》等顶刊上。整理了2000-2022年城市层面的乡村振兴指数与其30余个原始变量数据,供大家使用。 数据来源&…

(docker)mysql镜像拉取-创建容器-容器的使用【个人笔记】

【容器的第一次创建】 容器的第一次创建,需要先下载镜像,从 镜像拉取 0、可以搜索镜像的版本 docker search mysql1、先拉取MySQL的镜像,默认拉取最新版,使用下面的命令拉取mysql镜像 docker pull mysql也可以指定mysql的版本…

微服务06-分布式事务解决方案Seata

1、Seata 概述 Seata事务管理中有三个重要的角色: TC (Transaction Coordinator) - **事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。 TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。 RM (Resourc…

[C初阶笔记]P1

什么是C语言 1、机器语言(二进制)>汇编语言(助记符)>高级语言(C、C等) 2、c语言擅长底层软件开发(操作系统、驱动程序),并不意味着不能开发其他。 C语言更贴近操作…

Android多屏幕支持-Android12

Android多屏幕支持-Android12 1、概览及相关文章2、屏幕窗口配置2.1 配置xml文件2.2 DisplayInfo#uniqueId 屏幕标识2.3 adb查看信息 3、配置文件解析3.1 xml字段读取3.2 简要时序图 4、每屏幕焦点 android12-release 1、概览及相关文章 AOSP > 文档 > 心主题 > 多屏…

Mac M2 Pro安装使用Cocoapods

Mac Pro M2安装使用Cocoapods 在新公司要做iOS开发,所以在新电脑上安装Cocoapods 在升级gem,sudo gem update --system,和安装cocoapods时都遇到如下的提示: ERROR: While executing gem ... (Errno::EPERM)Operation not per…

关于使用pycharm遇到只能使用unittest方式运行,无法直接选择Run

相信大家可能都遇到过这个问题,使用pycharm直接运行脚本的时候,只能选择unittest的方式,能愁死个人 经过几次各种尝试无果之后,博主就放弃死磕了,原谅博主是个菜鸟 后来遇到这样的问题,往往也就直接使用cm…

最新Anaconda安装-保姆级教程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 随着人工智能的不断发展,机器学习这门技术也越来越重要,很多…

Linux源码剖析匿名共享内存shmem原理

如下问题如果都清楚了就不用看本文了: 1. shmem ram文件系统的初始化流程是怎样的 2. shmem思想上想复用基于文件的操作流程,实现上shmem也引入了一个文件,那么类似文件open会生成struct file,shmem的struct file怎么生成的 3.…

5款鲜为人知的国产办公软件,可私有化部署,保障内部数据安全

在企业内部办公环境中,如何保障内部数据安全是一项至关重要的事项。为了应对这个挑战,越来越多的企业选择使用私有化部署的办公软件来确保数据的自主性和安全性。下面我将分享5款鲜为人知的国产办公软件,均支持私有化部署,保障企业…

代理设计模式——静态代理和动态代理

代理模式 在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式,在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。 意图:为其他对象提…

limereport报表使用

在这里我使用报表是以报表的形式显示数据库的信息。所以首先需要准备的资料有:limereport源码,还有数据库,我这里使用的是qsqlite数据库。 1、下载limereport报表源码 2、运行自带的案例:demo_r1 3、点击 “Run Report Designer”…

PDF文件限制编辑怎么取消?

PDF文件设置了限制编辑,想要取消PDF文件的限制编辑,很简单,打开PDF编辑器,点击工具栏中的文件,选择属性按钮,进入到熟悉感界面之后,点击安全,然后我们点击权限下拉框,选择…

如何使用Spark/Flink等分布式计算引擎做网络入侵检测

如何使用Spark/Flink等分布式计算引擎做网络入侵检测 引言16 Distributed Abnormal Behavior Detection Approach Based on Deep Belief Network and Ensemble SVM Using Spark17 Spark configurations to optimize decision tree classification on UNSW-NB1518 A dynamic spa…

【Pytroch】基于支持向量机算法的数据分类预测(Excel可直接替换数据)

【Pytroch】基于支持向量机算法的数据分类预测(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.下载地址6.完整代码7.运行结果 1.模型原理 支持向量机(Support Vector Machine,SVM)是一种强大的监…

换架 3D 飞机,继续飞呀飞

相信大多数图扑 HT 用户都曾见过这个飞机的 Demo,在图扑发展的这十年,这个 Demo 是许多学习 HT 用户一定会参考的经典 Demo 之一。 这个 Demo 用简洁的代码生动地展示了 OBJ 模型加载、数据绑定、动画和漫游等功能的实现。许多用户参考这个简单的 Demo 后…

Redis进阶(4)——结合redis.conf配置文件深入理解 Redis两种数据持久化方案:RDB和AOF

目录 引出持久化方案RDBAOF Redis的持久化方案RDB如果采用docker stop关闭如果采用强制关闭 AOF参数设置混编方式的加载让aof进行重写 两种持久化方案的优缺点AOF优缺点RDB优势和劣势 总结 引出 1.Redis数据持久化的两种方式,RDB和AOF; 2.RDB采用二进制存储&#xf…

QT报表Limereport v1.5.35编译及使用

1、编译说明 下载后QT CREATER中打开limereport.pro然后直接编译就可以了。编译后结果如下图: 一次编译可以得到库文件和DEMO执行程序。 2、使用说明 拷贝如下图编译后的lib目录到自己的工程目录中。 release版本的重新命名为librelease. PRO文件中配置 QT …

抖音小程序实现less语言编译样式

1.在抖音开发工具中搜索扩展less 2. 然后点击小齿轮选择扩展设置 3. 然后在扩展设置中选择在settings.json中编辑# 4. 在settings.json中加入以下这段代码即可 // Easy LESS配置"less.compile": {"compress": false,//是否压缩"sourceMap": fal…

virtualBox桥接模式下openEuler镜像修改IP地址、openEule修改IP地址、openEule设置IP地址

安装好openEuler后,设置远程登入前,必不可少的一步,主机与虚拟机之间的通信要解决,下面给出详细步骤: 第一步:检查虚拟机适配器模式:桥接模式 第二步:登入虚拟机修改IP cd /etc/sysconfig/network-scripts vim ifcfg-enpgs3 没有vim的安装或者用vi代替:sudo dnf …