浅谈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语言的底层机制,从而更利于自己职业生涯的发展。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

带你了解—使用内网穿透,公网远程访问本地硬盘文件

文章目录 前言1. 下载cpolar和Everything软件3. 设定http服务器端口4. 进入cpolar的设置5. 生成公网连到本地内网穿透数据隧道 总结 前言 随着云概念的流行,不少企业采用云存储技术来保存办公文件,同时,很多个人用户也感受到云存储带来的便利…

带你了解SpringBoot支持的复杂参数--自定义对象参数-自动封装

😀前言 本篇博文是关于SpringBoot 在响应客户端请求时支持的复杂参数和自定义对象参数,希望您能够喜欢😊 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章…

FPGA GTP全网最细讲解,aurora 8b/10b协议,OV5640摄像头板对板视频传输,提供2组4套工程源码和技术支持

目录 1、前言免责声明 2、我这里已有的 GT 高速接口解决方案3、GTP 全网最细解读GTP 基本结构GTP 发送和接收处理流程GTP 的参考时钟GTP 发送接口GTP 接收接口GTP IP核调用和使用 4、设计思路框架OV5640摄像头配置及采集视频数据组包GTP aurora 8b/10b数据对齐视频数据解包图像…

Oracle外部表ORACLE_LOADER方式加载数据

当数据源为文本或其它csv文件时,oracle可通过使用外部表加载数据方式,不需要导入可直接查询文件内的数据。 1、如下有一个文件名为:test1.txt 的数据文件。数据文件内容为: 2、使用sys授权hr用户可读写 DATA_PUMP_DIR 目录权限&a…

苹果支付的实现

由于app经常需要用到支付功能,然而ios用户,是无法用支付宝、微信进行支付,这时候只能用到苹果支付。苹果支付是苹果公司推出的一种在线支付方式,用户可以通过苹果支付购买应用、内购道具等等。 原理 苹果支付的实现原理是通过在…

力扣75——多维动态规划

总结leetcode75中的多维动态规划算法题解题思路。 上一篇:力扣75——一维动态规划 力扣75——多维动态规划 1 不同路径2 最长公共子序列3 买卖股票的最佳时机含手续费4 编辑距离1 - 4 解题总结 1 不同路径 题目: 一个机器人位于一个 m x n 网格的左上角…

Python发送QQ邮件

使用Python的smtplib可以发送QQ邮件,代码如下 #!/usr/bin/python3 import smtplib from email.mime.text import MIMEText from email.header import Headersender 111qq.com # 发送邮箱 receivers [222qq.com] # 接收邮箱 auth_code "abc" # 授权…

力扣 518. 零钱兑换 II

题目来源:https://leetcode.cn/problems/coin-change-ii/description/ C题解(来源代码随想录): 这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。但本题和纯完全背包不一样&#xff0c…

【AI大模型】训练Al大模型 (上篇)

大模型超越AI 前言 洁洁的个人主页 我就问你有没有发挥! 知行合一,志存高远。 目前所指的大模型,是“大规模深度学习模型”的简称,指具有大量参数和复杂结构的机器学习模型,可以处理大规模的数据和复杂的问题&#x…

K8S系列一:概念入门

写在前面 本文组织方式: K8S的架构、作用和目的。需要首先对K8S整体有所了解。 K8S是什么? 为什么是K8S? K8S怎么做? K8S的重要概念,即K8S的API对象。要学习和使用K8S必须知道和掌握的几个对象。 Pod 实例 Volume 数…

修改 el-select 背景图 样式

1. 原图------------效果图 2. css /***********大的背景图***************/ .el-popper.is-pure {background: url(/src/assets/imgList/memuBG.png) no-repeat;border: none;background-size: 100% 100%; }/*********选中行的字体***********/ .el-select-dropdown__item.s…

pytest运行时参数说明,pytest详解,pytest.ini详解

一、Pytest简介 1.pytest是一个非常成熟的全功能的Python测试框架,主要有一下几个特点: 简单灵活,容易上手,支持参数化 2.能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium、appium等自动化测试&#xf…

编程练习(2)

一.选择题 第一题: 考察转义字符和strlen函数求解字符串长度 进一步在VS中可以智能看出哪些字符是转义字符: 因此本体答案选择B 第二题: 本体较为简单,宏定义了三个数N,M,NUM,N值为2,M值为3,因此NUM值为8,…

PHP 公交公司充电桩管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 公交公司充电桩管理系统是一套完善的web设计系统,对理解php编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。 源码下载 https://download.csdn.net/download/qq_41221322/88220946 论文下…

springboot艰难版本升级之路!! springboot 2.3.x版本升级到2.7.x版本

文章目录 1.缘起1.1 升级到版本2.7.12启动失败,而且没有报错信息1.2 application-dev.yml 配置加载问题1.3 openfeign依赖问题汇总1.4 datasource报错1.5 MySQL驱动升级1.6 循环依赖报错1.7 跨域错误临时总结1.缘起 由于服务需要搭建链路追踪, 需要把springboot版本升级到2.7.1…

怎么对mp4视频进行压缩?分享了几个不错的方法

怎么对mp4视频进行压缩?这个问题非常重要。确实,MP4视频文件由于包含音频和图像,通常会占据较大的存储空间。如果我们在手机或电脑上保存过多的MP4视频文件,随着时间的积累,会导致存储容量不足的问题。另外&#xff0c…

安装ubuntu22.04系统,配置国内源以及ssh远程登录

一、安装ubuntu22.04系统 原文连接:Ubuntu操作系统22.04版本安装教程-VMware虚拟机_wx63f86e949a470的技术博客_51CTO博客 1.点击界面左侧的开启此虚拟机,即可进入Ubuntu操作系统安装界面,点击​​Try or Install Ubuntu ​​即可开始安装 …

VIOOVI:掌握三大原则轻松学会企业生产动作分析

企业实施动作分析的目的,就是要找出既省时又省力的操作方法。使操作人员最大限度地发挥其最大效能,创建合理的现场环境、机械及工具。 在实际操作中,掌握三大原则学会企业生产动作分析: 和人体相关的原则。在实际企业生产动作分析…

vscode + python

序 参考链接: 【教程】VScode中配置Python运行环境_哔哩哔哩_bilibili Python部分 Python Releases for Windows | Python.org vscode部分 Visual Studio Code - Code Editing. Redefined 一路next,全部勾上: 就可以了: 安装插…

LLaMA长度外推高性价比trick:线性插值法及相关改进源码阅读及相关记录

前言 最近,开源了可商用的llama2,支持长度相比llama1的1024,拓展到了4096长度,然而,相比GPT-4、Claude-2等支持的长度,llama的长度外推显得尤为重要,本文记录了三种网络开源的RoPE改进方式及相…