为何会写这篇文章呢?主要是以前调研过日志脱敏相关的一些,具体能够参考LOG4j脱敏插件如何编写 里面描述了日志脱敏插件编写方法:html
期中二三两个已经实现了,开发这个的确比较有趣,本身的知识面也获得了扩展,后续会经过写4-5篇的文章,一步一步的带你们如何去实现这些有趣的工具,学会了以后,经过你们丰富的想象力相信能实现更多有意思的东西。java
例如我这篇文章要介绍的经过修改字节码去实现日志脱敏,其实就是修改toString的字节码: 能够看看怎么用:git
@Desensitized
public class StreamDemo1 {
private User user;
@DesFiled(MobileDesFilter.class)
private String name;
private String idCard;
@DesFiled(AddressDesFilter.class)
private List<String> mm;
public static void main(String[] args) throws IOException {
StreamDemo1 streamDemo1 = new StreamDemo1();
streamDemo1.setUser(new User());
streamDemo1.setName("18428368642");
streamDemo1.setIdCard("22321321321");
streamDemo1.setMm(Arrays.asList("北京是朝阳区打撒所大所大","北京是朝阳区打撒所大所大"));
System.out.println(streamDemo1);
}
@Override
public String toString() {
return "StreamDemo1{" +
"user=" + user +
", name='" + name + '\'' + ", idCard='" + idCard + '\'' + ", mm=" + mm + '}'; } } 复制代码
这个类很普通对吧,和其余的实体类,惟一的区别是多了一个注解: @DesFiled(MobileDesFilter.class),有了这个注解咱们执行这个main方法:他会输出:程序员
StreamDemo1{user=bean.User@22d8cfe0, name='184****4777', idCard='22321321321', mm=[北京是朝阳区打*****, 北京是朝阳区打*****]}
复制代码
能够看见咱们明明输入的是不带号的手机号,为何输出缺带号了呢,这就是操纵字节码的神奇。固然你们也能够本身扩展思惟,你能够用他来作aop切面,固然cglib作切面的确也是操纵的字节码,你也能够用它来作你想让它作的事github
另外一方面我也调研了lombok的实现,对此我发现修改抽象语法树,彷佛更加有趣,你能够想象,你平时是否重复的给每一个方法打印入参出参,耗时耗力?你平时是否在为缺乏关键的日志而感到想骂人?你平时是否惧怕用写AOP用反射打日志会影响性能?为了解决这个问题作了一个意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida123/slothlog.git (固然也求各位大佬们给点star,O(∩_∩)O哈哈~)。面试
@LogInfo
public class DemoService {
public String hello(String name, int age){
System.out.println(name + age + "hello");
return name+age;
}
public static void main(String[] args) {
DemoService demoService = new DemoService();
demoService.hello("java", 100);
}
}
复制代码
经过上面会输出如下信息,将方法的出参,入参都进行输出,脱离了调试时缺乏日志的苦恼数组
[INFO ] 2018-07-20 20:02:42,219 DemoService.main invoke start args: {}
[INFO ] 2018-07-20 20:02:42,220 DemoService.hello invoke start name: java ,age: 100
java100hello
[INFO ] 2018-07-20 20:02:42,221 DemoService.hello invoke end name: java ,age: 100 , result: java100
复制代码
后续我会一步一步的教你们如何去完成一个相似Lombok的修改语法树的框架,作更多有趣的事。bash
若是你不喜欢上面这些东西,也别着急,字节码是java的基础,我以为是全部Java程序员须要必备的,固然你也有必要了解一下。 本篇是系列的第一篇,这篇主要讲的主要是字节码是什么,经过对这篇的了解,也是后续章节的基础。网络
机器码(machine code)顾名思义也就是,机器能识别的代码,也叫原生码。机器码是CPU可直接解读的指令。机器码与硬件等有关,不一样的CPU架构支持的硬件码也不相同。机器码是和咱们的底层硬件直接打交道,如今学的人也是逐渐的变少了,若是对这个感兴趣的同窗能够去学习一下汇编,汇编的指令会被翻译成机器码。数据结构
字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是程序的中间表示形式:介于人类可读的源码和机器码之间。它常常被看做是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是由于一般每一个操做码 是一字节长,因此字节码的程度是根据一字节来的。字节码也是由,一组操做码组成,而操做码其实是对栈的操做,能够移走参数和地址空间,也能够放入结果。JAVA经过JIT(即时编译)能够将字节码转换为机器码。
字节码的实现方式是经过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为能够直接执行的指令。在java中通常是用Javac编译源文件变成字节码,也就是咱们的class文件。
从网络上找到了两张图片,下面是java源码编译器生成字节码过程:
java虚拟机执行引擎过程,这里会分为两个阶段:
普通的代码(非热)都是走的字节码解释器
热代码:屡次调用的方法,屡次执行的循环体,会被JIT优化成机器码。
方法调用在JVM中转换成的是字节码执行,字节码指令执行的数据结构就是栈帧(stack frame)。也就是在虚拟机栈中的栈元素。虚拟机会为每一个方法分配一个栈帧,由于虚拟机栈是LIFO(后进先出)的,因此当前线程正在活动的栈帧,也就是栈顶的栈帧,JVM规范中称之为“CurrentFrame”,这个当前栈帧对应的方法就是“CurrentMethod”。字节码的执行操做,指的就是对当前栈帧数据结构进行的操做。
JVM的运行时数据区的结构以下图:。
咱们这里主要讨论栈帧的数据结构:有四个部分,局部变量区,操做数栈,动态连接,方法的返回地址。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在Code属性中locals变量:
以下面代码反编译后就能看见locals=5。
局部变量的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot能够存放一个32位之内的数据类型(boolean、byte、char、short、int、float、reference(引用)和returnAddress八种)。
同时Slot对对象的引用会影响GC,(要是被引用,不会被回收)。
系统不会为局部变量赋予初始值,也就是说不存在类变量那样的准备阶段。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,若是是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中经过this访问。
咱们上面的代码中是4个Int的solt加一个this 的solt因此就等于5。
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操做数栈。
操做数栈同局部变量表同样,也是编译期间就能决定了其存储空间(最大的单位长度),经过 Code属性存储在类或接口的字节流中。操做数栈也是个LIFO栈。 它不是经过索引来访问,而是经过标准的栈操做—压栈和出栈—来访问的。好比,若是某个指令把一个值压入到操做数栈中,稍后另外一个指令就能够弹出这个值来使用。
虚拟机在操做数栈中存储数据的方式和在局部变量区中是同样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操做数栈以前,也会被转换为int。
动态连接就是将符号引用所表示的方法,转换成方法的直接引用。加载阶段或第一次使用时转化为直接引用的(将变量的访问转化为访问这些变量的存储结构所在的运行时内存位置)就叫作静态解析。JVM的动态连接还支持运行期转化为直接引用。也能够叫作Late Binding,晚期绑定。动态连接是java灵活OO的基础结构。
注:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时能够找到相应的位置。你好比说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。
当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次以后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,经过偏移量虚拟机能够直接在该类的内存区域中找到方法字节码的起始位置。重写就是动态连接,重载就是静态解析。
**方法正常退出,JVM执行引擎会恢复上层方法局部变量表操做数栈并把返回值压入调用者的栈帧的操做数栈,PC计数器的值就会调整到方法调用指令后面的一条指令。**这样使得当前的栈帧可以和调用者链接起来,而且让调用者的栈帧的操做数栈继续往下执行。 方法的异常调用完成,若是异常没有被捕获住,或者遇到athrow字节码指令显示抛出,那么就没有返回值给调用者。
加载和存储指令用于将数据从栈帧的局部变量表和操做数栈之间来回传输。
1)将一个局部变量加载到操做数栈的指令包括:iload,iload_,lload、lload、float、 fload_、dload、dload_,aload、aload。
2)将一个数值从操做数栈存储到局部变量表的指令:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_
3)将常量加载到操做数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
4)局部变量表的访问索引指令:wide
算术指令用于对两个操做数栈上的值进行某种特定运算,并把结果从新存入到操做栈顶。
1)加法指令:iadd,ladd,fadd,dadd
2)减法指令:isub,lsub,fsub,dsub
3)乘法指令:imul,lmul,fmul,dmul
4)除法指令:idiv,ldiv,fdiv,ddiv
5)求余指令:irem,lrem,frem,drem
6)取反指令:ineg,leng,fneg,dneg
7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr
8)按位或指令:ior,lor
9)按位与指令:iand,land
10)按位异或指令:ixor,lxor
11)局部变量自增指令:iinc
12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
Java虚拟机没有明确规定整型数据溢出的状况,但规定了处理整型数据时,只有除法和求余指令出现除数为0时会致使虚拟机抛出异常。
Java虚拟机要求在浮点数运算的时候,全部结果否必须舍入到适当的精度,若是有两种可表示的形式与该值同样,会优先选择最低有效位为零的。称之为最接近数舍入模式。
浮点数向整数转换的时候,Java虚拟机使用IEEE 754标准中的向零舍入模式,这种模式舍入的结果会致使数字被截断,全部小数部分的有效字节会被丢掉。
类型转换指令将两种Java虚拟机数值类型相互转换,这些操做通常用于实现用户代码的显式类型转换操做。JVM直接就支持宽化类型转换(小范围类型向大范围类型转换):
1.int类型到long,float,double类型
2.long类型到float,double类型
3.float到double类型
但在处理窄化类型转换时,必须显式使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。将int 或 long 窄化为整型T的时候,仅仅简单的把除了低位的N个字节之外的内容丢弃,N是T的长度。这有可能致使转换结果与输入值有不一样的正负号。
在将一个浮点值窄化为整数类型T(仅限于 int 和 long 类型),将遵循如下转换规则:
1)若是浮点值是NaN , 那转换结果就是int 或 long 类型的0
2)若是浮点值不是无穷大,浮点值使用IEEE 754 的向零舍入模式取整,得到整数v, 若是v在T表示范围以内,那就是v
3)不然,根据v的符号, 转换为T 所能表示的最大或者最小正数
虽然类实例和数组都是对象,Java虚拟机对类实例和数组的建立与操做使用了不一样的字节码指令。
1)建立实例的指令:new
2)建立数组的指令:newarray,anewarray,multianewarray
3)访问字段指令:getfield,putfield,getstatic,putstatic
4)把数组元素加载到操做数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
5)将操做数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
6)取数组长度指令:arraylength JVM支持方法级同步和方法内部一段指令序列同步,这两种都是经过moniter实现的。
7)检查实例类型指令:instanceof,checkcast
如同操做一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操做操做舒展的指令,包括:
1)将操做数栈的栈顶一个或两个元素出栈:pop、pop2
2)复制栈顶一个或两个数值并将复制值或双份的复制值从新压入栈顶:dup、dup二、dup_x一、dup2_x一、dup_x二、dup2_x2。
3)将栈最顶端的两个数值互换:swap
让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:
1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等
2)复合条件分支:tableswitch,lookupswitch
3)无条件分支:goto,goto_w,jsr,jsr_w,ret
JVM中有专门的指令集处理int和reference类型的条件分支比较操做,为了能够无明显标示一个实体值是不是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操做,都使用int类型的比较指令完成,而 long,float,double条件分支比较操做,由相应类型的比较运算指令,运算指令会返回一个整型值到操做数栈中,随后再执行int类型的条件比较操做完成整个分支跳转。各类类型的比较都最终会转化为int类型的比较操做。
invokevirtual指令:调用对象的实例方法,根据对象的实际类型进行分派(虚拟机分派)。
invokeinterface指令:调用接口方法,在运行时搜索一个实现这个接口方法的对象,找出合适的方法进行调用。
invokespecial:调用须要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
invokestatic:调用类方法(static)
方法返回指令是根据返回值的类型区分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn(long),freturn,drturn(double)和areturn(引用地址),另一个return供void方法,实例初始化方法,类和接口的类初始化i方法使用。
在Java程序中显式抛出异常的操做(throw语句)都有athrow 指令来实现,除了用throw 语句显示抛出异常状况外,Java虚拟机规范还规定了许多运行时异常会在其余Java虚拟机指令检测到异常情况时自动抛出。在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。
方法级的同步是隐式的,无需经过字节码指令来控制,它实如今方法调用和返回操做中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是不是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,而后执行方法,最后完成方法时释放moniter。同步一段指令集序列,一般由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。
大多数的指令有前缀和(或)后缀来代表其操做数的类型。
这一节将给你们分析如何一步一步的分析字节码。
有以下简单代码,下面代码是一个简单的demo,有一个常量,有一个类成员变量,同时方法有三个,一个构造方法,一个get(),一个静态main方法,用来输出信息。
package java8;
public class ByteCodeDemo {
private static final String name = "xiaoming";
private int age;
public ByteCodeDemo(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12);
System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge());
}
}
复制代码
用命令行找到咱们这段代码所在的路径,输入以下命令:
javac ByteCodeDemo.java
javap -p -v ByteCodeDemo
复制代码
有关Javap命令能够用help或者参考javap命令,咱们这里用的-p,-v输出全部类和成员信息,以及附加信息(文件路径,文件大小,常量池等等)
Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class
Last modified 2018-5-8; size 861 bytes
MD5 checksum d225c0249912bec4b11c41a0a52e6418
Compiled from "ByteCodeDemo.java"
public class java8.ByteCodeDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#31 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#32 // java8/ByteCodeDemo.age:I
#3 = Class #33 // java8/ByteCodeDemo
#4 = Methodref #3.#34 // java8/ByteCodeDemo."<init>":(I)V
#5 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Class #37 // java/lang/StringBuilder
#7 = Methodref #6.#31 // java/lang/StringBuilder."<init>":()V
#8 = String #38 // name:xiaomingage:
#9 = Methodref #6.#39 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #3.#40 // java8/ByteCodeDemo.getAge:()I
#11 = Methodref #6.#41 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #6.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #43.#44 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = Class #45 // java/lang/Object
#15 = Utf8 name
#16 = Utf8 Ljava/lang/String;
#17 = Utf8 ConstantValue
#18 = String #46 // xiaoming
#19 = Utf8 age
#20 = Utf8 I
#21 = Utf8 <init>
....省略部分
#58 = Utf8 (Ljava/lang/String;)V
{
private static final java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String xiaoming
private int age;
descriptor: I
flags: ACC_PRIVATE
public java8.ByteCodeDemo(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field age:I
9: return
LineNumberTable:
line 18: 0
line 19: 4
line 20: 9
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
LineNumberTable:
line 23: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #3 // class java8/ByteCodeDemo
3: dup
4: bipush 12
6: invokespecial #4 // Method "<init>":(I)V
9: astore_1
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
13: new #6 // class java/lang/StringBuilder
16: dup
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
20: ldc #8 // String name:xiaomingage:
22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: aload_1
26: invokevirtual #10 // Method getAge:()I
29: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
LineNumberTable:
line 27: 0
line 28: 10
line 29: 38
}
SourceFile: "ByteCodeDemo.java"
复制代码
若是你是第一次用javap,那你必定会以为这个是啥又臭又长,别着急下面我会一句一句给你翻译,这里你须要对照上面的字节码指令,一步一步的带你翻译。
Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class //输出了咱们的class文件的完整路径
Last modified 2018-5-8; size 861 bytes //以及class文件修改时间以及大小
MD5 checksum d225c0249912bec4b11c41a0a52e6418 //md5校验和
Compiled from "ByteCodeDemo.java" //从哪一个文件编译而来
public class java8.ByteCodeDemo
minor version: 0
major version: 52 //java主版本 major_version.minor_version 组成咱们的版本号52.0
flags: ACC_PUBLIC, ACC_SUPER //public,ACC_SUPER用于兼容早期编译器,新编译器都设置该标记,以在使用 invokespecial指令时对子类方法作特定处理。
Constant pool:
#1 = Methodref #14.#31 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#32 // java8/ByteCodeDemo.age:I
#3 = Class #33 // java8/ByteCodeDemo
.........
复制代码
部分信息在后面已经注释解释, 咱们主要来讲一下咱们的Constant pool,常量池:
在Java字节码中,有一个常量池,用来存放不一样类型的常量。因为Java设计的目的之一就是字节码须要经网络传输的,于是字节码须要比较紧凑,以减小网络传输的流量和时间。常量池的存在则可让一些相同类型的值经过索引(引用)的方式从常量池中找到,而不是在不一样地方有不一样拷贝,缩减了字节码的大小。
tag中表示的数据类型,有以下11种,:
CONSTANT_Class_info
CONSTANT_Integer_info
CONSTANT_Long\info
CONSTANT_Float_info
CONSTANT_Double_info
CONSTANT_String_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_InterfaceMethodref_info
CONSTANT_NameAndType_info
CONSTANT_Utf8_info
注:在Java字节码中,全部boolean、byte、char、short类型都是用int类型存放,于是在常量池中没有和它们对应的项。 有关常量池的介绍能够参照这里:
http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html
这里把main方法单独复制了出来,每一句话都进行了解释。
在看下面以前,能够本身尝试一下是否能将main方法字节码看懂
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V //方法描述,入参是String,返回是void
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1 //栈深最大3,局部变量2,args_size入参是1(若是是实体方法会把this也算入参)
0: new #3 // class java8/ByteCodeDemo new指令建立对象,这里引用了常量池的class 因此这里一共占了三行 2个字节是class
//一个字节是new,因此下个行号是 0+3 = 3 并把当前申请的空间地址放到栈顶
3: dup //将栈顶cpoy一份再次放入栈顶,也就是咱们上面的空间地址
4: bipush 12 //取常量12放入栈空间
6: invokespecial #4 // Method "<init>":(I)V //执行初始化方法这个时候会用到4的栈顶,和3的栈顶,弹出
9: astore_1 //将栈顶放入局部变量,也就是0的空间地址,这个时候栈是空的
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; //获取这个方法地址到栈顶
13: new #6 // class java/lang/StringBuilder 把新开辟的空间地址放到栈顶
16: dup //复制一份
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V //弹出栈顶
20: ldc #8 // String name:xiaomingage://取常量到栈顶
22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//弹出栈顶两个元素,压入StringBuilder的引用
25: aload_1 // 把局部变量,也就是咱们刚才的空间地址压入
26: invokevirtual #10 // Method getAge:()I //弹出栈顶,获取年龄,把年龄压入栈顶
29: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//弹出栈顶两个元素,压入StringBuilder
32: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//弹出栈顶两个元素,压入toString
35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//弹出栈顶两个元素,此时栈空
38: return //返回
LineNumberTable: //字节码偏移量到源代码行号之间的联系
line 29: 0
line 30: 10
line 31: 38
}
复制代码
思考:这里看懂了以后,你们能够本身尝试下本身写个稍微复杂的字节码,而后进行理解,加深一下映像。
下一篇字节码之ASM,下一篇将会给你们详细讲解如何经过asm去操做字节码,以及如何去实现咱们上面的功能,喜欢这一系列能够关注公众号,不丢失文章
若是你们以为这篇文章对你有帮助,或者想提早获取后续章节文章,或者你有什么疑问想提供1v1免费vip服务,均可以关注个人公众号,关注便可免费领取上百G最新java学习资料视频,以及最新面试资料,你的关注和转发是对我最大的支持,O(∩_∩)O: