在上一篇中有提到spring aop的动态字节码增强,我自己也没看过spring 的实现方式,按照大家的说法应该是动态生产一个子类去重写方法,由于自己没去看过,暂且不表,接下去,可能还是打算从分析字节码的角度去看类似于spring aop这个功能反应到字节码有哪些变化,或者说实现方式,
这个例子还是基于最简单的HelloWorld,还请大家回顾一下前面的几个章节,最要是这个 HelloWorld.class 文件的解读 这个例子和前面两个例子一下将基于它做一些变化,再去从字节码的角度去比较看看究竟做了什么。
首先考虑aop的一个最简单应用场景,就是日志输出,假设现在需要在输出hello world 的前后都打印日志:代码如下:
public class HelloWorld{
public static void main(String [] arvgs){
System.out.println("before log");
System.out.println("hello world");
System.out.println("after log");
}
}
编译后的class 文件如下:
00000000h: CA FE BA BE 00 00 00 32 00 21 0A 00 08 00 11 09 ; 漱壕...2.!......
00000010h: 00 12 00 13 08 00 14 0A 00 15 00 16 08 00 17 08 ; ................
00000020h: 00 18 07 00 19 07 00 1A 01 00 06 3C 69 6E 69 74 ; ...........<init
00000030h: 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 ; >...()V...Code..
00000040h: 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 ; .LineNumberTable
00000050h: 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 ; ...main...([Ljav
00000060h: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 ; a/lang/String;)V
00000070h: 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0F ; ...SourceFile...
00000080h: 48 65 6C 6C 6F 57 6F 72 6C 64 2E 6A 61 76 61 0C ; HelloWorld.java.
00000090h: 00 09 00 0A 07 00 1B 0C 00 1C 00 1D 01 00 0A 62 ; ...............b
000000a0h: 65 66 6F 72 65 20 6C 6F 67 07 00 1E 0C 00 1F 00 ; efore log.......
000000b0h: 20 01 00 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64 01 ; ...hello world.
000000c0h: 00 09 61 66 74 65 72 20 6C 6F 67 01 00 0A 48 65 ; ..after log...He
000000d0h: 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F ; lloWorld...java/
000000e0h: 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 ; lang/Object...ja
000000f0h: 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 ; va/lang/System..
00000100h: 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F 2F ; .out...Ljava/io/
00000110h: 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A ; PrintStream;...j
00000120h: 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 ; ava/io/PrintStre
00000130h: 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 ; am...println...(
00000140h: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E ; Ljava/lang/Strin
00000150h: 67 3B 29 56 00 21 00 07 00 08 00 00 00 00 00 02 ; g;)V.!..........
00000160h: 00 01 00 09 00 0A 00 01 00 0B 00 00 00 1D 00 01 ; ................
00000170h: 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 ; ......*?.?....
00000180h: 0C 00 00 00 06 00 01 00 00 00 01 00 09 00 0D 00 ; ................
00000190h: 0E 00 01 00 0B 00 00 00 3D 00 02 00 01 00 00 00 ; ........=.......
000001a0h: 19 B2 00 02 12 03 B6 00 04 B2 00 02 12 05 B6 00 ; .?...?.?...?
000001b0h: 04 B2 00 02 12 06 B6 00 04 B1 00 00 00 01 00 0C ; .?...?.?.....
000001c0h: 00 00 00 12 00 04 00 00 00 03 00 08 00 04 00 10 ; ................
000001d0h: 00 05 00 18 00 06 00 01 00 0F 00 00 00 02 00 10 ; ................
接下去首先将这个class文件和原始的HelloWorld的class 的文件对比,为了大家对比方面,把前面的class文件在拿过来:
00000000h: CA FE BA BE 00 00 00 32 00 1D 0A 00 06 00 0F 09 ; 漱壕...2........
00000010h: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07 ; ................
00000020h: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 ; .....<init>...()
00000030h: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E ; V...Code...LineN
00000040h: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 ; umberTable...mai
00000050h: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 ; n...([Ljava/lang
00000060h: 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 ; /String;)V...Sou
00000070h: 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57 ; rceFile...HelloW
00000080h: 6F 72 6C 64 2E 6A 61 76 61 0C 00 07 00 08 07 00 ; orld.java.......
00000090h: 17 0C 00 18 00 19 01 00 0B 68 65 6C 6C 6F 20 77 ; .........hello w
000000a0h: 6F 72 6C 64 07 00 1A 0C 00 1B 00 1C 01 00 0A 48 ; orld...........H
000000b0h: 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 ; elloWorld...java
000000c0h: 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A ; /lang/Object...j
000000d0h: 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 ; ava/lang/System.
000000e0h: 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F ; ..out...Ljava/io
000000f0h: 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 ; /PrintStream;...
00000100h: 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 ; java/io/PrintStr
00000110h: 65 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 ; eam...println...
00000120h: 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 ; (Ljava/lang/Stri
00000130h: 6E 67 3B 29 56 00 21 00 05 00 06 00 00 00 00 00 ; ng;)V.!.........
00000140h: 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D 00 ; ................
00000150h: 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 ; .......*?.?...
00000160h: 00 0A 00 00 00 06 00 01 00 00 00 01 00 09 00 0B ; ................
00000170h: 00 0C 00 01 00 09 00 00 00 25 00 02 00 01 00 00 ; .........%......
00000180h: 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01 00 ; ..?...?.?....
00000190h: 0A 00 00 00 0A 00 02 00 00 00 03 00 08 00 04 00 ; ................
000001a0h: 01 00 0D 00 00 00 02 00 0E ; .........
这里不会对全部的class 文件进行说明,大家可以参考 HelloWorld.class 文件的解读 和 helloWorld.class -方法解读
首先从常量池开始看,改动后的HelloWorld(下面称 HelloWorld后),有32 个常量,而改动前的HelloWorld(下面称HelloWorld前)只有28个常量,改动后多了4个常量,大家可以假设我们多输出了两个字符串,会多两个常量,那么还有两个呢?还记得constant_String 类型吗?或者说有没有主要到我们一般不会直接使用constant_UTF8类型的值,一般都有一个具体的类型来引用它,所以剩下的两个便是constant_String 类型的引用,来引用我们要输出的字符串,那么多了两个常量对其他的常量会有什么影响呢,很容易想到,其他的常量的引用可能会增加1到4个索引
好了,有了前面的理解,接下去在去仔细看看具体的常量:
1、先看第一号常量:tag=0X 0A 为一个 constant_methodref 类型(对一个类中申明的方法的符号引用),根据它的定义,后面四个字节属于它,class_index=0X00 08,name_and_type_index=0X00 11 ;和HelloWorld前相比会发现两个index 都后移2;接下去2号常量一样也只是所以后偏移了2位
2、接下来看3、4、5、6号常量,对应到HelloWorld前应该是3、4号常量,先看HelloWorld前的意思,3号表示要输出的字符串,4号表示println方法,在看HelloWorld后,3号表示一个输出的字符串 “before log”,4号任然表示println方法,5号表示这个字符串“hello world”,6号表示最后一个 “after log”,由于HelloWorld后都是调用println方法,而对这个方法的描述只一次是可以理解的;到这里我们已经找出了2个多出的常量,
3、接下去7、8号常量对应HelloWorld前的5、6号,可以发现也只是index向后偏移4位
4、接下去的9到19号常量对应HelloWorld前的7到17号常量,发现如果是constant_UTF8类型则内容一样,其他的也只是index 向后偏移2位或者4位
5、接下去看20号到24号常量,对应HelloWorld前18、19、20 号,同样先看HelloWorld前,18号constant_utf8类型是hello world字符串;19号时对printstream类的引用,20号时对println方法的定义;再看HelloWorld后20号时constant_utf8类型是before log 的字符串,20、22对应HelloWorld前的19、20;23号对应HelloWorld前的18号,24号表示constant_utf8类型的是after log 字符串;这里又有两个多出来的字符串常量;这样4个多出来的常量就全部找到了
6、接下去大家可以发现剩下的常量,都是index可能有2位或者4位的偏移;
7、接下我们关注 method_info结构,先说第一个method_info,表示的是init方法,我们的改动不会影响这个方法,所有跟前面一样只是引用常量池的时候index有一些变化,这里不关注;
8、接下去就是我们第三个重点了,第二个method_info结构,表示main方法;
(1)、接下去的2个字节表示access_flags=0X 00 09,表示是一个ACC_PUBLIC和ACC_STATIC 的方法,
(2)、在两个字节0X 00 0D(HelloWorld前为0B,后移2位)表示 name_index,表示常量池中第13个常量为main ,即是 main 方法,
(3)、在接下两个字节 0X00 0E(HelloWorld前为0C,后移2位) 表示desciptor_index,表示常量池第14个常量为([Ljava/lang/Str ing;)V,即是参数为String [],返回值为void;
(4)、在接下去两个字节0X 00 01 表示attribute_count,表示有1个attribute,索引接下去表示一个attribute_info 的结构;所有查看attribute_info 的结构定义
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
所以 在接下去的两个字节 0X 00 0B(HelloWorld前为09,后移2位),查看第11好常量池为Code,然后code_attribute的定义:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
在看这个结构体 attribute_name_index =0X 00 09,然后4个字节0X 00 00 00 3D(HelloWorld前为25,表示长度 为37个字节) 表示61个字节;这个61个字节是我们关注的;我们单独拿出来
HelloWorld后:
00000199h: 00 02 00 01 00 00 00 19 B2 00 02 12 03 B6 00 04 ; ........?...?.
000001a9h: B2 00 02 12 05 B6 00 04 B2 00 02 12 06 B6 00 04 ; ?...?.?...?.
000001b9h: B1 00 00 00 01 00 0C 00 00 00 12 00 04 00 00 00 ; ?..............
000001c9h: 03 00 08 00 04 00 10 00 05 00 18 00 06 ; .............
对 helloword前的解析参见这里 中的第二块
(1)、0X 00 02 表示max_stack;表示该方法执行的时候操作数栈最大的长度;这里表示操作数栈的长度为2;
(2)、0X 00 01 表示 max_locals;表示方法局部变量所需要的空间的长度
(3)、0X 00 00 00 19 表示code_length=25;即后面的25个字节为code的内容;
(4)、 B2 00 02 12 03 B6 00 04 B2 00 02 12 05 B6 00 04 B2 00 02 12 06 B6 00 04 B1 :25个字节表示的便是code 的内容;
该code[] 包含的实现该方法的JVM 的实际的字节,
- 0X B2 :getstatic 指令:表示获取指定类的静态域,并将其值压入栈顶,后面的0X 00 02 ;查看2号常量池,即将out(sysytem.out)压入栈
- 0X 12 :ldc:表示将一个常量池压入操作栈,后面的0X 03 便是这个操作数,查看第3号常量池,为berfore log,我们要输出的内容;
- 0X B6 : invokevirtual,调用实例方法,后面的0X 00 04 ,查看4号常量池 表示java/io/PrintStream的println方法,这个指令弹出两个操作数,即是调用 out.print("before log");
-
0X B2 :getstatic 指令:表示获取指定类的静态域,并将其值压入栈顶,后面的0X 00 02 ;查看2号常量池,即将out(sysytem.out)压入栈
- 0X 12 :ldc:表示将一个常量池压入操作栈,后面的0X 05 便是这个操作数,查看第5号常量池,为hello world,我们要输出的内容;
- 0X B6 : invokevirtual,调用实例方法,后面的0X 00 04 ,查看4号常量池 表示java/io/PrintStream的println方法,这个指令弹出两个操作数,即是调用 out.print("hello world");
- 0X B2 :getstatic 指令:表示获取指定类的静态域,并将其值压入栈顶,后面的0X 00 02 ;查看2号常量池,即将out(sysytem.out)压入栈
- X 12 :ldc:表示将一个常量池压入操作栈,后面的0X 06 便是这个操作数,查看第3号常量池,为after log,我们要输出的内容;
- X B6 : invokevirtual,调用实例方法,后面的0X 00 04 ,查看4号常量池 表示java/io/PrintStream的println方法,这个指令弹出两个操作数,即是调用 out.print("after log");
- 0X B1 : return ;返回void
(5)、0X 00 00 :表示exception_table_length=0;也就是说没有异常处理;
(6)、0X 00 01 :表示attributes_count=1;接下来有一个attribute_info 的结构:
1)、0X 00 0C (HelloWorld前为0A,后移2位) :表示 attribute_name_index,查看10号常量池,为LineNumberTable ;
查 看LineNumberTable 属性的定义:
2)、0X 00 00 00 12 (HelloWorld前为0A) :表示attribute_length=18,
3)、0X 00 04 :表示line_number_table_length=4,即后面有4个line_number_info 结构
3.1)、0X 00 00 表示 start_pc;新行开始时,代码数组的偏移量,该偏移量从代码数组的起始位置开始;
3.2)、0X 00 03 表示 line_number=3
3.3)、0X 00 08 表示 start_pc;新行开始时,代码数组的偏移量,该偏移量从代码数组的起始位置开始;
3.4)、0X 00 04 表示 line_number=4
3.5)、0X 00 10表示 start_pc;新行开始时,代码数组的偏移量,该偏移量从代码数组的起始位置开始;
3.6)、0X 00 05 表示 line_number=5
3.7)、0X 00 18 表示 start_pc;新行开始时,代码数组的偏移量,该偏移量从代码数组的起始位置开始;
3.8)、0X 00 06 表示 line_number=6
LineNumberTable 中包含了一些调试信息,不做讨论;
这样main方法就好了;
9、接下去表示SourceFile属性,不去关注;
10、终结一下我们比较的结构,可以发现首先是在常量池中会增加4个常量,这是由于我们多输出了两个字符串引发的,接着由于这4个常量的出现,打乱了原来常量池的顺序,导致索引大量向后偏移;最后就是main方法的coed 的字节码增加了,由原来的9个增加的 25个;再仔细看原来这个9个字节,其实前8个是方法体,最后一个是return;所以当我们增加了2个输出语句,这样3*8=24 再加1个返回就是25个字节了
最后,我们考虑如果我们需要用这种字节码增强的方式去实现aop的话,那么最大的麻烦在于需要后移原来的常量池的索引,如果能够保持原来的常量池中的常量的位置,新增的常量只是加在最后面的话,这样就可以省去大量的工作,下一篇我们将尝试用这种方法去直接修改二进制码来尝试;在下去希望可以通过程序实现我们手工做的事情;