`

CLASS 文件使用例子三--AOP字节码增强探索一

    博客分类:
  • JVM
阅读更多

上一篇中有提到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的话,那么最大的麻烦在于需要后移原来的常量池的索引,如果能够保持原来的常量池中的常量的位置,新增的常量只是加在最后面的话,这样就可以省去大量的工作,下一篇我们将尝试用这种方法去直接修改二进制码来尝试;在下去希望可以通过程序实现我们手工做的事情;

 

本站支持 pay for your wishes

2
2
分享到:
评论

相关推荐

    spring-boot aop

    默认情况下,Spring使用基于Java的代理,但对于需要在静态方法或非Spring管理对象上应用AOP的情况,可能需要使用CGLIB或AspectJ字节码代理。 5. **理解代理行为**:理解Spring AOP代理的工作方式很重要,因为这可能...

    Aop配置示例

    在Spring中,AOP主要通过两种方式实现:基于代理的AOP(Proxy-based AOP)和基于ASM字节码操作的AOP(Class-based AOP)。基于代理的AOP通常用于接口,而基于字节码的AOP则可以应用于类。在"SpringAopMvcDemo"中,...

    java字节码例子 可以动态修改类 bcel

    在Java中,我们可以使用字节码技术来实现动态代码修改、增强或优化,这在很多场景下非常有用,比如AOP(面向切面编程)、性能监控和调试工具等。 BCEL(Byte Code Engineering Library),全称Java字节码操纵库,是...

    反射实现 AOP 动态代理模式(Spring AOP 的实现 原理) - Java 例子 -

    它不仅可以处理接口代理,还可以处理基于类的代理,支持CGLIB库生成字节码实现。此外,Spring AOP还提供了一套强大的切点表达式和注解,使得切点和通知的定义更加简洁直观。 总结来说,Spring AOP通过动态代理实现...

    spring_aop例子

    Spring AOP有两种主要的实现方式:代理模式和AspectJ字节码增强。代理模式包括JDK动态代理和CGLIB代理,适用于接口或无接口的类。AspectJ则是在编译时或运行时通过字节码操作来实现切面,提供更强大的功能。 三、切...

    bytebuddy 字节码增强 创建注解

    ByteBuddy是一个强大的字节码库,它允许开发者在不使用Java代理(Java Proxy)或者ASM等底层字节码库的情况下,便捷地创建和修改Java类与接口。本资源主要关注如何使用ByteBuddy来创建和处理注解,这对于理解和实现...

    springAop与spring定时器

    基于代理的AOP主要使用JDK动态代理或CGLIB库来创建代理对象,而基于字节码的AOP则使用AspectJ库,可以在编译时或运行时修改字节码来实现切面。 `@Aspect`注解用于定义一个切面,其中可以包含多个通知方法。`@Before...

    Spring 动态代理和aop切面编程例子

    而CGLIB则是通过字节码技术生成一个类的子类来实现代理,因此即使目标类没有接口也能进行代理。Spring会根据目标对象是否实现了接口自动选择合适的代理方式。 AOP,全称Aspect Oriented Programming,是一种编程...

    AOP 的利器 ASM 3.0

    本文将详细介绍一种轻量级的Java字节码操控框架——ASM 3.0,并探讨其如何帮助实现AOP。 #### 什么是ASM? ASM是一个用于动态生成或增强现有Java类的字节码操控框架。它可以被用来直接修改`.class`文件,也可以在...

    SpringBoot第 9 讲:SpringBoot+AOP

    Spring AOP有两种实现方式:代理模式(Proxy-based AOP)和基于ASM的字节码增强(Bytecode-based AOP)。代理模式主要通过JDK动态代理或CGLIB实现,适用于接口或非最终类。字节码增强则在运行时修改类的字节码,...

    java字节码框架ASM操作字节码的方法浅析

    在上面的例子中,当向`Bazhang`类中添加新的`newFunc`方法时,我们需要使用`ClassWriter`来拼接新的字节码。 在实际使用中,你可以创建一个`ClassReader`实例来读取类的字节码,然后使用`accept`方法将解析出的信息...

    一个牛逼的 Java 字节码类库!(csdn)————程序.pdf

    Java 字节码是Java平台的一种低级表示形式,它是由JVM(Java虚拟机)理解和执行的二进制代码。字节码使得Java程序具有跨平台性,因为不同的操作系统上的JVM可以解析并运行相同的字节码。在Java中,我们可以通过API来...

    spring aop 所需jar包

    字节码增强是一种技术,它能够在类加载到JVM之前修改类的字节码,从而实现切面的织入。`aspectjweaver.jar`与Spring框架配合,可以在应用启动时自动识别和织入切面,无需额外的配置步骤。 要使用Spring AOP,你需要...

    使用Javassist对.class文件进行修改.doc

    Javaassist 是一个强大的 Java 字节码操作库,允许开发者在运行时动态修改或创建新的 Java 类。这个库在 AOP(面向切面编程)和其他需要在程序执行期间修改类行为的场景中特别有用。Javassist 提供了一套 API,使得...

    spring AspectJ aop学习

    而基于字节码的方式则利用AspectJ的编译器或 weaving agent,在运行时对字节码进行增强,实现切面逻辑。 AspectJ是AOP的一个强大实现,它提供了完整的语言支持,可以定义切面、切点、通知等概念。在Spring中,通过...

    SpringAOP 的使用(两种方式)

    CGLIB代理是在运行时动态生成字节码实现的。 #### 2. 基于注解的AOP Spring 2.5引入了基于注解的AOP,简化了配置和使用。主要涉及以下注解: - `@Aspect`:声明一个类为切面,包含通知和切入点。 - `@Before`:...

    AOP的实现机制.pdf

    在实际的AOP实现中,有两个主要的技术:代理模式和字节码操作。 - **代理模式**:通过在目标对象周围创建一个代理对象来实现AOP。当调用目标对象的方法时,代理会先执行通知,然后转发调用给目标对象。Java中的动态...

    aspectJ与XML配置的AOP

    而AspectJ则更加强大,它是一个完整的编译时和运行时AOP系统,可以进行字节码级别操作,提供更广泛的切面定义和织入时机。AspectJ支持静态织入,可以在编译时就将切面代码整合到目标类中,这使得应用的性能更好,但...

Global site tag (gtag.js) - Google Analytics